diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 10ee511..2394d76 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -120,7 +120,9 @@ dependencies { androidTestImplementation(libs.androidx.ui.test.junit4) debugImplementation(libs.androidx.ui.tooling) debugImplementation(libs.androidx.ui.test.manifest) - implementation(libs.android.driver) + implementation(libs.sql.delight) + implementation(libs.sql.delight.paging3) + implementation(libs.paging.compose) implementation(libs.coroutines.extensions) implementation(project.dependencies.platform(libs.koin.bom)) implementation(libs.koin.core) diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/data/AccountPagingSource.kt b/app/src/main/java/com/yogeshpaliyal/deepr/data/AccountPagingSource.kt new file mode 100644 index 0000000..99837da --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/data/AccountPagingSource.kt @@ -0,0 +1,74 @@ +package com.yogeshpaliyal.deepr.data + +import androidx.paging.PagingSource +import androidx.paging.PagingState +import com.yogeshpaliyal.deepr.DeeprQueries +import com.yogeshpaliyal.deepr.GetLinksAndTags +import com.yogeshpaliyal.deepr.Tags +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.withContext + +class AccountPagingSource( + private val deeprQueries: DeeprQueries, + private val searchQuery: String, + private val favouriteFilter: Int, + private val selectedTags: List, + private val sortField: String, + private val sortType: String, +) : PagingSource() { + override fun getRefreshKey(state: PagingState): Int? = + state.anchorPosition?.let { anchorPosition -> + state.closestPageToPosition(anchorPosition)?.prevKey?.plus(state.config.pageSize) + ?: state.closestPageToPosition(anchorPosition)?.nextKey?.minus(state.config.pageSize) + } + + override suspend fun load(params: LoadParams): LoadResult = + try { + val offset = params.key ?: 0 + val limit = params.loadSize + + val tagIdsString = + if (selectedTags.isEmpty()) "" else selectedTags.joinToString(",") { it.id.toString() } + val tagCount = selectedTags.size.toLong() + + // Fetch data using the paged query + val dataFromPagedQuery = + withContext(Dispatchers.IO) { + deeprQueries + .getLinksAndTagsPaged( + searchQuery = searchQuery, + favouriteFilter = favouriteFilter.toLong(), + tagIdsString = tagIdsString, + tagCount = tagCount, + sortField = sortField, + sortType = sortType, + limit = limit.toLong(), + offset = offset.toLong(), + ).executeAsList() + } + + val mappedData = + dataFromPagedQuery.map { pagedItem -> + GetLinksAndTags( + id = pagedItem.id, + link = pagedItem.link, + name = pagedItem.name, + createdAt = pagedItem.createdAt, + openedCount = pagedItem.openedCount, + isFavourite = pagedItem.isFavourite, + notes = pagedItem.notes, + lastOpenedAt = pagedItem.lastOpenedAt, + tagsNames = pagedItem.tagsNames, + tagsIds = pagedItem.tagsIds, + ) + } + + LoadResult.Page( + data = mappedData, + prevKey = if (offset == 0) null else offset - limit, + nextKey = if (mappedData.isEmpty()) null else offset + limit, + ) + } catch (e: Exception) { + LoadResult.Error(e) + } +} 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 e265bd3..34cc0aa 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 @@ -69,6 +69,10 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import androidx.paging.LoadState +import androidx.paging.compose.LazyPagingItems +import androidx.paging.compose.collectAsLazyPagingItems +import androidx.paging.compose.itemKey import com.journeyapps.barcodescanner.ScanOptions import com.yogeshpaliyal.deepr.DeeprQueries import com.yogeshpaliyal.deepr.GetLinksAndTags @@ -428,89 +432,97 @@ fun Content( viewModel: AccountViewModel = koinViewModel(), editDeepr: (GetLinksAndTags) -> Unit = {}, ) { - val accounts by viewModel.accounts.collectAsStateWithLifecycle() + val accounts = viewModel.accounts.collectAsLazyPagingItems() - if (accounts == null) { - Column( - modifier = modifier.fillMaxSize(), - horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.Center, - ) { ContainedLoadingIndicator() } - return - } - - Column(modifier.fillMaxSize()) { - val context = LocalContext.current - var showShortcutDialog by remember { mutableStateOf(null) } - var showQrCodeDialog by remember { mutableStateOf(null) } - var showDeleteConfirmDialog by remember { mutableStateOf(null) } - - showShortcutDialog?.let { deepr -> - CreateShortcutDialog( - deepr = deepr, - onDismiss = { showShortcutDialog = null }, - ) - } - - showQrCodeDialog?.let { - QrCodeDialog(it) { - showQrCodeDialog = null - } + when (accounts.loadState.refresh) { + is LoadState.Loading -> { + Column( + modifier = modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { ContainedLoadingIndicator() } } - showDeleteConfirmDialog?.let { deepr -> - DeleteConfirmationDialog( - deepr = deepr, - onDismiss = { showDeleteConfirmDialog = null }, - onConfirm = { - viewModel.deleteAccount(it.id) - Toast.makeText(context, "Deleted", Toast.LENGTH_SHORT).show() - }, - ) + is LoadState.Error -> { } - DeeprList( - modifier = - Modifier - .weight(1f) - .hazeSource(state = hazeState) - .padding(8.dp), - contentPaddingValues = contentPaddingValues, - accounts = accounts!!, - selectedTag = selectedTag, - onItemClick = { - when (it) { - is MenuItem.Click -> { - viewModel.incrementOpenedCount(it.item.id) - openDeeplink(context, it.item.link) - } + is LoadState.NotLoading -> { + Column(modifier.fillMaxSize()) { + val context = LocalContext.current + var showShortcutDialog by remember { mutableStateOf(null) } + var showQrCodeDialog by remember { mutableStateOf(null) } + var showDeleteConfirmDialog by remember { mutableStateOf(null) } - is MenuItem.Delete -> showDeleteConfirmDialog = it.item - is MenuItem.Edit -> editDeepr(it.item) - is MenuItem.FavouriteClick -> viewModel.toggleFavourite(it.item.id) - is MenuItem.ResetCounter -> { - viewModel.resetOpenedCount(it.item.id) - Toast.makeText(context, "Opened count reset", Toast.LENGTH_SHORT).show() - } + showShortcutDialog?.let { deepr -> + CreateShortcutDialog( + deepr = deepr, + onDismiss = { showShortcutDialog = null }, + ) + } - is MenuItem.Shortcut -> { - showShortcutDialog = it.item + showQrCodeDialog?.let { + QrCodeDialog(it) { + showQrCodeDialog = null } + } - is MenuItem.ShowQrCode -> showQrCodeDialog = it.item + showDeleteConfirmDialog?.let { deepr -> + DeleteConfirmationDialog( + deepr = deepr, + onDismiss = { showDeleteConfirmDialog = null }, + onConfirm = { + viewModel.deleteAccount(it.id) + Toast.makeText(context, "Deleted", Toast.LENGTH_SHORT).show() + }, + ) } - }, - onTagClick = { - // Toggle the tag in the filter by tag name - viewModel.setSelectedTagByName(it) - }, - ) + + DeeprList( + modifier = + Modifier + .weight(1f) + .hazeSource(state = hazeState) + .padding(8.dp), + contentPaddingValues = contentPaddingValues, + accounts = accounts, + selectedTag = selectedTag, + onItemClick = { + when (it) { + is MenuItem.Click -> { + viewModel.incrementOpenedCount(it.item.id) + openDeeplink(context, it.item.link) + } + + is MenuItem.Delete -> showDeleteConfirmDialog = it.item + is MenuItem.Edit -> editDeepr(it.item) + is MenuItem.FavouriteClick -> viewModel.toggleFavourite(it.item.id) + is MenuItem.ResetCounter -> { + viewModel.resetOpenedCount(it.item.id) + Toast + .makeText(context, "Opened count reset", Toast.LENGTH_SHORT) + .show() + } + + is MenuItem.Shortcut -> { + showShortcutDialog = it.item + } + + is MenuItem.ShowQrCode -> showQrCodeDialog = it.item + } + }, + onTagClick = { + // Toggle the tag in the filter by tag name + viewModel.setSelectedTagByName(it) + }, + ) + } + } } } @Composable fun DeeprList( - accounts: List, + accounts: LazyPagingItems, selectedTag: List, contentPaddingValues: PaddingValues, onItemClick: (MenuItem) -> Unit, @@ -518,7 +530,7 @@ fun DeeprList( modifier: Modifier = Modifier, ) { AnimatedVisibility( - visible = accounts.isEmpty(), + visible = (accounts.itemCount == 0), enter = scaleIn() + expandVertically(expandFrom = Alignment.CenterVertically), exit = scaleOut() + shrinkVertically(shrinkTowards = Alignment.CenterVertically), ) { @@ -561,105 +573,112 @@ fun DeeprList( Spacer(modifier = Modifier.weight(1f)) // Push content up } } - AnimatedVisibility( - visible = accounts.isNotEmpty(), - enter = scaleIn() + expandVertically(expandFrom = Alignment.CenterVertically), - exit = scaleOut() + shrinkVertically(shrinkTowards = Alignment.CenterVertically), - ) { - LazyColumn( - modifier = modifier, - contentPadding = contentPaddingValues, - verticalArrangement = Arrangement.spacedBy(4.dp), + accounts.let { pagingItems -> + AnimatedVisibility( + visible = (accounts.itemCount != 0), + enter = scaleIn() + expandVertically(expandFrom = Alignment.CenterVertically), + exit = scaleOut() + shrinkVertically(shrinkTowards = Alignment.CenterVertically), ) { - items( - count = accounts.size, - key = { index -> accounts[index].id }, - ) { index -> - val account = accounts[index] - val dismissState = - rememberSwipeToDismissBoxState( - confirmValueChange = { value -> - when (value) { - SwipeToDismissBoxValue.EndToStart -> { - onItemClick(MenuItem.Delete(account)) - false - } + LazyColumn( + modifier = modifier, + contentPadding = contentPaddingValues, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + items( + count = accounts.itemCount, + key = + pagingItems.itemKey { item -> + item.id + }, + ) { index -> + val account = pagingItems[index] + account?.let { + val dismissState = + rememberSwipeToDismissBoxState( + confirmValueChange = { value -> + when (value) { + SwipeToDismissBoxValue.EndToStart -> { + onItemClick(MenuItem.Delete(account)) + false + } - SwipeToDismissBoxValue.StartToEnd -> { - onItemClick(MenuItem.Edit(account)) - false - } + SwipeToDismissBoxValue.StartToEnd -> { + onItemClick(MenuItem.Edit(account)) + false + } - else -> { - false - } - } - }, - ) + else -> { + false + } + } + }, + ) - SwipeToDismissBox( - modifier = - Modifier - .fillMaxSize() - .clip(RoundedCornerShape(8.dp)), - state = dismissState, - backgroundContent = { - when (dismissState.dismissDirection) { - SwipeToDismissBoxValue.StartToEnd -> { - Box( - modifier = - Modifier - .background( - Color.Gray.copy(alpha = 0.5f), - ).fillMaxSize() - .clip( - RoundedCornerShape(8.dp), - ), - contentAlignment = Alignment.CenterStart, - ) { - Icon( - imageVector = TablerIcons.Edit, - contentDescription = stringResource(R.string.edit), - tint = Color.White, - modifier = Modifier.padding(16.dp), - ) - } - } + SwipeToDismissBox( + modifier = + Modifier + .fillMaxSize() + .clip(RoundedCornerShape(8.dp)), + state = dismissState, + backgroundContent = { + when (dismissState.dismissDirection) { + SwipeToDismissBoxValue.StartToEnd -> { + Box( + modifier = + Modifier + .background( + Color.Gray.copy(alpha = 0.5f), + ).fillMaxSize() + .clip( + RoundedCornerShape(8.dp), + ), + contentAlignment = Alignment.CenterStart, + ) { + Icon( + imageVector = TablerIcons.Edit, + contentDescription = stringResource(R.string.edit), + tint = Color.White, + modifier = Modifier.padding(16.dp), + ) + } + } - SwipeToDismissBoxValue.EndToStart -> { - Box( - modifier = - Modifier - .background( - Color.Red.copy(alpha = 0.5f), - ).fillMaxSize() - .clip( - RoundedCornerShape(8.dp), - ), - contentAlignment = Alignment.CenterEnd, - ) { - Icon( - imageVector = TablerIcons.Trash, - contentDescription = stringResource(R.string.delete), - tint = Color.White, - modifier = Modifier.padding(16.dp), - ) - } - } + SwipeToDismissBoxValue.EndToStart -> { + Box( + modifier = + Modifier + .background( + Color.Red.copy(alpha = 0.5f), + ).fillMaxSize() + .clip( + RoundedCornerShape(8.dp), + ), + contentAlignment = Alignment.CenterEnd, + ) { + Icon( + imageVector = TablerIcons.Trash, + contentDescription = stringResource(R.string.delete), + tint = Color.White, + modifier = Modifier.padding(16.dp), + ) + } + } - else -> { - Color.White - } + else -> { + Color.White + } + } + }, + ) { + DeeprItem( + modifier = Modifier.animateItem(), + account = account, + selectedTag = selectedTag, + onItemClick = onItemClick, + onTagClick = onTagClick, + ) } - }, - ) { - DeeprItem( - modifier = Modifier.animateItem(), - account = account, - selectedTag = selectedTag, - onItemClick = onItemClick, - onTagClick = onTagClick, - ) + } } } } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/util/Utils.kt b/app/src/main/java/com/yogeshpaliyal/deepr/util/Utils.kt index d3c77ed..5b0de33 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/util/Utils.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/util/Utils.kt @@ -8,6 +8,8 @@ import android.widget.Toast import androidx.core.graphics.createBitmap import androidx.core.graphics.drawable.IconCompat import androidx.core.net.toUri +import com.yogeshpaliyal.deepr.GetLinkById +import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.R fun openDeeplink( @@ -94,3 +96,17 @@ fun isValidDeeplink(link: String): Boolean { false } } + +fun GetLinkById.toGetLinksAndTags() = + GetLinksAndTags( + id = id, + link = link, + name = name, + createdAt = createdAt, + openedCount = openedCount, + isFavourite = isFavourite, + notes = notes, + lastOpenedAt = lastOpenedAt, + tagsNames = tagsNames, + tagsIds = tagsIds, + ) 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 a6c86e2..e0b166b 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt @@ -4,6 +4,12 @@ import android.net.Uri import androidx.annotation.StringDef import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.filter +import androidx.paging.map import app.cash.sqldelight.coroutines.asFlow import app.cash.sqldelight.coroutines.mapToList import app.cash.sqldelight.coroutines.mapToOneOrNull @@ -14,11 +20,13 @@ import com.yogeshpaliyal.deepr.Tags import com.yogeshpaliyal.deepr.backup.AutoBackupWorker import com.yogeshpaliyal.deepr.backup.ExportRepository import com.yogeshpaliyal.deepr.backup.ImportRepository +import com.yogeshpaliyal.deepr.data.AccountPagingSource import com.yogeshpaliyal.deepr.data.LinkInfo import com.yogeshpaliyal.deepr.data.NetworkRepository import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore import com.yogeshpaliyal.deepr.sync.SyncRepository import com.yogeshpaliyal.deepr.util.RequestResult +import com.yogeshpaliyal.deepr.util.toGetLinksAndTags import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.channels.Channel @@ -134,6 +142,9 @@ class AccountViewModel( private val _favouriteFilter = MutableStateFlow(-1) val favouriteFilter: StateFlow = _favouriteFilter + private val itemUpdates = MutableStateFlow>(emptyMap()) + private val deletedItems = MutableStateFlow>(emptySet()) + // Set tag filter - toggle tag in the list fun setTagFilter(tag: Tags?) { if (tag == null) { @@ -223,42 +234,58 @@ class AccountViewModel( } @OptIn(ExperimentalCoroutinesApi::class) - val accounts: StateFlow?> = + val accounts: Flow> = 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 + mapOf( + "query" to query, + "sorting" to sorting, + "tags" to tags, + "favourite" to favourite, + ) + }.flatMapLatest { filters -> + val query = filters["query"] as String + val sorting = (filters["sorting"] as String).split("_") + val tags = filters["tags"] as List + val favourite = filters["favourite"] 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, - favourite.toLong(), - favourite.toLong(), - tagIdsString, - tagIdsString, - tagCount, - sortType, - sortField, - sortType, - sortField, - ).asFlow() - .mapToList(viewModelScope.coroutineContext) - }.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), null) + // Create a new Pager whenever the filters change + Pager( + config = + PagingConfig( + pageSize = 20, + enablePlaceholders = false, + prefetchDistance = 1, + ), + pagingSourceFactory = { + AccountPagingSource( + deeprQueries = deeprQueries, + searchQuery = query, + favouriteFilter = favourite, + selectedTags = tags, + sortField = sortField, + sortType = sortType, + ) + }, + ).flow + }.cachedIn(viewModelScope) + .combine(itemUpdates) { pagingData, updates -> + pagingData.map { item -> + updates[item.id] ?: item + } + }.combine(deletedItems) { pagingData, deletedIds -> + // Filter out deleted items + pagingData.filter { item -> + item.id !in deletedIds + } + } fun search(query: String) { searchQuery.update { query } @@ -306,6 +333,9 @@ class AccountViewModel( fun deleteAccount(id: Long) { viewModelScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { + deletedItems.update { it + id } + } val tagsToDelete = mutableListOf() deeprQueries.getTagsForLink(id).executeAsList().forEach { tag -> @@ -340,18 +370,33 @@ class AccountViewModel( viewModelScope.launch(Dispatchers.IO) { deeprQueries.incrementOpenedCount(id) deeprQueries.insertDeeprOpenLog(id) + deeprQueries.getLinkById(id).executeAsOneOrNull()?.let { item -> + itemUpdates.update { currentMap -> + currentMap + (id to item.toGetLinksAndTags()) + } + } } } fun resetOpenedCount(id: Long) { viewModelScope.launch(Dispatchers.IO) { deeprQueries.resetOpenedCount(id) + deeprQueries.getLinkById(id).executeAsOneOrNull()?.let { item -> + itemUpdates.update { currentMap -> + currentMap + (id to item.toGetLinksAndTags()) + } + } } } fun toggleFavourite(id: Long) { viewModelScope.launch(Dispatchers.IO) { deeprQueries.toggleFavourite(id) + deeprQueries.getLinkById(id).executeAsOneOrNull()?.let { item -> + itemUpdates.update { currentMap -> + currentMap + (id to item.toGetLinksAndTags()) + } + } } } @@ -364,6 +409,11 @@ class AccountViewModel( ) { viewModelScope.launch(Dispatchers.IO) { deeprQueries.updateDeeplink(newLink, newName, notes, id) + deeprQueries.getLinkById(id).executeAsOneOrNull()?.let { item -> + itemUpdates.update { currentMap -> + currentMap + (id to item.toGetLinksAndTags()) + } + } modifyTagsForLink(id, tagsList) syncToMarkdown() } diff --git a/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq b/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq index 3eda533..0a7069a 100644 --- a/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq +++ b/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq @@ -146,7 +146,6 @@ SELECT COUNT(*) FROM Deepr; getDeeprByLink: SELECT * FROM Deepr WHERE link = ?; - -- Tag operations insertTag: INSERT OR IGNORE INTO Tags (name) VALUES (?); @@ -173,7 +172,6 @@ SELECT count(*) FROM Deepr; countOfFavouriteLinks: SELECT count(*) FROM Deepr WHERE isFavourite = 1; - -- Link-Tag relations addTagToLink: INSERT OR IGNORE INTO LinkTags (linkId, tagId) VALUES (?, ?); @@ -184,7 +182,6 @@ DELETE FROM LinkTags WHERE linkId = ? AND tagId = ?; deleteLinkRelations: DELETE FROM LinkTags WHERE linkId = ?; - deleteTag: DELETE FROM Tags WHERE id = ?; @@ -204,12 +201,97 @@ SELECT COUNT(*) FROM LinkTags WHERE tagId = ?; insertDeeprOpenLog: INSERT INTO DeeprOpenLog (deeplinkId) VALUES (?); -getLastOpenedTime: -SELECT openedAt FROM DeeprOpenLog WHERE deeplinkId = ? ORDER BY openedAt DESC LIMIT 1; - toggleFavourite: UPDATE Deepr SET isFavourite = CASE WHEN isFavourite = 0 THEN 1 ELSE 0 END WHERE id = ?; -setFavourite: -UPDATE Deepr SET isFavourite = ? WHERE id = ?; +getLinksAndTagsPaged: +SELECT + Deepr.id AS id, + Deepr.link, + Deepr.name, + Deepr.createdAt, + Deepr.openedCount, + Deepr.isFavourite, + Deepr.notes, + DOL_Max.lastOpenedAt, + GROUP_CONCAT(Tags.name, ', ') AS tagsNames, + GROUP_CONCAT(Tags.id, ', ') AS tagsIds +FROM + Deepr + LEFT JOIN ( + -- Subquery to find the single latest log entry per deeplink + SELECT + deeplinkId, + MAX(openedAt) AS lastOpenedAt + FROM + DeeprOpenLog + GROUP BY + deeplinkId + ) AS DOL_Max ON Deepr.id = DOL_Max.deeplinkId + LEFT JOIN LinkTags ON Deepr.id = LinkTags.linkId + LEFT JOIN Tags ON LinkTags.tagId = Tags.id +WHERE + (Deepr.link LIKE '%' || :searchQuery || '%' OR Deepr.name LIKE '%' || :searchQuery || '%') + AND ( + :favouriteFilter = -1 OR Deepr.isFavourite = :favouriteFilter + ) + AND ( + :tagIdsString = '' OR ( + SELECT COUNT(DISTINCT lt.tagId) + FROM LinkTags lt + WHERE lt.linkId = Deepr.id + AND (',' || :tagIdsString || ',' LIKE '%,' || CAST(lt.tagId AS TEXT) || ',%') + ) = :tagCount + ) +GROUP BY + Deepr.id +ORDER BY + CASE WHEN :sortType = 'ASC' THEN + CASE :sortField + WHEN 'createdAt' THEN Deepr.createdAt + WHEN 'openedCount' THEN Deepr.openedCount + WHEN 'name' THEN Deepr.name + WHEN 'link' THEN Deepr.link + ELSE Deepr.createdAt + END + END ASC, + CASE WHEN :sortType = 'DESC' THEN + CASE :sortField + WHEN 'createdAt' THEN Deepr.createdAt + WHEN 'openedCount' THEN Deepr.openedCount + WHEN 'name' THEN Deepr.name + WHEN 'link' THEN Deepr.link + ELSE Deepr.createdAt + END + END DESC +LIMIT :limit OFFSET :offset; +getLinkById: +SELECT + Deepr.id AS id, + Deepr.link, + Deepr.name, + Deepr.createdAt, + Deepr.openedCount, + Deepr.isFavourite, + Deepr.notes, + DOL_Max.lastOpenedAt, + GROUP_CONCAT(Tags.name, ', ') AS tagsNames, + GROUP_CONCAT(Tags.id, ', ') AS tagsIds +FROM + Deepr + LEFT JOIN ( + SELECT + deeplinkId, + MAX(openedAt) AS lastOpenedAt + FROM + DeeprOpenLog + GROUP BY + deeplinkId + ) AS DOL_Max ON Deepr.id = DOL_Max.deeplinkId + LEFT JOIN LinkTags ON Deepr.id = LinkTags.linkId + LEFT JOIN Tags ON LinkTags.tagId = Tags.id +WHERE + Deepr.id = :id +GROUP BY + Deepr.id; \ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index a5cff18..c14810e 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -13,7 +13,7 @@ ktlint = "0.4.27" lifecycleRuntimeKtx = "2.9.3" activityCompose = "1.11.0" composeBom = "2025.09.00" -sqldelight = "2.1.0" +paging = "3.3.0" koin-bom = "4.1.1" tablerIcons = "1.1.1" nav3Core = "1.0.0-alpha09" @@ -33,7 +33,9 @@ googleServices = "4.4.3" firebaseCrashlytics = "3.0.6" [libraries] -android-driver = { module = "app.cash.sqldelight:android-driver", version.ref = "androidDriver" } +sql-delight = { module = "app.cash.sqldelight:android-driver", version.ref = "androidDriver" } +sql-delight-paging3 = { module = "app.cash.sqldelight:androidx-paging3-extensions", version.ref = "androidDriver" } +paging-compose = { module = "androidx.paging:paging-compose", version.ref = "paging" } androidx-compose-material-icons-extended = { module = "androidx.compose.material:material-icons-extended" } androidx-core-ktx = { group = "androidx.core", name = "core-ktx", version.ref = "coreKtx" } androidx-documentfile = { module = "androidx.documentfile:documentfile", version.ref = "documentfile" } @@ -91,7 +93,7 @@ android-application = { id = "com.android.application", version.ref = "agp" } kotlin-android = { id = "org.jetbrains.kotlin.android", version.ref = "kotlin" } kotlin-compose = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } kotlin-serialization = { id = "org.jetbrains.kotlin.plugin.serialization", version.ref = "kotlin" } -sqldelight = { id = "app.cash.sqldelight", version.ref = "sqldelight" } +sqldelight = { id = "app.cash.sqldelight", version.ref = "androidDriver" } kotlinter = { id = "org.jmailen.kotlinter", version.ref = "kotlinter" } googleServices = { id = "com.google.gms.google-services", version.ref = "googleServices" } firebaseCrashlytics = { id = "com.google.firebase.crashlytics", version.ref = "firebaseCrashlytics" } \ No newline at end of file