diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 4f0c68cf..9355a4c7 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -152,6 +152,9 @@ dependencies { implementation(libs.ktor.client.cio) implementation(libs.ktor.server.core) implementation(libs.ktor.server.cio) + implementation(libs.ktor.server.websockets) + implementation(libs.ktor.client.websockets) + implementation(libs.ktor.server.content.negotiation) implementation(libs.ktor.serialization.kotlinx.json) implementation(libs.ktor.client.content.negotiation) diff --git a/app/src/main/assets/index.html b/app/src/main/assets/index.html index e210902f..d7214ad4 100644 --- a/app/src/main/assets/index.html +++ b/app/src/main/assets/index.html @@ -268,8 +268,83 @@

Loading links...

diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt b/app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt index 40fd01da..d52052e7 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt @@ -12,6 +12,7 @@ import com.yogeshpaliyal.deepr.backup.ExportRepository import com.yogeshpaliyal.deepr.backup.ExportRepositoryImpl import com.yogeshpaliyal.deepr.backup.ImportRepository import com.yogeshpaliyal.deepr.backup.ImportRepositoryImpl +import com.yogeshpaliyal.deepr.data.DataProvider import com.yogeshpaliyal.deepr.data.HtmlParser import com.yogeshpaliyal.deepr.data.NetworkRepository import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore @@ -133,6 +134,10 @@ class DeeprApplication : Application() { ReviewManagerFactory.create() } + single { + DataProvider(get()) + } + viewModel { TransferLinkLocalServerViewModel(get()) } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/CsvBookmarkImporter.kt b/app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/CsvBookmarkImporter.kt index 193f4798..e2da3249 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/CsvBookmarkImporter.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/CsvBookmarkImporter.kt @@ -54,8 +54,8 @@ class CsvBookmarkImporter( val tagsString = row.getOrNull(5) ?: "" val thumbnail = row.getOrNull(6) ?: "" val isFavourite = row.getOrNull(7)?.toLongOrNull() ?: 0 - val existing = deeprQueries.getDeeprByLink(link).executeAsOneOrNull() - if (link.isNotBlank() && existing == null) { + val existing = deeprQueries.isLinkExists(link).executeAsOneOrNull() ?: 0L + if (link.isNotBlank() && existing == 0L) { updatedCount++ deeprQueries.transaction { deeprQueries.importDeepr( diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/HtmlBookmarkImporter.kt b/app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/HtmlBookmarkImporter.kt index 9110f891..088c66d5 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/HtmlBookmarkImporter.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/backup/importer/HtmlBookmarkImporter.kt @@ -28,8 +28,8 @@ abstract class HtmlBookmarkImporter( val bookmarks = extractBookmarks(document) bookmarks.forEach { bookmark -> - val existing = deeprQueries.getDeeprByLink(bookmark.url).executeAsOneOrNull() - if (bookmark.url.isNotBlank() && existing == null) { + val existing = deeprQueries.isLinkExists(bookmark.url).executeAsOneOrNull() + if (bookmark.url.isNotBlank() && existing == 0L) { try { deeprQueries.transaction { deeprQueries.insertDeepr( @@ -38,6 +38,7 @@ abstract class HtmlBookmarkImporter( name = bookmark.title, notes = bookmark.folder ?: "", thumbnail = "", + isFavourite = 0 ) // Add tags if present diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/data/DataManager.kt b/app/src/main/java/com/yogeshpaliyal/deepr/data/DataManager.kt new file mode 100644 index 00000000..e2b63fa5 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/data/DataManager.kt @@ -0,0 +1,283 @@ +package com.yogeshpaliyal.deepr.data + +import app.cash.sqldelight.coroutines.asFlow +import com.yogeshpaliyal.deepr.DeeprQueries +import com.yogeshpaliyal.deepr.server.CountData +import com.yogeshpaliyal.deepr.server.CountType +import com.yogeshpaliyal.deepr.server.DeeprTag +import com.yogeshpaliyal.deepr.server.LinksListData +import com.yogeshpaliyal.deepr.server.TagsListData +import com.yogeshpaliyal.deepr.viewmodel.SortType +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +class DataProvider( + private val deeprQueries: DeeprQueries, +) { + fun getLinks( + coroutineScope: CoroutineScope, + searchQuery: Flow, + sortOrder: Flow<@SortType String>, + selectedTagFilter: Flow>, + favouriteFilter: Flow, + ): StateFlow = + combine( + searchQuery, + sortOrder, + selectedTagFilter, + favouriteFilter, + ) { query, sorting, tags, favourite -> + listOf(query, sorting, tags, favourite) + }.flatMapLatest { combined -> + val query = combined[0] as String + val sorting = (combined[1] as String).split("_") + val tags = combined[2] as List + val favourite = combined[3] as Int + val sortField = sorting.getOrNull(0) ?: "createdAt" + val sortType = sorting.getOrNull(1) ?: "DESC" + + // Prepare tag filter parameters + val tagIdsString = + if (tags.isEmpty()) "" else tags.joinToString(",") { it.id.toString() } + + deeprQueries + .getLinksAndTags( + query, + query, + query, + favourite.toLong(), + favourite.toLong(), + tagIdsString, + tagIdsString, + sortType, + sortField, + sortType, + sortField, + ).asFlow() + .map { + val dbList = it.executeAsList() + val mappedList = dbList.map { dbObj -> DeeprLink(dbObj) } + LinksListData(mappedList) + } + }.stateIn(coroutineScope, SharingStarted.WhileSubscribed(5000), null) + + fun getAllTags(coroutineScope: CoroutineScope): StateFlow = + deeprQueries + .DeeprTag() + .asFlow() + .map { + val list = + it.executeAsList().map { dto -> DeeprTag(dto.id, dto.name, dto.linkCount) } + TagsListData(list) + } + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(5000), null) + + fun countOfLinks(coroutineScope: CoroutineScope): StateFlow { + return deeprQueries + .countOfLinks() + .asFlow() + .map { + val data = it.executeAsOneOrNull() + CountData(data, CountType.TOTAL) + }.stateIn(coroutineScope, SharingStarted.WhileSubscribed(5000), null) + } + + + fun countOfFavouritesLinks(coroutineScope: CoroutineScope): StateFlow { + return deeprQueries + .countOfFavouriteLinks() + .asFlow() + .map { + val data = it.executeAsOneOrNull() + CountData(data, CountType.FAVOURITE) + }.stateIn(coroutineScope, SharingStarted.WhileSubscribed(5000), null) + } + + + fun removeTagFromLink( + linkId: Long, + tagId: Long, + ) { + + deeprQueries.removeTagFromLink(linkId, tagId) + + } + + fun addTagToLink( + linkId: Long, + tagId: Long, + ) { + deeprQueries.addTagToLink(linkId, tagId) + } + + + fun addTagToLinkByName( + linkId: Long, + tagName: String, + ) { + // Create the tag if it doesn't exist + deeprQueries.insertTag(tagName) + + // Get the tag ID + val tag = deeprQueries.getTagByName(tagName).executeAsOneOrNull() + + if (tag != null) { + // Add the tag to the link + deeprQueries.addTagToLink(linkId, tag.id) + } + } + + + fun getTagByName(tagName: String): DeeprTag? { + val dbTag = deeprQueries.getTagByName(tagName).executeAsOneOrNull() + if (dbTag != null) { + return DeeprTag(dbTag.id, dbTag.name, dbTag.linkCount) + } else { + return null + } + } + + + fun modifyTagsForLink( + linkId: Long, + tagsList: List, + ) { + + // Then add selected tags + tagsList.forEach { tag -> + if (tag.id > 0) { + // Existing tag + addTagToLink(linkId, tag.id) + } else { + // New tag + addTagToLinkByName(linkId, tag.name) + } + } + + } + + fun insertLink( + link: String, + name: String, + tagsList: List? = null, + notes: String = "", + thumbnail: String = "", + openedCount: Long = 0, + isFavourite: Long = 0, + createdAt: String? = null, + ): Long? { + if (createdAt == null) { + deeprQueries.insertDeepr( + link = link, + name, + openedCount, + notes, + thumbnail, + isFavourite + ) + } else { + deeprQueries.importDeepr( + link = link, + name, + openedCount, + notes, + thumbnail, + isFavourite, + createdAt + ) + } + val linkId = deeprQueries.lastInsertRowId().executeAsOneOrNull() + linkId?.let { linkId -> + tagsList?.let { + modifyTagsForLink(linkId, tagsList) + } + } + return linkId + } + + + fun deleteAccount(id: Long) { + + val tagsToDelete = mutableListOf() + + deeprQueries.getTagsForLink(id).executeAsList().forEach { tag -> + val linkCount = deeprQueries.hasTagLinks(tag.id).executeAsOne() + if (linkCount == 1L) { + tagsToDelete.add(tag.id) + } + } + + deeprQueries.deleteDeeprById(id) + deeprQueries.deleteLinkRelations(id) + tagsToDelete.forEach { tagId -> + deeprQueries.deleteTag(tagId) + } + + + } + + fun deleteTag(id: Long) { + deeprQueries.deleteTag(id) + deeprQueries.deleteTagRelations(id) + } + + fun updateTag(tag: DeeprTag) { + + deeprQueries.updateTag(tag.name, tag.id) + + } + + fun incrementOpenedCount(id: Long) { + + deeprQueries.incrementOpenedCount(id) + deeprQueries.insertDeeprOpenLog(id) + + } + + fun resetOpenedCount(id: Long) { + deeprQueries.resetOpenedCount(id) + } + + + fun toggleFavourite(id: Long) { + + deeprQueries.toggleFavourite(id) + + } + + fun updateDeeplink( + id: Long, + newLink: String, + newName: String, + tagsList: List, + notes: String = "", + thumbnail: String = "", + ) { + deeprQueries.updateDeeplink(newLink, newName, notes, thumbnail, id) + modifyTagsForLink(id, tagsList) + } + + fun isLinkExist(link: String): Boolean { + val dbObj = deeprQueries.isLinkExists(link).executeAsOneOrNull() + return (dbObj ?: 0L) > 0L + } + + fun insertTag(name: String) { + deeprQueries.insertTag(name) + } + + + fun runInDbTransaction(block: () -> Unit) { + deeprQueries.transaction { + block() + } + } + + +} diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/data/DeeprLink.kt b/app/src/main/java/com/yogeshpaliyal/deepr/data/DeeprLink.kt new file mode 100644 index 00000000..b41b5185 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/data/DeeprLink.kt @@ -0,0 +1,35 @@ +package com.yogeshpaliyal.deepr.data + +import com.yogeshpaliyal.deepr.GetLinksAndTags +import kotlinx.serialization.Serializable + +@Serializable +data class DeeprLink( + val id: Long, + val link: String, + val name: String, + val createdAt: String, + val openedCount: Long, + val isFavourite: Long, + val notes: String, + val thumbnail: String, + val lastOpenedAt: String?, + val tagsNames: String?, + val tagsIds: String?, + val tags: List = emptyList(), +) { + constructor(dbObj: GetLinksAndTags) : this( + id = dbObj.id, + link = dbObj.link, + name = dbObj.name, + createdAt = dbObj.createdAt, + openedCount = dbObj.openedCount, + isFavourite = dbObj.isFavourite, + notes = dbObj.notes, + thumbnail = dbObj.thumbnail, + lastOpenedAt = dbObj.lastOpenedAt, + tagsNames = dbObj.tagsNames, + tagsIds = dbObj.tagsIds, + tags = dbObj.tagsNames?.split(",")?.filter { it.isNotEmpty() } ?: emptyList(), + ) +} diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt b/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt index a7f7e2c6..77373da3 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt @@ -4,12 +4,12 @@ import android.content.Context import android.net.wifi.WifiManager import android.util.Log import com.yogeshpaliyal.deepr.BuildConfig -import com.yogeshpaliyal.deepr.DeeprQueries -import com.yogeshpaliyal.deepr.Tags import com.yogeshpaliyal.deepr.analytics.AnalyticsManager +import com.yogeshpaliyal.deepr.data.DataProvider import com.yogeshpaliyal.deepr.data.NetworkRepository import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore import com.yogeshpaliyal.deepr.viewmodel.AccountViewModel +import com.yogeshpaliyal.deepr.viewmodel.SortType import io.ktor.client.HttpClient import io.ktor.client.call.body import io.ktor.client.plugins.timeout @@ -20,6 +20,7 @@ import io.ktor.http.HttpStatusCode import io.ktor.http.URLProtocol import io.ktor.http.isSuccess import io.ktor.http.path +import io.ktor.serialization.kotlinx.KotlinxWebsocketSerializationConverter import io.ktor.serialization.kotlinx.json.json import io.ktor.server.application.install import io.ktor.server.cio.CIO @@ -33,11 +34,18 @@ import io.ktor.server.response.respondText import io.ktor.server.routing.get import io.ktor.server.routing.post import io.ktor.server.routing.routing +import io.ktor.server.websocket.WebSockets +import io.ktor.server.websocket.pingPeriod +import io.ktor.server.websocket.timeout +import io.ktor.server.websocket.webSocket import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asStateFlow +import kotlinx.coroutines.flow.collectLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.update import kotlinx.coroutines.launch import kotlinx.coroutines.withContext @@ -45,15 +53,17 @@ import kotlinx.serialization.Serializable import kotlinx.serialization.json.Json import java.net.NetworkInterface import java.util.Locale +import kotlin.time.Duration.Companion.seconds open class LocalServerRepositoryImpl( private val context: Context, - private val deeprQueries: DeeprQueries, +// private val deeprQueries: DeeprQueries, private val httpClient: HttpClient, private val accountViewModel: AccountViewModel, private val networkRepository: NetworkRepository, private val analyticsManager: AnalyticsManager, private val preferenceDataStore: AppPreferenceDataStore, + private val dataProvider: DataProvider, ) : LocalServerRepository { private var server: EmbeddedServer? = null @@ -107,6 +117,13 @@ open class LocalServerRepositoryImpl( server = embeddedServer(CIO, host = "0.0.0.0", port = port) { + install(WebSockets) { + pingPeriod = 15.seconds + timeout = 15.seconds + maxFrameSize = Long.MAX_VALUE + masking = false + contentConverter = KotlinxWebsocketSerializationConverter(Json) + } install(ContentNegotiation) { json( Json { @@ -118,6 +135,33 @@ open class LocalServerRepositoryImpl( } routing { + webSocket("/ws/updates") { + try { + dataProvider + .getLinks( + this, + flowOf(""), + flowOf(SortType.SORT_LINK_ASC), + flowOf(emptyList()), + flowOf(-1), + ).collectLatest { + if (it != null) { + sendDeeprSerialized(data = it) + } + } + + + dataProvider.getAllTags(this).collectLatest { + if (it != null) { + sendDeeprSerialized(data = it) + } + } + + } catch (e: Exception) { + Log.e("LocalServer", "WebSocket error", e) + } + } + get("/") { try { val htmlContent = @@ -142,51 +186,6 @@ open class LocalServerRepositoryImpl( } } - get("/api/links") { - try { - val links = - deeprQueries - .getLinksAndTags( - "", - "", - "", - -1L, - -1L, - "", - "", - 0L, - "DESC", - "createdAt", - "DESC", - "createdAt", - ).executeAsList() - val response = - links.map { link -> - LinkResponse( - id = link.id, - link = link.link, - name = link.name, - createdAt = link.createdAt, - isFavourite = link.isFavourite, - openedCount = link.openedCount, - notes = link.notes, - thumbnail = link.thumbnail, - tags = - link.tagsNames - ?.split(", ") - ?.filter { it.isNotEmpty() } ?: emptyList(), - ) - } - call.respond(HttpStatusCode.OK, response) - } catch (e: Exception) { - Log.e("LocalServer", "Error getting links", e) - call.respond( - HttpStatusCode.InternalServerError, - ErrorResponse("Error getting links: ${e.message}"), - ) - } - } - post("/api/links") { try { val request = call.receive() @@ -214,34 +213,8 @@ open class LocalServerRepositoryImpl( get("/api/tags") { try { // Get all tags from the database with their IDs - val allTags = deeprQueries.getAllTags().executeAsList() - val response = - allTags.map { tag -> - // Count how many links use this tag - val linkCount = - deeprQueries - .getLinksAndTags( - "", - "", - "", - -1L, - -1L, - tag.id.toString(), - tag.id.toString(), - 1L, - "DESC", - "createdAt", - "DESC", - "createdAt", - ).executeAsList() - .size - TagResponse( - id = tag.id, - name = tag.name, - count = linkCount, - ) - } - call.respond(HttpStatusCode.OK, response) + val allTags = dataProvider.getAllTags(GlobalScope).value + call.respond(HttpStatusCode.OK, allTags?.tags ?: listOf()) } catch (e: Exception) { Log.e("LocalServer", "Error getting tags", e) call.respond( @@ -358,10 +331,10 @@ open class LocalServerRepositoryImpl( } private fun importToDatabase(links: List) { - deeprQueries.transaction { + dataProvider.runInDbTransaction { links.forEach { deeplink -> - if (deeprQueries.getDeeprByLink(deeplink.link).executeAsList().isEmpty()) { - deeprQueries.importDeepr( + if (dataProvider.isLinkExist(deeplink.link).not()) { + val insertedId = dataProvider.insertLink( link = deeplink.link, name = deeplink.name, openedCount = deeplink.openedCount, @@ -371,15 +344,18 @@ open class LocalServerRepositoryImpl( createdAt = deeplink.createdAt, ) - val insertedId = deeprQueries.lastInsertRowId().executeAsOne() + if (insertedId != null) { - deeplink.tags.forEach { tagName -> - deeprQueries.insertTag(name = tagName) - val tag = deeprQueries.getTagByName(tagName).executeAsOne() - deeprQueries.addTagToLink( - linkId = insertedId, - tagId = tag.id, - ) + deeplink.tags.forEach { tagName -> + dataProvider.insertTag(name = tagName) + dataProvider.getTagByName(tagName)?.let { + dataProvider.addTagToLink( + linkId = insertedId, + tagId = it.id, + ) + } + + } } } } @@ -441,25 +417,12 @@ open class LocalServerRepositoryImpl( } } -@Serializable -data class LinkResponse( - val id: Long, - val link: String, - val name: String, - val createdAt: String, - val openedCount: Long, - val notes: String, - val thumbnail: String, - val isFavourite: Long, - val tags: List, -) - @Serializable data class TagData( val id: Long, val name: String, ) { - fun toDbTag() = Tags(id, name) + fun toDbTag() = DeeprTag(id, name, count = 0) } @Serializable @@ -486,13 +449,6 @@ data class ErrorResponse( val error: String, ) -@Serializable -data class TagResponse( - val id: Long, - val name: String, - val count: Int, -) - @Serializable data class QRTransferInfo( val ip: String, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerTransferLink.kt b/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerTransferLink.kt index 2bcaebad..1cb3ce9a 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerTransferLink.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerTransferLink.kt @@ -3,6 +3,7 @@ package com.yogeshpaliyal.deepr.server import android.content.Context import com.yogeshpaliyal.deepr.DeeprQueries import com.yogeshpaliyal.deepr.analytics.AnalyticsManager +import com.yogeshpaliyal.deepr.data.DataProvider import com.yogeshpaliyal.deepr.data.NetworkRepository import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore import com.yogeshpaliyal.deepr.viewmodel.AccountViewModel @@ -10,20 +11,20 @@ import io.ktor.client.HttpClient class LocalServerTransferLink( context: Context, - deeprQueries: DeeprQueries, httpClient: HttpClient, accountViewModel: AccountViewModel, networkRepository: NetworkRepository, preferenceDataStore: AppPreferenceDataStore, analyticsManager: AnalyticsManager, + dataProvider: DataProvider, ) : LocalServerRepositoryImpl( context, - deeprQueries, httpClient, accountViewModel, networkRepository, analyticsManager, preferenceDataStore, + dataProvider, ) { override suspend fun startServer(port: Int) { super.startServer(port) diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/server/WebSocketData.kt b/app/src/main/java/com/yogeshpaliyal/deepr/server/WebSocketData.kt new file mode 100644 index 00000000..527d4b35 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/server/WebSocketData.kt @@ -0,0 +1,68 @@ +package com.yogeshpaliyal.deepr.server + +import com.yogeshpaliyal.deepr.data.DeeprLink +import io.ktor.server.websocket.WebSocketServerSession +import io.ktor.server.websocket.receiveDeserialized +import io.ktor.server.websocket.sendSerialized +import io.ktor.util.reflect.typeInfo +import kotlinx.serialization.Serializable + +@Serializable +sealed class WebSocketData( + val dataType: String, +) + +@Serializable +data class LinksListData( + val links: List, +) : WebSocketData(dataType = "links_list") + +@Serializable +data class TagsListData( + val tags: List, +) : WebSocketData(dataType = "tags_list") + + +@Serializable +data class CountData( + val count: Long?, + val type: @CountType String, +) : WebSocketData(dataType = "links_count") + + +@Retention(AnnotationRetention.SOURCE) +@Target( + AnnotationTarget.TYPE, +) +annotation class CountType { + companion object { + const val TOTAL = "total" + const val FAVOURITE = "favourite" + } +} + +suspend inline fun WebSocketServerSession.sendDeeprSerialized(data: WebSocketData) { + sendSerialized(data, typeInfo()) +} + +suspend inline fun WebSocketServerSession.receiveDeeprDeserialized(): WebSocketData? = receiveDeserialized(typeInfo()) + +@Serializable +data class LinkResponse( + val id: Long, + val link: String, + val name: String, + val createdAt: String, + val openedCount: Long, + val notes: String, + val thumbnail: String, + val isFavourite: Long, + val tags: List, +) + +@Serializable +data class DeeprTag( + val id: Long, + val name: String, + val count: Long, +) diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/CreateShortcutDialog.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/CreateShortcutDialog.kt index da3c7e34..5c7b6ac6 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/CreateShortcutDialog.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/CreateShortcutDialog.kt @@ -11,8 +11,8 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.platform.LocalContext import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.analytics.AnalyticsManager +import com.yogeshpaliyal.deepr.data.DeeprLink import com.yogeshpaliyal.deepr.util.createShortcut import com.yogeshpaliyal.deepr.util.getShortcut import com.yogeshpaliyal.deepr.util.isShortcutSupported @@ -22,7 +22,7 @@ import org.koin.compose.koinInject @Composable fun CreateShortcutDialog( - deepr: GetLinksAndTags, + deepr: DeeprLink, onDismiss: () -> Unit, viewModel: AccountViewModel = koinViewModel(), analyticsManager: AnalyticsManager = koinInject(), diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/DeleteConfirmationDialog.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/DeleteConfirmationDialog.kt index 7b0674d9..60bf36a5 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/DeleteConfirmationDialog.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/DeleteConfirmationDialog.kt @@ -14,14 +14,14 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.withStyle -import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.data.DeeprLink @Composable fun DeleteConfirmationDialog( - deepr: GetLinksAndTags, + deepr: DeeprLink, onDismiss: () -> Unit, - onConfirm: (GetLinksAndTags) -> Unit, + onConfirm: (DeeprLink) -> Unit, modifier: Modifier = Modifier, ) { AlertDialog( diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/QRCodeDialog.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/QRCodeDialog.kt index 3284e7ae..20b85375 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/QRCodeDialog.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/components/QRCodeDialog.kt @@ -24,12 +24,12 @@ import androidx.compose.ui.unit.dp import com.lightspark.composeqr.DotShape import com.lightspark.composeqr.QrCodeColors import com.lightspark.composeqr.QrCodeView -import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.data.DeeprLink @Composable fun QrCodeDialog( - deepr: GetLinksAndTags, + deepr: DeeprLink, onDismiss: () -> Unit, ) { AlertDialog( diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItem.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItem.kt index 07bd61d0..1560d7ce 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItem.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItem.kt @@ -36,7 +36,6 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.draw.clip import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag @@ -45,9 +44,9 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.yogeshpaliyal.deepr.GetLinksAndTags +import com.yogeshpaliyal.deepr.server.DeeprTag import com.yogeshpaliyal.deepr.R -import com.yogeshpaliyal.deepr.Tags +import com.yogeshpaliyal.deepr.data.DeeprLink import com.yogeshpaliyal.deepr.ui.getDeeprItemBackgroundColor import com.yogeshpaliyal.deepr.ui.getDeeprItemTextColor import compose.icons.TablerIcons @@ -58,56 +57,56 @@ import java.util.Locale import java.util.TimeZone sealed class MenuItem( - val item: GetLinksAndTags, + val item: DeeprLink, ) { class Click( - item: GetLinksAndTags, + item: DeeprLink, ) : MenuItem(item) class Copy( - item: GetLinksAndTags, + item: DeeprLink, ) : MenuItem(item) class Shortcut( - item: GetLinksAndTags, + item: DeeprLink, ) : MenuItem(item) class ShowQrCode( - item: GetLinksAndTags, + item: DeeprLink, ) : MenuItem(item) class FavouriteClick( - item: GetLinksAndTags, + item: DeeprLink, ) : MenuItem(item) class OpenWith( - item: GetLinksAndTags, + item: DeeprLink, ) : MenuItem(item) class Edit( - item: GetLinksAndTags, + item: DeeprLink, ) : MenuItem(item) class ResetCounter( - item: GetLinksAndTags, + item: DeeprLink, ) : MenuItem(item) class Delete( - item: GetLinksAndTags, + item: DeeprLink, ) : MenuItem(item) class MoreOptionsBottomSheet( - item: GetLinksAndTags, + item: DeeprLink, ) : MenuItem(item) } @OptIn(ExperimentalFoundationApi::class) @Composable fun DeeprItem( - account: GetLinksAndTags, + account: DeeprLink, onItemClick: (MenuItem) -> Unit, onTagClick: (tag: String) -> Unit, - selectedTag: List, + selectedTag: List, isThumbnailEnable: Boolean, modifier: Modifier = Modifier, analyticsManager: com.yogeshpaliyal.deepr.analytics.AnalyticsManager = org.koin.compose.koinInject(), diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemCompact.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemCompact.kt index b02c0c43..275e4fb4 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemCompact.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemCompact.kt @@ -27,8 +27,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.data.DeeprLink import com.yogeshpaliyal.deepr.ui.getDeeprItemBackgroundColor import com.yogeshpaliyal.deepr.ui.getDeeprItemTextColor import compose.icons.TablerIcons @@ -37,7 +37,7 @@ import compose.icons.tablericons.DotsVertical @OptIn(ExperimentalFoundationApi::class) @Composable fun DeeprItemCompact( - account: GetLinksAndTags, + account: DeeprLink, onItemClick: (MenuItem) -> Unit, isThumbnailEnable: Boolean, modifier: Modifier = Modifier, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemGrid.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemGrid.kt index ae37df57..02131f1a 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemGrid.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemGrid.kt @@ -26,8 +26,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage -import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.data.DeeprLink import com.yogeshpaliyal.deepr.ui.getDeeprItemBackgroundColor import com.yogeshpaliyal.deepr.ui.getDeeprItemTextColor import compose.icons.TablerIcons @@ -36,7 +36,7 @@ import compose.icons.tablericons.DotsVertical @OptIn(ExperimentalFoundationApi::class) @Composable fun DeeprItemGrid( - account: GetLinksAndTags, + account: DeeprLink, onItemClick: (MenuItem) -> Unit, modifier: Modifier = Modifier, isThumbnailEnable: Boolean = true, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemSwipable.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemSwipable.kt index f70be059..8aef80eb 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemSwipable.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemSwipable.kt @@ -18,8 +18,8 @@ import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.data.DeeprLink import compose.icons.TablerIcons import compose.icons.tablericons.Edit import compose.icons.tablericons.Trash @@ -27,7 +27,7 @@ import kotlinx.coroutines.launch @Composable fun DeeprItemSwipable( - account: GetLinksAndTags, + account: DeeprLink, onItemClick: (MenuItem) -> Unit, modifier: Modifier = Modifier, content: @Composable () -> Unit, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt index 847e3686..d6febc38 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/Home.kt @@ -96,15 +96,14 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.yogeshpaliyal.deepr.DeeprQueries -import com.yogeshpaliyal.deepr.GetLinksAndTags +import com.yogeshpaliyal.deepr.server.DeeprTag import com.yogeshpaliyal.deepr.LocalSharedText import com.yogeshpaliyal.deepr.R import com.yogeshpaliyal.deepr.SharedLink -import com.yogeshpaliyal.deepr.Tags import com.yogeshpaliyal.deepr.analytics.AnalyticsEvents import com.yogeshpaliyal.deepr.analytics.AnalyticsManager import com.yogeshpaliyal.deepr.analytics.AnalyticsParams +import com.yogeshpaliyal.deepr.data.DeeprLink import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.ui.TopLevelRoute import com.yogeshpaliyal.deepr.ui.components.ClearInputIconButton @@ -158,7 +157,7 @@ data object Home ExperimentalMaterial3ExpressiveApi::class, ) class Dashboard2( - val mSelectedLink: GetLinksAndTags? = null, + val mSelectedLink: DeeprLink? = null, ) : TopLevelRoute { override val icon: ImageVector get() = TablerIcons.Home @@ -196,9 +195,8 @@ data class FilterTagItem( fun HomeScreen( windowInsets: WindowInsets, modifier: Modifier = Modifier, - deeprQueries: DeeprQueries = koinInject(), analyticsManager: AnalyticsManager = koinInject(), - mSelectedLink: GetLinksAndTags? = null, + mSelectedLink: DeeprLink? = null, sharedText: SharedLink? = null, resetSharedText: () -> Unit, ) { @@ -206,9 +204,10 @@ fun HomeScreen( val currentViewType by viewModel.viewType.collectAsStateWithLifecycle() val localNavigator = LocalNavigator.current val hapticFeedback = LocalHapticFeedback.current - val tags = viewModel.allTagsWithCount.collectAsStateWithLifecycle() + val tagsCollect = viewModel.allTagsWithCount.collectAsStateWithLifecycle() + val tags = tagsCollect.value?.tags ?: listOf() - var selectedLink by remember { mutableStateOf(mSelectedLink) } + var selectedLink by remember { mutableStateOf(mSelectedLink) } val selectedTag by viewModel.selectedTagFilter.collectAsStateWithLifecycle() val hazeState = rememberHazeState(blurEnabled = true) val context = LocalContext.current @@ -261,16 +260,16 @@ fun HomeScreen( } } - LaunchedEffect(selectedTag, tags.value) { + LaunchedEffect(selectedTag, tags) { // Get unique tags by merging both but first items should be selected tag and then tags - val allTagsList = tags.value + val allTagsList = tags val mergedList = mutableListOf() val mapOfSelectedList = HashMap() val alreadyAdded = HashSet() allTagsList.forEach { tag -> - mapOfSelectedList.put(tag.name, tag.linkCount) + mapOfSelectedList.put(tag.name, tag.count) } selectedTag.forEach { tag -> @@ -282,7 +281,7 @@ fun HomeScreen( allTagsList.forEach { tag -> if (alreadyAdded.contains(tag.name).not()) { alreadyAdded.add(tag.name) - mergedList.add(FilterTagItem(tag.name, tag.linkCount, false)) + mergedList.add(FilterTagItem(tag.name, tag.count, false)) } } finalTagsInfo = mergedList @@ -421,28 +420,46 @@ fun HomeScreen( contentPadding = PaddingValues(horizontal = 8.dp), ) { item { - FilterChip(selectedTag.isEmpty() && favouriteFilter == -1, { - viewModel.setFavouriteFilter(-1) - viewModel.setTagFilter(null) - }, label = { - Text(stringResource(R.string.all) + " (${totalLinks ?: 0})") - }, modifier = Modifier.animateItem(), shape = RoundedCornerShape(percent = 50)) + FilterChip( + selectedTag.isEmpty() && favouriteFilter == -1, + { + viewModel.setFavouriteFilter(-1) + viewModel.setTagFilter(null) + }, + label = { + Text(stringResource(R.string.all) + " (${totalLinks?.count ?: 0})") + }, + modifier = Modifier.animateItem(), + shape = RoundedCornerShape(percent = 50), + ) } item { - FilterChip(selectedTag.isEmpty() && favouriteFilter == 1, { - viewModel.setFavouriteFilter(1) - viewModel.setTagFilter(null) - }, label = { - Text(stringResource(R.string.favourites) + " (${favouriteLinks ?: 0})") - }, modifier = Modifier.animateItem(), shape = RoundedCornerShape(percent = 50)) + FilterChip( + selectedTag.isEmpty() && favouriteFilter == 1, + { + viewModel.setFavouriteFilter(1) + viewModel.setTagFilter(null) + }, + label = { + Text(stringResource(R.string.favourites) + " (${favouriteLinks?.count ?: 0})") + }, + modifier = Modifier.animateItem(), + shape = RoundedCornerShape(percent = 50), + ) } items(finalTagsInfo ?: listOf()) { - FilterChip(it.isSelected, { - viewModel.setSelectedTagByName(it.name) - }, label = { - Text(it.name + " (${it.count})") - }, modifier = Modifier.animateItem(), shape = RoundedCornerShape(percent = 50)) + FilterChip( + it.isSelected, + { + viewModel.setSelectedTagByName(it.name) + }, + label = { + Text(it.name + " (${it.count})") + }, + modifier = Modifier.animateItem(), + shape = RoundedCornerShape(percent = 50), + ) } } } @@ -494,7 +511,6 @@ fun HomeScreen( selectedLink?.let { HomeBottomContent( - deeprQueries = deeprQueries, selectedLink = it, ) { updatedValue -> if (updatedValue != null) { @@ -514,19 +530,19 @@ fun HomeScreen( fun Content( listState: ScrollableState, hazeState: HazeState, - selectedTag: List, + selectedTag: List, contentPaddingValues: PaddingValues, currentViewType: @ViewType Int, searchQuery: String, favouriteFilter: Int, viewModel: AccountViewModel, modifier: Modifier = Modifier, - editDeepr: (GetLinksAndTags) -> Unit = {}, + editDeepr: (DeeprLink) -> Unit = {}, ) { val accounts by viewModel.accounts.collectAsStateWithLifecycle() val isThumbnailEnable by viewModel.isThumbnailEnable.collectAsStateWithLifecycle() val showMoreBottomSheet = rememberModalBottomSheetState(skipPartiallyExpanded = true) - var showMoreSelectedItem by remember { mutableStateOf(null) } + var showMoreSelectedItem by remember { mutableStateOf(null) } val analyticsManager = koinInject() if (accounts == null) { @@ -539,9 +555,9 @@ fun Content( } val context = LocalContext.current - var showShortcutDialog by remember { mutableStateOf(null) } - var showQrCodeDialog by remember { mutableStateOf(null) } - var showDeleteConfirmDialog by remember { mutableStateOf(null) } + var showShortcutDialog by remember { mutableStateOf(null) } + var showQrCodeDialog by remember { mutableStateOf(null) } + var showDeleteConfirmDialog by remember { mutableStateOf(null) } showShortcutDialog?.let { deepr -> CreateShortcutDialog( @@ -640,7 +656,7 @@ fun Content( .hazeSource(state = hazeState) .padding(horizontal = 8.dp), contentPaddingValues = contentPaddingValues, - accounts = accounts!!, + accounts = accounts?.links ?: emptyList(), selectedTag = selectedTag, onTagClick = { viewModel.setSelectedTagByName(it) @@ -912,8 +928,8 @@ fun MenuListItem( @Composable fun DeeprList( listState: ScrollableState, - accounts: List, - selectedTag: List, + accounts: List, + selectedTag: List, contentPaddingValues: PaddingValues, onItemClick: (MenuItem) -> Unit, onTagClick: (String) -> Unit, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/HomeBottomContent.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/HomeBottomContent.kt index 212f1ab0..79218c3d 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/HomeBottomContent.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/HomeBottomContent.kt @@ -48,9 +48,9 @@ import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.yogeshpaliyal.deepr.DeeprQueries -import com.yogeshpaliyal.deepr.GetLinksAndTags +import com.yogeshpaliyal.deepr.server.DeeprTag import com.yogeshpaliyal.deepr.R -import com.yogeshpaliyal.deepr.Tags +import com.yogeshpaliyal.deepr.data.DeeprLink import com.yogeshpaliyal.deepr.ui.components.ClearInputIconButton import com.yogeshpaliyal.deepr.util.isValidDeeplink import com.yogeshpaliyal.deepr.util.normalizeLink @@ -64,8 +64,7 @@ import org.koin.compose.koinInject @OptIn(ExperimentalHazeMaterialsApi::class, ExperimentalMaterial3Api::class) @Composable fun HomeBottomContent( - deeprQueries: DeeprQueries, - selectedLink: GetLinksAndTags, + selectedLink: DeeprLink, modifier: Modifier = Modifier, viewModel: AccountViewModel = koinInject(), onSaveDialogInfoChange: ((SaveDialogInfo?) -> Unit) = {}, @@ -84,9 +83,10 @@ fun HomeBottomContent( var isFetchingMetadata by remember { mutableStateOf(false) } // Tags var newTagName by remember { mutableStateOf("") } - val allTags by viewModel.allTags.collectAsStateWithLifecycle() - val selectedTags = remember { mutableStateListOf() } - val initialSelectedTags = remember { mutableStateListOf() } + val allTagsState by viewModel.allTagsWithCount.collectAsStateWithLifecycle() + val allTags = allTagsState?.tags ?: listOf() + val selectedTags = remember { mutableStateListOf() } + val initialSelectedTags = remember { mutableStateListOf() } val isThumbnailEnable by viewModel.isThumbnailEnable.collectAsStateWithLifecycle() val isCreate = selectedLink.id == 0L @@ -119,12 +119,13 @@ fun HomeBottomContent( if (isCreate.not()) { val existingTags = selectedLink.tagsIds?.split(",")?.mapIndexed { index, tagId -> - Tags( + DeeprTag( tagId.trim().toLong(), selectedLink.tagsNames ?.split(",") ?.getOrNull(index) ?.trim() ?: "Unknown", + count = 0, ) } selectedTags.clear() @@ -140,7 +141,7 @@ fun HomeBottomContent( // Normalize the link before saving val normalizedLink = normalizeLink(deeprInfo.link) - if (isCreate && deeprQueries.getDeeprByLink(normalizedLink).executeAsOneOrNull() != null) { + if (isCreate && viewModel.isLinkExist(normalizedLink)) { Toast.makeText(context, deeplinkExistsText, Toast.LENGTH_SHORT).show() return@save } @@ -382,7 +383,7 @@ fun HomeBottomContent( } } else { // Create a temporary tag with ID 0 (will be properly created on save) - selectedTags.add(Tags(0, newTagName)) + selectedTags.add(DeeprTag(0, newTagName, 0)) } newTagName = "" // Clear input @@ -486,11 +487,7 @@ fun HomeBottomContent( modifier = Modifier, onClick = { if (isValidDeeplink(deeprInfo.link)) { - if (deeprQueries - .getDeeprByLink(deeprInfo.link) - .executeAsList() - .isNotEmpty() - ) { + if (viewModel.isLinkExist(deeprInfo.link)) { Toast .makeText( context, @@ -520,11 +517,7 @@ fun HomeBottomContent( if (isCreate) { Button(onClick = { if (isValidDeeplink(deeprInfo.link)) { - if (deeprQueries - .getDeeprByLink(deeprInfo.link) - .executeAsList() - .isNotEmpty() - ) { + if (viewModel.isLinkExist(deeprInfo.link)) { Toast .makeText( context, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/OpenCountAndTags.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/OpenCountAndTags.kt index 7e3ef7a9..c7b36f00 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/OpenCountAndTags.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/OpenCountAndTags.kt @@ -11,7 +11,7 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp -import com.yogeshpaliyal.deepr.GetLinksAndTags +import com.yogeshpaliyal.deepr.data.DeeprLink import com.yogeshpaliyal.deepr.ui.getDeeprItemTextColor import compose.icons.TablerIcons import compose.icons.tablericons.ExternalLink @@ -19,7 +19,7 @@ import compose.icons.tablericons.Tag @Composable fun OpenCountAndTags( - account: GetLinksAndTags, + account: DeeprLink, modifier: Modifier = Modifier, ) { Row(modifier = modifier, verticalAlignment = Alignment.CenterVertically) { diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/SaveCompleteDialog.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/SaveCompleteDialog.kt index cf6cd42e..0f0e806d 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/SaveCompleteDialog.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/SaveCompleteDialog.kt @@ -1,9 +1,9 @@ package com.yogeshpaliyal.deepr.ui.screens.home -import com.yogeshpaliyal.deepr.GetLinksAndTags +import com.yogeshpaliyal.deepr.data.DeeprLink data class SaveDialogInfo( - val deepr: GetLinksAndTags, + val deepr: DeeprLink, val executeAfterSave: Boolean, ) @@ -13,8 +13,8 @@ fun createDeeprObject( openedCount: Long = 0, notes: String = "", thumbnail: String = "", -): GetLinksAndTags = - GetLinksAndTags( +): DeeprLink = + DeeprLink( id = 0, name = name, link = link, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ShortcutMenuItem.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ShortcutMenuItem.kt index eac5a698..ad3a58f8 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ShortcutMenuItem.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ShortcutMenuItem.kt @@ -4,8 +4,8 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.remember import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource -import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.data.DeeprLink import com.yogeshpaliyal.deepr.util.hasShortcut import com.yogeshpaliyal.deepr.util.isShortcutSupported import compose.icons.TablerIcons @@ -13,8 +13,8 @@ import compose.icons.tablericons.Plus @Composable fun ShortcutMenuItem( - account: GetLinksAndTags, - onShortcutClick: (GetLinksAndTags) -> Unit, + account: DeeprLink, + onShortcutClick: (DeeprLink) -> Unit, ) { val context = LocalContext.current val shortcutExists = remember(account.id) { hasShortcut(context, account.id) } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ShowQRCodeMenuItem.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ShowQRCodeMenuItem.kt index 165ba9a0..9b2714df 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ShowQRCodeMenuItem.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ShowQRCodeMenuItem.kt @@ -6,15 +6,15 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource -import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.data.DeeprLink import compose.icons.TablerIcons import compose.icons.tablericons.Qrcode @Composable fun ShowQRCodeMenuItem( - account: GetLinksAndTags, - onQrCodeClick: (GetLinksAndTags) -> Unit, + account: DeeprLink, + onQrCodeClick: (DeeprLink) -> Unit, modifier: Modifier = Modifier, ) { androidx.compose.material3.ListItem( diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt index e9fad689..f765134f 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt @@ -1,7 +1,6 @@ package com.yogeshpaliyal.deepr.ui.screens.home import android.database.sqlite.SQLiteConstraintException -import android.view.Surface import android.widget.Toast import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.scaleIn @@ -11,7 +10,6 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.WindowInsets @@ -58,10 +56,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.yogeshpaliyal.deepr.DeeprQueries -import com.yogeshpaliyal.deepr.GetAllTagsWithCount import com.yogeshpaliyal.deepr.R -import com.yogeshpaliyal.deepr.Tags +import com.yogeshpaliyal.deepr.server.DeeprTag import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.ui.TopLevelRoute import com.yogeshpaliyal.deepr.ui.components.ClearInputIconButton @@ -73,7 +69,6 @@ import compose.icons.tablericons.Plus import compose.icons.tablericons.Tag import compose.icons.tablericons.Trash import kotlinx.coroutines.runBlocking -import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinActivityViewModel object TagSelectionScreen : TopLevelRoute { @@ -88,12 +83,12 @@ object TagSelectionScreen : TopLevelRoute { val viewModel: AccountViewModel = koinActivityViewModel() val selectedTag by viewModel.selectedTagFilter.collectAsStateWithLifecycle() var newTagName by remember { mutableStateOf("") } - val tagsWithCount by viewModel.allTagsWithCount.collectAsStateWithLifecycle() + val tagsWithCountState by viewModel.allTagsWithCount.collectAsStateWithLifecycle() + val tagsWithCount = tagsWithCountState?.tags ?: listOf() val context = LocalContext.current val navigator = LocalNavigator.current - val deeprQueries: DeeprQueries = koinInject() - var isTagEditEnable by remember { mutableStateOf(null) } - var isTagDeleteEnable by remember { mutableStateOf(null) } + var isTagEditEnable by remember { mutableStateOf(null) } + var isTagDeleteEnable by remember { mutableStateOf(null) } var tagEditError by remember { mutableStateOf(null) } Scaffold( @@ -189,7 +184,7 @@ object TagSelectionScreen : TopLevelRoute { Toast.LENGTH_SHORT, ).show() } else { - deeprQueries.insertTag(trimmedTagName) + viewModel.insertTag(trimmedTagName) newTagName = "" Toast .makeText( @@ -302,7 +297,7 @@ object TagSelectionScreen : TopLevelRoute { Modifier .fillMaxWidth() .clickable { - viewModel.setTagFilter(Tags(tag.id, tag.name)) + viewModel.setTagFilter(tag) }, shape = RoundedCornerShape(12.dp), colors = @@ -331,7 +326,7 @@ object TagSelectionScreen : TopLevelRoute { androidx.compose.material3.Checkbox( checked = isSelected, onCheckedChange = { - viewModel.setTagFilter(Tags(tag.id, tag.name)) + viewModel.setTagFilter(tag) }, ) @@ -351,7 +346,7 @@ object TagSelectionScreen : TopLevelRoute { ) Spacer(modifier = Modifier.height(4.dp)) Text( - text = "${tag.linkCount} ${if (tag.linkCount == 1L) "link" else "links"}", + text = "${tag.count} ${if (tag.count == 1L) "link" else "links"}", style = MaterialTheme.typography.bodySmall, color = if (isSelected) { @@ -467,7 +462,7 @@ object TagSelectionScreen : TopLevelRoute { val result = runBlocking { try { - viewModel.updateTag(Tags(tag.id, trimmedName)) + viewModel.updateTag(tag) Result.success(true) } catch (e: Exception) { return@runBlocking Result.failure(e) @@ -543,7 +538,7 @@ object TagSelectionScreen : TopLevelRoute { } Text(text = message) - if (tag.linkCount > 0) { + if (tag.count > 0) { Spacer(modifier = Modifier.height(8.dp)) Card( colors = @@ -555,7 +550,7 @@ object TagSelectionScreen : TopLevelRoute { ), ) { Text( - text = "This tag is used by ${tag.linkCount} ${if (tag.linkCount == 1L) "link" else "links"}", + text = "This tag is used by ${tag.count} ${if (tag.count == 1L) "link" else "links"}", modifier = Modifier.padding(12.dp), style = MaterialTheme.typography.bodySmall, color = MaterialTheme.colorScheme.onErrorContainer, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/util/ShortcutUtils.kt b/app/src/main/java/com/yogeshpaliyal/deepr/util/ShortcutUtils.kt index 55894a64..729239c8 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/util/ShortcutUtils.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/util/ShortcutUtils.kt @@ -7,11 +7,11 @@ import android.os.Build import androidx.core.content.pm.ShortcutInfoCompat import androidx.core.content.pm.ShortcutManagerCompat import androidx.core.net.toUri -import com.yogeshpaliyal.deepr.GetLinksAndTags +import com.yogeshpaliyal.deepr.data.DeeprLink fun createShortcut( context: Context, - deepr: GetLinksAndTags, + deepr: DeeprLink, shortcutName: String, alreadyExists: Boolean, useLinkBasedIcon: Boolean, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt b/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt index d3774c37..69d65964 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt @@ -4,20 +4,17 @@ import android.net.Uri import androidx.annotation.StringDef import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import app.cash.sqldelight.coroutines.asFlow -import app.cash.sqldelight.coroutines.mapToList -import app.cash.sqldelight.coroutines.mapToOneOrNull -import com.yogeshpaliyal.deepr.DeeprQueries -import com.yogeshpaliyal.deepr.GetAllTagsWithCount -import com.yogeshpaliyal.deepr.GetLinksAndTags -import com.yogeshpaliyal.deepr.Tags +import com.yogeshpaliyal.deepr.server.DeeprTag import com.yogeshpaliyal.deepr.analytics.AnalyticsManager import com.yogeshpaliyal.deepr.backup.AutoBackupWorker import com.yogeshpaliyal.deepr.backup.ExportRepository import com.yogeshpaliyal.deepr.backup.ImportRepository +import com.yogeshpaliyal.deepr.data.DataProvider import com.yogeshpaliyal.deepr.data.LinkInfo import com.yogeshpaliyal.deepr.data.NetworkRepository import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore +import com.yogeshpaliyal.deepr.server.CountData +import com.yogeshpaliyal.deepr.server.LinksListData import com.yogeshpaliyal.deepr.sync.SyncRepository import com.yogeshpaliyal.deepr.ui.screens.home.ViewType import com.yogeshpaliyal.deepr.util.RequestResult @@ -28,9 +25,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow -import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.first -import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.receiveAsFlow import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.flow.update @@ -69,51 +64,24 @@ annotation class SortType { } class AccountViewModel( - private val deeprQueries: DeeprQueries, private val exportRepository: ExportRepository, private val importRepository: ImportRepository, private val syncRepository: SyncRepository, private val networkRepository: NetworkRepository, private val autoBackupWorker: AutoBackupWorker, private val analyticsManager: AnalyticsManager, + private val dataProvider: DataProvider, ) : ViewModel(), KoinComponent { private val preferenceDataStore: AppPreferenceDataStore = get() private val reviewManager: com.yogeshpaliyal.deepr.review.ReviewManager = get() private val searchQuery = MutableStateFlow("") - // State for tags - val allTags: StateFlow> = - deeprQueries - .getAllTags() - .asFlow() - .mapToList( - viewModelScope.coroutineContext, - ).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), listOf()) - - val allTagsWithCount: StateFlow> = - deeprQueries - .getAllTagsWithCount() - .asFlow() - .mapToList( - viewModelScope.coroutineContext, - ).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), listOf()) - - val countOfLinks: StateFlow = - deeprQueries - .countOfLinks() - .asFlow() - .mapToOneOrNull( - viewModelScope.coroutineContext, - ).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0L) - - val countOfFavouriteLinks: StateFlow = - deeprQueries - .countOfFavouriteLinks() - .asFlow() - .mapToOneOrNull( - viewModelScope.coroutineContext, - ).stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0L) + val allTagsWithCount = dataProvider.getAllTags(viewModelScope) + + val countOfLinks: StateFlow = dataProvider.countOfLinks(viewModelScope) + + val countOfFavouriteLinks: StateFlow = dataProvider.countOfFavouritesLinks(viewModelScope) private val sortOrder: Flow<@SortType String> = preferenceDataStore.getSortingOrder @@ -131,8 +99,8 @@ class AccountViewModel( val syncValidationFlow = syncValidationChannel.receiveAsFlow() // State for tag filter - now supports multiple tags - private val _selectedTagFilter = MutableStateFlow>(emptyList()) - val selectedTagFilter: StateFlow> = _selectedTagFilter + private val _selectedTagFilter = MutableStateFlow>(emptyList()) + val selectedTagFilter: StateFlow> = _selectedTagFilter // State for favourite filter (-1 = All, 0 = Not Favourite, 1 = Favourite) private val defaultPageFavourites: Flow = preferenceDataStore.getDefaultPageFavourites @@ -149,7 +117,7 @@ class AccountViewModel( // Track user properties for total links and tags viewModelScope.launch { countOfLinks.collect { count -> - count?.let { + count?.count?.let { analyticsManager.setUserProperty( com.yogeshpaliyal.deepr.analytics.AnalyticsUserProperties.TOTAL_LINKS, it.toString(), @@ -159,10 +127,10 @@ class AccountViewModel( } viewModelScope.launch { - allTags.collect { tags -> + allTagsWithCount.collect { tags -> analyticsManager.setUserProperty( com.yogeshpaliyal.deepr.analytics.AnalyticsUserProperties.TOTAL_TAGS, - tags.size.toString(), + tags?.tags?.size.toString(), ) } } @@ -178,7 +146,7 @@ class AccountViewModel( } // Set tag filter - toggle tag in the list - fun setTagFilter(tag: Tags?) { + fun setTagFilter(tag: DeeprTag?) { if (tag == null) { _selectedTagFilter.update { emptyList() } analyticsManager.logEvent( @@ -224,7 +192,7 @@ class AccountViewModel( tagId: Long, ) { viewModelScope.launch(Dispatchers.IO) { - deeprQueries.removeTagFromLink(linkId, tagId) + dataProvider.removeTagFromLink(linkId, tagId) } } @@ -234,7 +202,7 @@ class AccountViewModel( tagId: Long, ) { withContext(Dispatchers.IO) { - deeprQueries.addTagToLink(linkId, tagId) + dataProvider.addTagToLink(linkId, tagId) } } @@ -244,22 +212,13 @@ class AccountViewModel( tagName: String, ) { withContext(Dispatchers.IO) { - // Create the tag if it doesn't exist - deeprQueries.insertTag(tagName) - - // Get the tag ID - val tag = deeprQueries.getTagByName(tagName).executeAsOneOrNull() - - if (tag != null) { - // Add the tag to the link - deeprQueries.addTagToLink(linkId, tag.id) - } + dataProvider.addTagToLinkByName(linkId, tagName) } } fun setSelectedTagByName(tagName: String) { viewModelScope.launch(Dispatchers.IO) { - val tag = deeprQueries.getTagByName(tagName).executeAsOneOrNull() + val tag = dataProvider.getTagByName(tagName) if (tag != null) { setTagFilter(tag) } @@ -280,44 +239,14 @@ class AccountViewModel( } @OptIn(ExperimentalCoroutinesApi::class) - val accounts: StateFlow?> = - combine( + val accounts: StateFlow = + dataProvider.getLinks( + viewModelScope, searchQuery, sortOrder, selectedTagFilter, favouriteFilter, - ) { query, sorting, tags, favourite -> - listOf(query, sorting, tags, favourite) - }.flatMapLatest { combined -> - val query = combined[0] as String - val sorting = (combined[1] as String).split("_") - val tags = combined[2] as List - val favourite = combined[3] as Int - val sortField = sorting.getOrNull(0) ?: "createdAt" - val sortType = sorting.getOrNull(1) ?: "DESC" - - // Prepare tag filter parameters - val tagIdsString = - if (tags.isEmpty()) "" else tags.joinToString(",") { it.id.toString() } - val tagCount = tags.size.toLong() - - deeprQueries - .getLinksAndTags( - query, - query, - query, - favourite.toLong(), - favourite.toLong(), - tagIdsString, - tagIdsString, - tagCount, - sortType, - sortField, - sortType, - sortField, - ).asFlow() - .mapToList(viewModelScope.coroutineContext) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) + ) fun search(query: String) { searchQuery.update { query } @@ -343,13 +272,12 @@ class AccountViewModel( link: String, name: String, executed: Boolean, - tagsList: List, + tagsList: List, notes: String = "", thumbnail: String = "", ) { viewModelScope.launch(Dispatchers.IO) { - deeprQueries.insertDeepr(link = link, name, if (executed) 1 else 0, notes, thumbnail) - deeprQueries.lastInsertRowId().executeAsOneOrNull()?.let { + dataProvider.insertLink(link = link, name = name, tagsList = tagsList , notes = notes, thumbnail = thumbnail, openedCount = if (executed) 1 else 0,)?.let { modifyTagsForLink(it, tagsList) analyticsManager.logEvent( com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.ADD_LINK, @@ -365,38 +293,14 @@ class AccountViewModel( suspend fun modifyTagsForLink( linkId: Long, - tagsList: List, + tagsList: List, ) { - withContext(Dispatchers.IO) { - // Then add selected tags - tagsList.forEach { tag -> - if (tag.id > 0) { - // Existing tag - addTagToLink(linkId, tag.id) - } else { - // New tag - addTagToLinkByName(linkId, tag.name) - } - } - } + dataProvider.modifyTagsForLink(linkId, tagsList) } fun deleteAccount(id: Long) { viewModelScope.launch(Dispatchers.IO) { - val tagsToDelete = mutableListOf() - - deeprQueries.getTagsForLink(id).executeAsList().forEach { tag -> - val linkCount = deeprQueries.hasTagLinks(tag.id).executeAsOne() - if (linkCount == 1L) { - tagsToDelete.add(tag.id) - } - } - - deeprQueries.deleteDeeprById(id) - deeprQueries.deleteLinkRelations(id) - tagsToDelete.forEach { tagId -> - deeprQueries.deleteTag(tagId) - } + dataProvider.deleteAccount(id) analyticsManager.logEvent( com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.DELETE_LINK, @@ -407,27 +311,25 @@ class AccountViewModel( fun deleteTag(id: Long) { viewModelScope.launch(Dispatchers.IO) { - deeprQueries.deleteTag(id) - deeprQueries.deleteTagRelations(id) + dataProvider.deleteTag(id) } } - suspend fun updateTag(tag: Tags) { + suspend fun updateTag(tag: DeeprTag) { withContext(Dispatchers.IO) { - deeprQueries.updateTag(tag.name, tag.id) + dataProvider.updateTag(tag) } } fun incrementOpenedCount(id: Long) { viewModelScope.launch(Dispatchers.IO) { - deeprQueries.incrementOpenedCount(id) - deeprQueries.insertDeeprOpenLog(id) + dataProvider.incrementOpenedCount(id) } } fun resetOpenedCount(id: Long) { viewModelScope.launch(Dispatchers.IO) { - deeprQueries.resetOpenedCount(id) + dataProvider.resetOpenedCount(id) analyticsManager.logEvent( com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.RESET_COUNTER, mapOf(com.yogeshpaliyal.deepr.analytics.AnalyticsParams.LINK_ID to id), @@ -437,7 +339,7 @@ class AccountViewModel( fun toggleFavourite(id: Long) { viewModelScope.launch(Dispatchers.IO) { - deeprQueries.toggleFavourite(id) + dataProvider.toggleFavourite(id) analyticsManager.logEvent( com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.TOGGLE_FAVOURITE, mapOf(com.yogeshpaliyal.deepr.analytics.AnalyticsParams.LINK_ID to id), @@ -449,13 +351,12 @@ class AccountViewModel( id: Long, newLink: String, newName: String, - tagsList: List, + tagsList: List, notes: String = "", thumbnail: String = "", ) { viewModelScope.launch(Dispatchers.IO) { - deeprQueries.updateDeeplink(newLink, newName, notes, thumbnail, id) - modifyTagsForLink(id, tagsList) + dataProvider.updateDeeplink(id, newLink, newName, tagsList, notes, thumbnail) syncToMarkdown() analyticsManager.logEvent( com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.EDIT_LINK, @@ -690,6 +591,16 @@ class AccountViewModel( } } + fun isLinkExist(link: String): Boolean { + return dataProvider.isLinkExist(link) + } + + fun insertTag(name: String) { + viewModelScope.launch(Dispatchers.IO) { + dataProvider.insertTag(name) + } + } + fun requestReview(activity: android.app.Activity) { reviewManager.requestReview(activity) } diff --git a/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq b/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq index a1fa4789..f28f404a 100644 --- a/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq +++ b/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq @@ -33,7 +33,7 @@ lastInsertRowId: SELECT last_insert_rowid(); insertDeepr: -INSERT INTO Deepr (link, name, openedCount, notes, thumbnail) VALUES (?, ?, ?, ?, ?); +INSERT INTO Deepr (link, name, openedCount, notes, thumbnail, isFavourite) VALUES (?, ?, ?, ?, ?, ?); importDeepr: INSERT INTO Deepr (link, name, openedCount, notes, thumbnail, isFavourite, createdAt) VALUES (?, ?, ?, ?, ?, ?, ?); @@ -76,7 +76,7 @@ WHERE FROM LinkTags lt WHERE lt.linkId = Deepr.id AND (',' || ? || ',' LIKE '%,' || CAST(lt.tagId AS TEXT) || ',%') - ) = ? + ) > 0 ) GROUP BY Deepr.id @@ -150,8 +150,8 @@ UPDATE Deepr SET link = ? , name = ?, notes = ?, thumbnail = ? WHERE id = ?; countDeepr: SELECT COUNT(*) FROM Deepr; -getDeeprByLink: -SELECT * FROM Deepr WHERE link = ?; +isLinkExists: +SELECT count(*) FROM Deepr WHERE link = ?; -- Tag operations @@ -159,12 +159,18 @@ insertTag: INSERT OR IGNORE INTO Tags (name) VALUES (?); getTagByName: -SELECT * FROM Tags WHERE name = ?; +SELECT + Tags.id, + Tags.name, + COUNT(LinkTags.linkId) AS linkCount +FROM Tags +LEFT JOIN LinkTags ON Tags.id = LinkTags.tagId +WHERE Tags.name = ? +GROUP BY Tags.id, Tags.name +ORDER BY linkCount DESC, Tags.name; -getAllTags: -SELECT * FROM Tags ORDER BY name; -getAllTagsWithCount: +DeeprTag: SELECT Tags.id, Tags.name, diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index f9d80682..4cbfd552 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,6 +10,7 @@ junit = "4.13.2" junitVersion = "1.3.0" espressoCore = "3.7.0" ktlint = "0.4.27" +ktorServerWebsockets = "3.2.3" lifecycleRuntimeKtx = "2.9.3" activityCompose = "1.11.0" composeBom = "2025.09.00" @@ -62,6 +63,8 @@ koin-core = { module = "io.insert-koin:koin-core" } koin-android = { module = "io.insert-koin:koin-android" } koin-compose = { module = "io.insert-koin:koin-androidx-compose" } ktlint = { module = "io.nlopez.compose.rules:ktlint", version.ref = "ktlint" } +ktor-server-websockets = { module = "io.ktor:ktor-server-websockets", version.ref = "ktorServerWebsockets" } +ktor-client-websockets = { module = "io.ktor:ktor-client-websockets", version.ref = "ktorServerWebsockets" } tabler-icons = { module = "br.com.devsrsouza.compose.icons:tabler-icons", version.ref = "tablerIcons" } androidx-datastore-preferences = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "dataStore" } opencsv = { group = "com.opencsv", name = "opencsv", version.ref = "opencsv" }