Skip to content

Commit

Permalink
check if bookmark exists, and preview title & description
Browse files Browse the repository at this point in the history
  • Loading branch information
danielyrovas committed May 6, 2024
1 parent afd8047 commit 5d77d93
Show file tree
Hide file tree
Showing 7 changed files with 160 additions and 37 deletions.
10 changes: 10 additions & 0 deletions app/src/main/java/org/yrovas/linklater/data/BookmarkMetadata.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package org.yrovas.linklater.data

import kotlinx.serialization.Serializable

@Serializable
data class BookmarkMetadata(
val url: String,
val title: String? = null,
val description: String? = null,
)
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import org.yrovas.linklater.data.Bookmark
import org.yrovas.linklater.data.BookmarkMetadata
import org.yrovas.linklater.data.LocalBookmark
import org.yrovas.linklater.domain.APIError
import org.yrovas.linklater.domain.BookmarkAPI
Expand Down Expand Up @@ -42,4 +43,8 @@ class EmptyBookmarkAPI : BookmarkAPI {
override suspend fun getTags(page: Int): Res<List<String>, APIError> {
return Ok(emptyList())
}

override suspend fun checkExists(url: String): Pair<Bookmark?, BookmarkMetadata?> {
return null to null
}
}
31 changes: 26 additions & 5 deletions app/src/main/java/org/yrovas/linklater/data/remote/LinkDingAPI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import io.ktor.client.HttpClient
import io.ktor.client.call.body
import io.ktor.client.request.get
import io.ktor.client.request.header
import io.ktor.client.request.parameter
import io.ktor.client.request.post
import io.ktor.client.request.setBody
import kotlinx.coroutines.flow.MutableStateFlow
Expand All @@ -17,6 +18,7 @@ import org.yrovas.linklater.AppScope
import org.yrovas.linklater.checkBookmarkAPIToken
import org.yrovas.linklater.checkURL
import org.yrovas.linklater.data.Bookmark
import org.yrovas.linklater.data.BookmarkMetadata
import org.yrovas.linklater.data.LocalBookmark
import org.yrovas.linklater.domain.APIError
import org.yrovas.linklater.domain.BookmarkAPI
Expand Down Expand Up @@ -78,6 +80,7 @@ class LinkDingAPI(
val response = client.get("${endpoint!!}/bookmarks/") {
header("Authorization", "Token ${token!!}")
if (page > 0) {
// parameter("offset", pageSize * page)
url.parameters.append(
"offset", (pageSize * page).toString()
)
Expand Down Expand Up @@ -125,11 +128,29 @@ class LinkDingAPI(
}
}

// @Serializable
// data class BookmarkSavedResponse(
// val id: Long,
// val url: String,
// )
override suspend fun checkExists(url: String): Pair<Bookmark?, BookmarkMetadata?> {
if (!authProvided.value) return null to null
if (url.isBlank()) return null to null

return try {
Log.d("DEBUG/net", "checkExists")
val response =
client.get("${endpoint!!}/bookmarks/check/") {
header("Authorization", "Token ${token!!}")
parameter("url", url)
}
val r = response.body<BookmarkExistsResponse>()
r.bookmark to r.metadata
} catch (e: Exception) {
null to null
}
}

@Serializable
data class BookmarkExistsResponse(
val bookmark: Bookmark?,
val metadata: BookmarkMetadata,
)

@Serializable
data class BookmarkResponse(
Expand Down
2 changes: 2 additions & 0 deletions app/src/main/java/org/yrovas/linklater/domain/BookmarkAPI.kt
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package org.yrovas.linklater.domain

import kotlinx.coroutines.flow.StateFlow
import org.yrovas.linklater.data.Bookmark
import org.yrovas.linklater.data.BookmarkMetadata
import org.yrovas.linklater.data.LocalBookmark
import org.yrovas.linklater.domain.APIError
import org.yrovas.linklater.domain.Res
Expand All @@ -13,4 +14,5 @@ interface BookmarkAPI {
suspend fun getBookmarks(page: Int, query: String? = null): Res<List<Bookmark>, APIError>
suspend fun saveBookmark(bookmark: LocalBookmark): Res<Bookmark, APIError>
suspend fun getTags(page: Int): Res<List<String>, APIError>
suspend fun checkExists(url: String): Pair<Bookmark?, BookmarkMetadata?>
}
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ fun SaveBookmarkScreen(
) {
@Suppress("NAME_SHADOWING") val state = viewModel { state() }
val isSubmitting by state.isSubmitting.collectAsState()
val bookmarkExists by state.bookmarkExists.collectAsState()
val scope = rememberCoroutineScope()

var showTagRow by remember { mutableStateOf(false) }
Expand All @@ -127,7 +128,7 @@ fun SaveBookmarkScreen(

Frame(
appBar = {
AppBar(page = "Add Bookmark", back = back) {
AppBar(page = if (bookmarkExists) "Edit Bookmark" else "Add Bookmark", back = back) {
IconButton(onClick = { state.sendEvent(Event.SubmitBookmark) }) {
Icon(
imageVector = Icons.Default.Bookmark,
Expand Down Expand Up @@ -164,6 +165,8 @@ private fun SaveBookmarkFields(
context: Context = LocalContext.current,
) {
val bookmark by state.bookmarkToSave.collectAsState()
val previewTitle by state.previewTitle.collectAsState()
val previewDescription by state.previewDescription.collectAsState()
val showPaste by state.showPaste.collectAsState()

Column(
Expand All @@ -184,34 +187,37 @@ private fun SaveBookmarkFields(

StyledTagRow(state, onTagFocus)

Spacer(modifier = Modifier.height(padding.standard))

StyledCheckBox("Share", bookmark.shared, onCheckedChange = {
state.sendEvent(Event.UpdateBookmark(shared = it))
})
StyledCheckBox("Mark as unread", bookmark.unread, onCheckedChange = {
state.sendEvent(Event.UpdateBookmark(unread = it))
})

Spacer(modifier = Modifier.height(padding.standard))

StyledTextField(
name = "Title",
value = bookmark.title ?: "",
icon = Icons.Default.Title,
placeholder = "Leave blank to use website title"
placeholder = previewTitle ?: "Leave blank to use website title"
) {
state.sendEvent(Event.UpdateBookmark(title = it.ifBlank { null }))
}

StyledTextField(
name = "Description",
value = bookmark.description ?: "",
icon = Icons.AutoMirrored.Filled.ShortText,
placeholder = "Leave blank to use website description"
placeholder = previewDescription ?: "Leave blank to use website description"
) {
state.sendEvent(Event.UpdateBookmark(description = it.ifBlank { null }))
}

TagSelectRow(state)

Spacer(modifier = Modifier.height(padding.standard))

StyledCheckBox("Share", bookmark.shared, onCheckedChange = {
state.sendEvent(Event.UpdateBookmark(shared = it))
})
StyledCheckBox("Mark as unread", bookmark.unread, onCheckedChange = {
state.sendEvent(Event.UpdateBookmark(unread = it))
})

Spacer(modifier = Modifier.height(padding.standard))

StyledTextField(
name = "Notes",
value = bookmark.notes ?: "",
Expand Down Expand Up @@ -275,19 +281,13 @@ fun SelectedTag(tag: String, onClick: (() -> Unit)) {
}
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
private fun StyledTagRow(
state: SaveBookmarkScreenState,
onFocus: (Boolean) -> Unit,
) {
var collapseTags by remember { mutableStateOf(true) }
val tagNames by state.tagNames.collectAsState()
val tags by state.tags.collectAsState()
val selectedTags by state.selectedTags.collectAsState()
val unselectedTags = remember(tags, selectedTags) {
mutableStateOf(tags - selectedTags.toSet())
}

StyledTextField(
name = "Tags",
Expand All @@ -304,6 +304,20 @@ private fun StyledTagRow(
}
}
}
}

@OptIn(ExperimentalLayoutApi::class)
@Composable
fun TagSelectRow(
state: SaveBookmarkScreenState,
) {
var collapseTags by remember { mutableStateOf(true) }
val tags by state.tags.collectAsState()
val selectedTags by state.selectedTags.collectAsState()
val unselectedTags = remember(tags, selectedTags) {
mutableStateOf(tags - selectedTags.toSet())
}

val rowSize by remember(unselectedTags.value) {
mutableIntStateOf(
max(round(unselectedTags.value.size / 3f).toInt() + 1, 8)
Expand Down Expand Up @@ -354,6 +368,7 @@ private fun StyledTagRow(
}
}
}

}

@Composable
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
package org.yrovas.linklater.ui.state

import android.util.Log
import androidx.lifecycle.viewModelScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
Expand Down Expand Up @@ -67,6 +67,16 @@ class SaveBookmarkScreenState(
private val _predictionTags = MutableStateFlow(emptyList<String>())
val predictedTags = _predictionTags.asStateFlow()

private val _previewTitle: MutableStateFlow<String?> = MutableStateFlow(null)
val previewTitle = _previewTitle.asStateFlow()

private val _previewDescription: MutableStateFlow<String?> =
MutableStateFlow(null)
val previewDescription = _previewDescription.asStateFlow()

private val _bookmarkExists = MutableStateFlow(false)
val bookmarkExists = _bookmarkExists.asStateFlow()

private val _showPaste = MutableStateFlow(bookmarkToSave.value.url.isBlank())
val showPaste = _showPaste.asStateFlow()

Expand All @@ -82,7 +92,6 @@ class SaveBookmarkScreenState(
unread: Boolean? = null,
shared: Boolean? = null,
) {
_showPaste.update { url.isNullOrBlank() }
_bookmarkToSave.update {
bookmarkToSave.value.withUpdates(
url = url,
Expand All @@ -94,6 +103,7 @@ class SaveBookmarkScreenState(
shared = shared,
)
}
_showPaste.update { bookmarkToSave.value.url.isBlank() }
}

private fun updateTagNames(tagNameString: String) {
Expand Down Expand Up @@ -158,26 +168,36 @@ class SaveBookmarkScreenState(
_tags.update { tagList.sorted() }
}

private lateinit var defaultBookmark: LocalBookmark
private fun getDefaultBookmark() {
viewModelScope.launch(Dispatchers.IO) {
updateBookmark(
defaultBookmark = LocalBookmark(
url = "",
is_archived = prefStore.getPref(
Prefs.BOOKMARK_DEFAULT_ARCHIVED, false
),
unread = prefStore.getPref(Prefs.BOOKMARK_DEFAULT_UNREAD, false),
shared = prefStore.getPref(Prefs.BOOKMARK_DEFAULT_SHARED, false)
)
setDefaultBookmark()
}
}

private fun setDefaultBookmark() {
updateBookmark(
is_archived = defaultBookmark.is_archived,
unread = defaultBookmark.unread,
shared = defaultBookmark.shared
)
}

private lateinit var defaultTags: List<String>
private fun getDefaultTags() {
viewModelScope.launch(Dispatchers.IO) {
val selectedTagList =
defaultTags =
prefStore.getPref(Prefs.BOOKMARK_DEFAULT_TAG_NAMES, "").intoTags()
setTagList((tags.value + selectedTagList).distinct())
selectedTagList.forEach {
toggleSelectTag(it)
}
setTagList((tags.value + defaultTags).distinct())
_selectedTags.update { defaultTags }
}
}

Expand All @@ -189,25 +209,75 @@ class SaveBookmarkScreenState(
}
}

private fun checkBookmarkExists(url: String): Job {
return viewModelScope.launch {
val (b, m) = bookmarkAPI.checkExists(url)
m?.title?.let { title ->
_previewTitle.update { title }
}
m?.description?.let { description ->
_previewDescription.update { description }
}

if (b != null) {
_bookmarkExists.update { true }
updateBookmark(
url = b.url,
title = b.title,
description = b.description,
notes = b.notes,
is_archived = b.is_archived,
unread = b.unread,
shared = b.shared,
)

setTagList((tags.value + b.tags).distinct()) // guard against tags not being present in database
// _tagNames.update { "" }
_selectedTags.update { emptyList() }
_selectedTags.update { b.tags }
} else {
_bookmarkExists.update { false }
}
}
}

init {
getTagList()
getDefaultBookmark()
getDefaultTags()
}

private fun clearPreview() {
_bookmarkExists.update { false }
_previewTitle.update { null }
_previewDescription.update { null }
_selectedTags.update { defaultTags }
setDefaultBookmark()
}

private var checkExistsJob: Job? = null
override fun handleEvent(event: Event) {
when (event) {
Event.SubmitBookmark -> submitBookmark()
is Event.ToggleSelectTag -> toggleSelectTag(event.tagName)
is Event.UpdateBookmark -> updateBookmark(
is Event.UpdateBookmark -> {
viewModelScope.launch {
checkExistsJob?.cancel()
clearPreview()
if (!event.url.isNullOrBlank() && checkURL(event.url)) {
checkExistsJob = checkBookmarkExists(event.url)
}
}
updateBookmark(
url = event.url,
title = event.title,
description = event.description,
notes = event.notes,
is_archived = event.is_archived,
unread = event.unread,
shared = event.shared,
)
)
}

is Event.UpdateTagNames -> updateTagNames(event.tagNames)
is Event.SelectTagPrediction -> selectTagPrediction(event.tagName)
Expand Down
Loading

0 comments on commit 5d77d93

Please sign in to comment.