From 8b3ab58f5f65cd3b64789c5984edfea7bf8cef9c Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Wed, 22 Oct 2025 20:15:06 +0530 Subject: [PATCH 1/5] Refactor metadata fetching logic to use a dedicated function and trigger on link selection --- .../ui/screens/home/HomeBottomContent.kt | 42 +++++++++++-------- 1 file changed, 25 insertions(+), 17 deletions(-) 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 15d29c3f..63c4469e 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 @@ -88,6 +88,30 @@ fun HomeBottomContent( val initialSelectedTags = remember { mutableStateListOf() } val isCreate = selectedLink.id == 0L + val fetchTitle: () -> Unit = { + isFetchingMetadata = true + viewModel.fetchMetaData(deeprInfo.link) { + isFetchingMetadata = false + if (it != null) { + deeprInfo = deeprInfo.copy(name = it.title ?: "") + isNameError = false + } else { + Toast + .makeText( + context, + fetchMetadataErrorText, + Toast.LENGTH_SHORT, + ).show() + } + } + } + + LaunchedEffect(selectedLink) { + if (isValidDeeplink(selectedLink.link) && selectedLink.name.isEmpty()) { + fetchTitle() + } + } + // Initialize selected tags if in edit mode LaunchedEffect(isCreate) { if (isCreate.not()) { @@ -201,23 +225,7 @@ fun HomeBottomContent( OutlinedButton( modifier = Modifier.fillMaxWidth(), enabled = deeprInfo.link.isNotBlank() && !isFetchingMetadata, - onClick = { - isFetchingMetadata = true - viewModel.fetchMetaData(deeprInfo.link) { - isFetchingMetadata = false - if (it != null) { - deeprInfo = deeprInfo.copy(name = it.title ?: "") - isNameError = false - } else { - Toast - .makeText( - context, - fetchMetadataErrorText, - Toast.LENGTH_SHORT, - ).show() - } - } - }, + onClick = fetchTitle, ) { if (isFetchingMetadata) { CircularProgressIndicator( From 3db31d78da38664946356a6ec484f24afc53b02f Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Wed, 22 Oct 2025 20:41:56 +0530 Subject: [PATCH 2/5] Enhance search functionality by including notes in query conditions and refactor API documentation layout --- .../deepr/server/LocalServerRepositoryImpl.kt | 2 + .../deepr/ui/screens/LocalNetworkServer.kt | 139 +++++++++--------- .../deepr/viewmodel/AccountViewModel.kt | 1 + .../com/yogeshpaliyal/deepr/Deepr.sq | 2 +- 4 files changed, 70 insertions(+), 74 deletions(-) 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 3f0fc867..8fd6740d 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt @@ -99,6 +99,7 @@ class LocalServerRepositoryImpl( val links = deeprQueries .getLinksAndTags( + "", "", "", -1L, @@ -158,6 +159,7 @@ class LocalServerRepositoryImpl( val linkCount = deeprQueries .getLinksAndTags( + "", "", "", -1L, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/LocalNetworkServer.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/LocalNetworkServer.kt index 466490ce..9fe4fd1a 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/LocalNetworkServer.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/LocalNetworkServer.kt @@ -254,82 +254,75 @@ fun LocalNetworkServerScreen( } } - // Server Details Section - AnimatedVisibility( - visible = isRunning && serverUrl != null, + // API Documentation Card + OutlinedCard( + modifier = Modifier.fillMaxWidth(), + colors = + CardDefaults.outlinedCardColors( + containerColor = MaterialTheme.colorScheme.surface, + ), ) { - Column(verticalArrangement = Arrangement.spacedBy(12.dp)) { - // API Documentation Card - OutlinedCard( - modifier = Modifier.fillMaxWidth(), - colors = - CardDefaults.outlinedCardColors( - containerColor = MaterialTheme.colorScheme.surface, - ), + Column( + modifier = Modifier.padding(20.dp), + verticalArrangement = Arrangement.spacedBy(16.dp), + ) { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp), ) { - Column( - modifier = Modifier.padding(20.dp), - verticalArrangement = Arrangement.spacedBy(16.dp), - ) { - Row( - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - Icon( - TablerIcons.InfoCircle, - contentDescription = null, - tint = MaterialTheme.colorScheme.secondary, - modifier = Modifier.size(20.dp), - ) - Text( - text = stringResource(R.string.api_endpoints), - style = MaterialTheme.typography.titleMedium, - fontWeight = FontWeight.SemiBold, - ) - } - Text( - text = stringResource(R.string.api_endpoints_description), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) + Icon( + TablerIcons.InfoCircle, + contentDescription = null, + tint = MaterialTheme.colorScheme.secondary, + modifier = Modifier.size(20.dp), + ) + Text( + text = stringResource(R.string.api_endpoints), + style = MaterialTheme.typography.titleMedium, + fontWeight = FontWeight.SemiBold, + ) + } + Text( + text = stringResource(R.string.api_endpoints_description), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) - Column( - modifier = - Modifier - .fillMaxWidth() - .background( - MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.5f), - RoundedCornerShape(12.dp), - ).padding(16.dp), - verticalArrangement = Arrangement.spacedBy(12.dp), - ) { - ApiEndpointItem( - "GET", - "/api/links", - stringResource(R.string.api_get_links), - ) - ApiEndpointItem( - "POST", - "/api/links", - stringResource(R.string.api_add_link), - ) - ApiEndpointItem( - "GET", - "/api/tags", - stringResource(R.string.api_get_tags), - ) - ApiEndpointItem( - "GET", - "/api/link-info", - stringResource(R.string.api_get_link_info), - ) - ApiEndpointItem( - "GET", - "/api/server-info", - stringResource(R.string.api_get_server_info), - ) - } - } + Column( + modifier = + Modifier + .fillMaxWidth() + .background( + MaterialTheme.colorScheme.surfaceContainer.copy(alpha = 0.5f), + RoundedCornerShape(12.dp), + ).padding(16.dp), + verticalArrangement = Arrangement.spacedBy(12.dp), + ) { + ApiEndpointItem( + "GET", + "/api/links", + stringResource(R.string.api_get_links), + ) + ApiEndpointItem( + "POST", + "/api/links", + stringResource(R.string.api_add_link), + ) + ApiEndpointItem( + "GET", + "/api/tags", + stringResource(R.string.api_get_tags), + ) + ApiEndpointItem( + "GET", + "/api/link-info", + stringResource(R.string.api_get_link_info), + ) + ApiEndpointItem( + "GET", + "/api/server-info", + stringResource(R.string.api_get_server_info), + ) } } } 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 7ef02a75..fb52cd04 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt @@ -254,6 +254,7 @@ class AccountViewModel( deeprQueries .getLinksAndTags( + query, query, query, favourite.toLong(), diff --git a/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq b/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq index 3eda5333..6f8a961c 100644 --- a/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq +++ b/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq @@ -61,7 +61,7 @@ FROM LEFT JOIN LinkTags ON Deepr.id = LinkTags.linkId LEFT JOIN Tags ON LinkTags.tagId = Tags.id WHERE - (Deepr.link LIKE '%' || ? || '%' OR Deepr.name LIKE '%' || ? || '%') + (Deepr.link LIKE '%' || ? || '%' OR Deepr.name LIKE '%' || ? || '%' OR Deepr.notes LIKE '%' || ? || '%') AND ( ? = -1 OR Deepr.isFavourite = ? ) From e452af0864ffc2dcd9cb4dd40ef2fa78aa62dcf6 Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Wed, 22 Oct 2025 21:20:33 +0530 Subject: [PATCH 3/5] Add thumbnail support to Deepr entries and update related functionality --- .../yogeshpaliyal/deepr/backup/CsvWriter.kt | 2 ++ .../deepr/backup/ImportRepositoryImpl.kt | 2 ++ .../deepr/ui/screens/home/DeeprItem.kt | 34 +++++++++++++++++++ .../ui/screens/home/HomeBottomContent.kt | 33 ++++++++++++++---- .../ui/screens/home/SaveCompleteDialog.kt | 2 ++ .../com/yogeshpaliyal/deepr/util/Constants.kt | 1 + .../deepr/viewmodel/AccountViewModel.kt | 6 ++-- .../com/yogeshpaliyal/deepr/Deepr.sq | 9 +++-- app/src/main/sqldelight/migrations/6.sqm | 1 + 9 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 app/src/main/sqldelight/migrations/6.sqm diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/backup/CsvWriter.kt b/app/src/main/java/com/yogeshpaliyal/deepr/backup/CsvWriter.kt index 2949b21e..53f537b6 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/backup/CsvWriter.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/backup/CsvWriter.kt @@ -22,6 +22,7 @@ class CsvWriter { Constants.Header.NAME, Constants.Header.NOTES, Constants.Header.TAGS, + Constants.Header.THUMBNAIL, ), ) // Write Data @@ -34,6 +35,7 @@ class CsvWriter { item.name, item.notes, item.tagsNames ?: "", + item.thumbnail, ), ) } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/backup/ImportRepositoryImpl.kt b/app/src/main/java/com/yogeshpaliyal/deepr/backup/ImportRepositoryImpl.kt index 525ac37b..b324f274 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/backup/ImportRepositoryImpl.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/backup/ImportRepositoryImpl.kt @@ -47,6 +47,7 @@ class ImportRepositoryImpl( val name = row.getOrNull(3)?.toString() ?: "" val notes = row.getOrNull(4)?.toString() ?: "" val tagsString = row.getOrNull(5)?.toString() ?: "" + val thumbnail = row.getOrNull(6)?.toString() ?: "" val existing = deeprQueries.getDeeprByLink(link).executeAsOneOrNull() if (link.isNotBlank() && existing == null) { updatedCount++ @@ -56,6 +57,7 @@ class ImportRepositoryImpl( openedCount = openedCount, name = name, notes = notes, + thumbnail = thumbnail, ) val linkId = deeprQueries.lastInsertRowId().executeAsOne() 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 100a6d7d..3f178579 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 @@ -14,6 +14,7 @@ import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height @@ -49,11 +50,14 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip import androidx.compose.ui.graphics.Color +import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import coil3.compose.AsyncImage import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.R import com.yogeshpaliyal.deepr.Tags @@ -101,6 +105,22 @@ sealed class MenuItem( ) : MenuItem(item) } +@Composable +@Preview +private fun DeeprItemPreview() { + DeeprItem( + account = + createDeeprObject( + name = "Yogesh Paliyal", + link = "https://yogeshpaliyal.com", + thumbnail = "https://yogeshpaliyal.com/og.png", + ), + {}, + {}, + listOf(), + ) +} + @OptIn(ExperimentalFoundationApi::class) @Composable fun DeeprItem( @@ -243,6 +263,20 @@ fun DeeprItem( }, ), ) { + if (account.thumbnail.isNotEmpty()) { + AsyncImage( + model = account.thumbnail, + contentDescription = account.name, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1.91f) + .background(MaterialTheme.colorScheme.surfaceVariant), + placeholder = null, + error = null, + contentScale = ContentScale.Crop, + ) + } Column( modifier = Modifier 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 63c4469e..845e75d2 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 @@ -3,11 +3,13 @@ package com.yogeshpaliyal.deepr.ui.screens.home import android.widget.Toast +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding @@ -42,10 +44,12 @@ 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.res.stringResource 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.R @@ -88,12 +92,12 @@ fun HomeBottomContent( val initialSelectedTags = remember { mutableStateListOf() } val isCreate = selectedLink.id == 0L - val fetchTitle: () -> Unit = { + val fetchMetadata: () -> Unit = { isFetchingMetadata = true viewModel.fetchMetaData(deeprInfo.link) { isFetchingMetadata = false if (it != null) { - deeprInfo = deeprInfo.copy(name = it.title ?: "") + deeprInfo = deeprInfo.copy(name = it.title ?: "", thumbnail = it.image ?: "") isNameError = false } else { Toast @@ -108,7 +112,7 @@ fun HomeBottomContent( LaunchedEffect(selectedLink) { if (isValidDeeplink(selectedLink.link) && selectedLink.name.isEmpty()) { - fetchTitle() + fetchMetadata() } } @@ -153,10 +157,10 @@ fun HomeBottomContent( if (deeprInfo.id == 0L) { // New Account - viewModel.insertAccount(normalizedLink, deeprInfo.name, executeAfterSave, selectedTags, deeprInfo.notes) + viewModel.insertAccount(normalizedLink, deeprInfo.name, executeAfterSave, selectedTags, deeprInfo.notes, deeprInfo.thumbnail) } else { // Edit - viewModel.updateDeeplink(deeprInfo.id, normalizedLink, deeprInfo.name, selectedTags, deeprInfo.notes) + viewModel.updateDeeplink(deeprInfo.id, normalizedLink, deeprInfo.name, selectedTags, deeprInfo.notes, deeprInfo.thumbnail) } onSaveDialogInfoChange( SaveDialogInfo( @@ -225,7 +229,7 @@ fun HomeBottomContent( OutlinedButton( modifier = Modifier.fillMaxWidth(), enabled = deeprInfo.link.isNotBlank() && !isFetchingMetadata, - onClick = fetchTitle, + onClick = fetchMetadata, ) { if (isFetchingMetadata) { CircularProgressIndicator( @@ -239,6 +243,23 @@ fun HomeBottomContent( Spacer(modifier = Modifier.height(8.dp)) + if (deeprInfo.thumbnail.isNotEmpty()) { + AsyncImage( + model = deeprInfo.thumbnail, + contentDescription = deeprInfo.name, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1.91f) + .background(MaterialTheme.colorScheme.surfaceVariant), + placeholder = null, + error = null, + contentScale = ContentScale.Crop, + ) + } + + Spacer(modifier = Modifier.height(8.dp)) + TextField( value = deeprInfo.name, onValueChange = { 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 1c6af0f9..cf6cd42e 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 @@ -12,6 +12,7 @@ fun createDeeprObject( link: String = "", openedCount: Long = 0, notes: String = "", + thumbnail: String = "", ): GetLinksAndTags = GetLinksAndTags( id = 0, @@ -24,4 +25,5 @@ fun createDeeprObject( lastOpenedAt = "", isFavourite = 0, notes = notes, + thumbnail = thumbnail, ) diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/util/Constants.kt b/app/src/main/java/com/yogeshpaliyal/deepr/util/Constants.kt index 875b09f1..2e9f5168 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/util/Constants.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/util/Constants.kt @@ -8,5 +8,6 @@ object Constants { const val CREATED_AT = "CreatedAt" const val NOTES = "Notes" const val TAGS = "Tags" + const val THUMBNAIL = "Thumbnail" } } 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 fb52cd04..4c543a5d 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt @@ -286,9 +286,10 @@ class AccountViewModel( executed: Boolean, tagsList: List, notes: String = "", + thumbnail: String = "", ) { viewModelScope.launch(Dispatchers.IO) { - deeprQueries.insertDeepr(link = link, name, if (executed) 1 else 0, notes) + deeprQueries.insertDeepr(link = link, name, if (executed) 1 else 0, notes, thumbnail) deeprQueries.lastInsertRowId().executeAsOneOrNull()?.let { modifyTagsForLink(it, tagsList) } @@ -371,9 +372,10 @@ class AccountViewModel( newName: String, tagsList: List, notes: String = "", + thumbnail: String = "", ) { viewModelScope.launch(Dispatchers.IO) { - deeprQueries.updateDeeplink(newLink, newName, notes, id) + deeprQueries.updateDeeplink(newLink, newName, notes, thumbnail, id) 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 6f8a961c..d6b03ad7 100644 --- a/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq +++ b/app/src/main/sqldelight/com/yogeshpaliyal/deepr/Deepr.sq @@ -5,7 +5,8 @@ name TEXT NOT NULL DEFAULT '', createdAt TEXT NOT NULL DEFAULT CURRENT_TIMESTAMP, openedCount INTEGER NOT NULL DEFAULT 0, isFavourite INTEGER NOT NULL DEFAULT 0, -notes TEXT NOT NULL DEFAULT '' +notes TEXT NOT NULL DEFAULT '', +thumbnail TEXT NOT NULL DEFAULT '' ); CREATE TABLE Tags ( @@ -32,7 +33,7 @@ lastInsertRowId: SELECT last_insert_rowid(); insertDeepr: -INSERT INTO Deepr (link, name, openedCount, notes) VALUES (?, ?, ?, ?); +INSERT INTO Deepr (link, name, openedCount, notes, thumbnail) VALUES (?, ?, ?, ?, ?); getLinksAndTags: SELECT @@ -43,6 +44,7 @@ SELECT Deepr.openedCount, Deepr.isFavourite, Deepr.notes, + Deepr.thumbnail, DOL_Max.lastOpenedAt, GROUP_CONCAT(Tags.name, ', ') AS tagsNames, GROUP_CONCAT(Tags.id, ', ') AS tagsIds @@ -107,6 +109,7 @@ SELECT Deepr.createdAt, Deepr.openedCount, Deepr.notes, + Deepr.thumbnail, DOL_Max.lastOpenedAt, GROUP_CONCAT(Tags.name, ', ') AS tagsNames FROM @@ -138,7 +141,7 @@ resetOpenedCount: UPDATE Deepr SET openedCount = 0 WHERE id = ?; updateDeeplink: -UPDATE Deepr SET link = ? , name = ?, notes = ? WHERE id = ?; +UPDATE Deepr SET link = ? , name = ?, notes = ?, thumbnail = ? WHERE id = ?; countDeepr: SELECT COUNT(*) FROM Deepr; diff --git a/app/src/main/sqldelight/migrations/6.sqm b/app/src/main/sqldelight/migrations/6.sqm new file mode 100644 index 00000000..e3405ffc --- /dev/null +++ b/app/src/main/sqldelight/migrations/6.sqm @@ -0,0 +1 @@ +ALTER TABLE Deepr ADD COLUMN thumbnail TEXT NOT NULL DEFAULT ''; From cdc4687615ff8e6f3a8eecfd59eaea944ce80e8c Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Wed, 22 Oct 2025 22:11:30 +0530 Subject: [PATCH 4/5] Implement view type selection with list, grid, and compact layouts in home screen --- .../deepr/ui/screens/home/DeeprItem.kt | 132 +------- .../deepr/ui/screens/home/DeeprItemCompact.kt | 142 +++++++++ .../deepr/ui/screens/home/DeeprItemGrid.kt | 164 ++++++++++ .../deepr/ui/screens/home/Home.kt | 283 ++++++++++++++++-- .../deepr/ui/screens/home/ViewType.kt | 7 + .../deepr/ui/screens/home/ViewTypeMenu.kt | 112 +++++++ 6 files changed, 684 insertions(+), 156 deletions(-) create mode 100644 app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemCompact.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemGrid.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewType.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewTypeMenu.kt 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 3f178579..d275549c 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 @@ -24,17 +24,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarBorder -import androidx.compose.material3.AlertDialog import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MenuDefaults -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.SwipeToDismissBox import androidx.compose.material3.SwipeToDismissBoxDefaults import androidx.compose.material3.SwipeToDismissBoxValue @@ -62,10 +57,7 @@ import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.R import com.yogeshpaliyal.deepr.Tags import compose.icons.TablerIcons -import compose.icons.tablericons.DotsVertical import compose.icons.tablericons.Edit -import compose.icons.tablericons.Note -import compose.icons.tablericons.Refresh import compose.icons.tablericons.Trash import kotlinx.coroutines.launch import java.text.DateFormat @@ -129,9 +121,8 @@ fun DeeprItem( onTagClick: (tag: String) -> Unit, selectedTag: List, modifier: Modifier = Modifier, + dropdownMenu: (@Composable () -> Unit)? = null, ) { - var expanded by remember { mutableStateOf(false) } - var selectedNote by remember { mutableStateOf(null) } var tagsExpanded by remember { mutableStateOf(false) } val context = LocalContext.current val selectedTags = @@ -139,27 +130,6 @@ fun DeeprItem( val linkCopied = stringResource(R.string.link_copied) - selectedNote?.let { - AlertDialog( - { - selectedNote = null - }, - title = { - Text("Note") - }, - text = { - Text(it) - }, - confirmButton = { - OutlinedButton({ - selectedNote = null - }) { - Text("Okay") - } - }, - ) - } - val dismissState = rememberSwipeToDismissBoxState( initialValue = SwipeToDismissBoxValue.Settled, @@ -340,103 +310,7 @@ fun DeeprItem( ) } - IconButton(onClick = { expanded = true }) { - Icon( - TablerIcons.DotsVertical, - contentDescription = stringResource(R.string.more_options), - ) - } - - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - if (account.notes.isNotEmpty()) { - DropdownMenuItem( - text = { Text(stringResource(R.string.view_note)) }, - onClick = { - expanded = false - selectedNote = account.notes - }, - leadingIcon = { - Icon( - TablerIcons.Note, - contentDescription = stringResource(R.string.view_note), - ) - }, - ) - } - - // Display last opened time - if (account.lastOpenedAt != null) { - DropdownMenuItem( - text = { - Text( - stringResource( - R.string.last_opened, - formatDateTime(account.lastOpenedAt), - ), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - }, - onClick = { }, - enabled = false, - ) - } - ShortcutMenuItem(account, { - onItemClick(MenuItem.Shortcut(it)) - expanded = false - }) - ShowQRCodeMenuItem(account, { - onItemClick(MenuItem.ShowQrCode(it)) - expanded = false - }) - DropdownMenuItem( - text = { Text(stringResource(R.string.reset_opened_count)) }, - onClick = { - onItemClick(MenuItem.ResetCounter(account)) - expanded = false - }, - leadingIcon = { - Icon( - TablerIcons.Refresh, - contentDescription = stringResource(R.string.reset_opened_count), - ) - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.edit)) }, - onClick = { - onItemClick(MenuItem.Edit(account)) - expanded = false - }, - leadingIcon = { - Icon( - TablerIcons.Edit, - contentDescription = stringResource(R.string.edit), - ) - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.delete)) }, - onClick = { - onItemClick(MenuItem.Delete(account)) - expanded = false - }, - leadingIcon = { - Icon( - TablerIcons.Trash, - contentDescription = stringResource(R.string.delete), - ) - }, - colors = - MenuDefaults.itemColors( - textColor = MaterialTheme.colorScheme.error, - leadingIconColor = MaterialTheme.colorScheme.error, - ), - ) - } + dropdownMenu?.invoke() } Text( @@ -493,7 +367,7 @@ fun DeeprItem( } } -private fun formatDateTime(dateTimeString: String): String { +fun formatDateTime(dateTimeString: String): String { try { val dbFormatter = SimpleDateFormat("yyyy-MM-dd HH:mm:ss", Locale.getDefault()) dbFormatter.timeZone = TimeZone.getTimeZone("UTC") 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 new file mode 100644 index 00000000..4af27dd4 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemCompact.kt @@ -0,0 +1,142 @@ +package com.yogeshpaliyal.deepr.ui.screens.home + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.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.Tags + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun DeeprItemCompact( + account: GetLinksAndTags, + onItemClick: (MenuItem) -> Unit, + onTagClick: (tag: String) -> Unit, + selectedTag: List, + modifier: Modifier = Modifier, + dropdownMenu: (@Composable () -> Unit)? = null, +) { + val context = LocalContext.current + val linkCopied = stringResource(R.string.link_copied) + + Card( + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + ), + modifier = + modifier + .fillMaxWidth() + .combinedClickable( + onClick = { onItemClick(MenuItem.Click(account)) }, + onLongClick = { + val clipboard = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText(linkCopied, account.link) + clipboard.setPrimaryClip(clip) + Toast.makeText(context, linkCopied, Toast.LENGTH_SHORT).show() + }, + ), + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + ) { + if (account.thumbnail.isNotEmpty()) { + AsyncImage( + model = account.thumbnail, + contentDescription = account.name, + modifier = + Modifier + .size(48.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + placeholder = null, + error = null, + contentScale = ContentScale.Crop, + ) + Spacer(modifier = Modifier.width(12.dp)) + } + + Column(modifier = Modifier.weight(1f)) { + if (account.name.isNotEmpty()) { + Text( + text = account.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleSmall, + ) + } + Text( + text = account.link, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + if (account.isFavourite == 1L) { + Icon( + imageVector = Icons.Rounded.Star, + contentDescription = stringResource(R.string.add_to_favourites), + tint = MaterialTheme.colorScheme.primary, + modifier = Modifier.size(20.dp), + ) + Spacer(modifier = Modifier.width(4.dp)) + } + + Text( + text = account.openedCount.toString(), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + dropdownMenu?.invoke() + } + } + } +} 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 new file mode 100644 index 00000000..d1dcc63f --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemGrid.kt @@ -0,0 +1,164 @@ +package com.yogeshpaliyal.deepr.ui.screens.home + +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context +import android.widget.Toast +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.background +import androidx.compose.foundation.combinedClickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +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.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.Tags + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun DeeprItemGrid( + account: GetLinksAndTags, + onItemClick: (MenuItem) -> Unit, + onTagClick: (tag: String) -> Unit, + selectedTag: List, + modifier: Modifier = Modifier, + dropdownMenu: (@Composable () -> Unit)? = null, +) { + val context = LocalContext.current + val linkCopied = stringResource(R.string.link_copied) + + Card( + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + ), + modifier = + modifier + .fillMaxWidth() + .combinedClickable( + onClick = { onItemClick(MenuItem.Click(account)) }, + onLongClick = { + val clipboard = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = ClipData.newPlainText(linkCopied, account.link) + clipboard.setPrimaryClip(clip) + Toast.makeText(context, linkCopied, Toast.LENGTH_SHORT).show() + }, + ), + ) { + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Box { + if (account.thumbnail.isNotEmpty()) { + AsyncImage( + model = account.thumbnail, + contentDescription = account.name, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .background(MaterialTheme.colorScheme.surfaceVariant), + placeholder = null, + error = null, + contentScale = ContentScale.Crop, + ) + } else { + Box( + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .background(MaterialTheme.colorScheme.surfaceVariant), + contentAlignment = Alignment.Center, + ) { + Text( + text = account.name.firstOrNull()?.uppercase() ?: "?", + style = MaterialTheme.typography.displayLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } + + // Favorite indicator overlay + if (account.isFavourite == 1L) { + Icon( + imageVector = Icons.Rounded.Star, + contentDescription = stringResource(R.string.add_to_favourites), + tint = MaterialTheme.colorScheme.primary, + modifier = + Modifier + .align(Alignment.TopEnd) + .padding(8.dp) + .size(24.dp), + ) + } + } + + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(12.dp), + ) { + if (account.name.isNotEmpty()) { + Text( + text = account.name, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleMedium, + ) + Spacer(modifier = Modifier.height(4.dp)) + } + + Text( + text = account.link, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = stringResource(R.string.opened_count, account.openedCount), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + + dropdownMenu?.invoke() + } + } + } + } +} 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 e047f26e..1607f7b2 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 @@ -1,5 +1,6 @@ package com.yogeshpaliyal.deepr.ui.screens.home +import android.text.format.DateUtils.formatDateTime import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.animation.AnimatedVisibility @@ -11,6 +12,7 @@ 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.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -19,10 +21,15 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.grid.GridCells +import androidx.compose.foundation.lazy.grid.LazyVerticalGrid import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.rememberTextFieldState +import androidx.compose.material3.AlertDialog import androidx.compose.material3.AppBarWithSearch import androidx.compose.material3.ContainedLoadingIndicator +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FloatingToolbarDefaults @@ -31,6 +38,8 @@ import androidx.compose.material3.HorizontalFloatingToolbar import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuDefaults +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Scaffold import androidx.compose.material3.SearchBarDefaults @@ -83,12 +92,17 @@ import com.yogeshpaliyal.deepr.util.openDeeplink import com.yogeshpaliyal.deepr.viewmodel.AccountViewModel import compose.icons.TablerIcons import compose.icons.tablericons.ArrowLeft +import compose.icons.tablericons.DotsVertical +import compose.icons.tablericons.Edit import compose.icons.tablericons.Link +import compose.icons.tablericons.Note import compose.icons.tablericons.Plus import compose.icons.tablericons.Qrcode +import compose.icons.tablericons.Refresh import compose.icons.tablericons.Search import compose.icons.tablericons.Settings import compose.icons.tablericons.Tag +import compose.icons.tablericons.Trash import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource @@ -117,6 +131,7 @@ fun HomeScreen( resetSharedText: () -> Unit, ) { var isTagsSelectionActive by remember { mutableStateOf(false) } + var currentViewType by remember { mutableStateOf(ViewType.LIST) } var selectedLink by remember { mutableStateOf(null) } val selectedTag by viewModel.selectedTagFilter.collectAsStateWithLifecycle() @@ -214,17 +229,32 @@ fun HomeScreen( }, trailingIcon = { if (searchBarState.currentValue == SearchBarValue.Collapsed) { - TooltipBox( - positionProvider = - TooltipDefaults.rememberTooltipPositionProvider( - TooltipAnchorPosition.Below, - ), - tooltip = { PlainTooltip { Text(stringResource(R.string.sorting)) } }, - state = rememberTooltipState(), - ) { - FilterMenu(onSortOrderChange = { - viewModel.setSortOrder(it) - }) + Row { + TooltipBox( + positionProvider = + TooltipDefaults.rememberTooltipPositionProvider( + TooltipAnchorPosition.Below, + ), + tooltip = { PlainTooltip { Text(stringResource(R.string.sorting)) } }, + state = rememberTooltipState(), + ) { + FilterMenu(onSortOrderChange = { + viewModel.setSortOrder(it) + }) + } + + TooltipBox( + positionProvider = + TooltipDefaults.rememberTooltipPositionProvider( + TooltipAnchorPosition.Below, + ), + tooltip = { PlainTooltip { Text("View Type") } }, + state = rememberTooltipState(), + ) { + ViewTypeMenu(currentViewType, { + currentViewType = it + }) + } } } else { if (textFieldState.text.isNotEmpty()) { @@ -361,6 +391,7 @@ fun HomeScreen( hazeState = hazeState, contentPaddingValues = contentPadding, selectedTag = selectedTag, + currentViewType = currentViewType, editDeepr = { selectedLink = it }, @@ -416,6 +447,7 @@ fun Content( hazeState: HazeState, selectedTag: List, contentPaddingValues: PaddingValues, + currentViewType: ViewType, modifier: Modifier = Modifier, viewModel: AccountViewModel = koinViewModel(), editDeepr: (GetLinksAndTags) -> Unit = {}, @@ -470,6 +502,7 @@ fun Content( contentPaddingValues = contentPaddingValues, accounts = accounts!!, selectedTag = selectedTag, + viewType = currentViewType, onItemClick = { when (it) { is MenuItem.Click -> { @@ -508,7 +541,10 @@ fun DeeprList( onItemClick: (MenuItem) -> Unit, onTagClick: (String) -> Unit, modifier: Modifier = Modifier, + viewType: ViewType = ViewType.LIST, ) { + var expandedItemId by remember { mutableStateOf(null) } + AnimatedVisibility( visible = accounts.isEmpty(), enter = scaleIn() + expandVertically(expandFrom = Alignment.CenterVertically), @@ -553,30 +589,223 @@ fun DeeprList( Spacer(modifier = Modifier.weight(1f)) // Push content up } } + val dropDownMenu = { + } + 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), + when (viewType) { + ViewType.LIST -> { + LazyColumn( + modifier = modifier, + contentPadding = contentPaddingValues, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + items( + count = accounts.size, + key = { index -> accounts[index].id }, + ) { index -> + val account = accounts[index] + + DeeprItem( + modifier = Modifier.animateItem(), + account = account, + selectedTag = selectedTag, + onItemClick = onItemClick, + onTagClick = onTagClick, + dropdownMenu = { + DropdownMenu(account, onItemClick) + }, + ) + } + } + } + ViewType.GRID -> { + LazyVerticalGrid( + columns = GridCells.Adaptive(minSize = 160.dp), + modifier = modifier, + contentPadding = contentPaddingValues, + horizontalArrangement = Arrangement.spacedBy(8.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items( + count = accounts.size, + key = { index -> accounts[index].id }, + ) { index -> + val account = accounts[index] + + DeeprItemGrid( + modifier = Modifier.animateItem(), + account = account, + selectedTag = selectedTag, + onItemClick = onItemClick, + onTagClick = onTagClick, + dropdownMenu = { + DropdownMenu(account, onItemClick) + }, + ) + } + } + } + ViewType.COMPACT -> { + LazyColumn( + modifier = modifier, + contentPadding = contentPaddingValues, + verticalArrangement = Arrangement.spacedBy(4.dp), + ) { + items( + count = accounts.size, + key = { index -> accounts[index].id }, + ) { index -> + val account = accounts[index] + + DeeprItemCompact( + modifier = Modifier.animateItem(), + account = account, + selectedTag = selectedTag, + onItemClick = onItemClick, + onTagClick = onTagClick, + dropdownMenu = { + DropdownMenu(account, onItemClick) + }, + ) + } + } + } + } + } +} + +@Composable +fun DropdownMenu( + account: GetLinksAndTags, + onItemClick: (MenuItem) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + var selectedNote by remember { mutableStateOf(null) } + + selectedNote?.let { + AlertDialog( + { + selectedNote = null + }, + title = { + Text("Note") + }, + text = { + Text(it) + }, + confirmButton = { + OutlinedButton({ + selectedNote = null + }) { + Text("Okay") + } + }, + ) + } + + Box(modifier = modifier) { + IconButton(onClick = { expanded = true }) { + Icon( + TablerIcons.DotsVertical, + contentDescription = stringResource(R.string.more_options), + ) + } + + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, ) { - items( - count = accounts.size, - key = { index -> accounts[index].id }, - ) { index -> - val account = accounts[index] - - DeeprItem( - modifier = Modifier.animateItem(), - account = account, - selectedTag = selectedTag, - onItemClick = onItemClick, - onTagClick = onTagClick, + if (account.notes.isNotEmpty()) { + DropdownMenuItem( + text = { Text(stringResource(R.string.view_note)) }, + onClick = { + expanded = false + selectedNote = account.notes + }, + leadingIcon = { + Icon( + TablerIcons.Note, + contentDescription = stringResource(R.string.view_note), + ) + }, + ) + } + + // Display last opened time + if (account.lastOpenedAt != null) { + DropdownMenuItem( + text = { + Text( + stringResource( + R.string.last_opened, + formatDateTime(account.lastOpenedAt), + ), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant, + ) + }, + onClick = { }, + enabled = false, ) } + ShortcutMenuItem(account, { + onItemClick(MenuItem.Shortcut(it)) + expanded = false + }) + ShowQRCodeMenuItem(account, { + onItemClick(MenuItem.ShowQrCode(it)) + expanded = false + }) + DropdownMenuItem( + text = { Text(stringResource(R.string.reset_opened_count)) }, + onClick = { + onItemClick(MenuItem.ResetCounter(account)) + expanded = false + }, + leadingIcon = { + Icon( + TablerIcons.Refresh, + contentDescription = stringResource(R.string.reset_opened_count), + ) + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.edit)) }, + onClick = { + onItemClick(MenuItem.Edit(account)) + expanded = false + }, + leadingIcon = { + Icon( + TablerIcons.Edit, + contentDescription = stringResource(R.string.edit), + ) + }, + ) + DropdownMenuItem( + text = { Text(stringResource(R.string.delete)) }, + onClick = { + onItemClick(MenuItem.Delete(account)) + expanded = false + }, + leadingIcon = { + Icon( + TablerIcons.Trash, + contentDescription = stringResource(R.string.delete), + ) + }, + colors = + MenuDefaults.itemColors( + textColor = MaterialTheme.colorScheme.error, + leadingIconColor = MaterialTheme.colorScheme.error, + ), + ) } } } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewType.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewType.kt new file mode 100644 index 00000000..853fa973 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewType.kt @@ -0,0 +1,7 @@ +package com.yogeshpaliyal.deepr.ui.screens.home + +enum class ViewType { + LIST, + GRID, + COMPACT, +} diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewTypeMenu.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewTypeMenu.kt new file mode 100644 index 00000000..3993bf4c --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewTypeMenu.kt @@ -0,0 +1,112 @@ +package com.yogeshpaliyal.deepr.ui.screens.home + +import androidx.compose.foundation.layout.Box +import androidx.compose.material3.DropdownMenu +import androidx.compose.material3.DropdownMenuItem +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.MenuDefaults +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import compose.icons.TablerIcons +import compose.icons.tablericons.LayoutGrid +import compose.icons.tablericons.LayoutList +import compose.icons.tablericons.LayoutRows + +@Composable +fun ViewTypeMenu( + currentViewType: ViewType, + setViewType: (ViewType) -> Unit, + modifier: Modifier = Modifier, +) { + var expanded by remember { mutableStateOf(false) } + Box(modifier) { + IconButton(onClick = { expanded = true }) { + Icon( + when (currentViewType) { + ViewType.LIST -> TablerIcons.LayoutList + ViewType.GRID -> TablerIcons.LayoutGrid + ViewType.COMPACT -> TablerIcons.LayoutRows + }, + contentDescription = "View Type", + ) + } + DropdownMenu( + expanded = expanded, + onDismissRequest = { expanded = false }, + ) { + DropdownMenuItem( + text = { Text("List view") }, + onClick = { + setViewType(ViewType.LIST) + expanded = false + }, + colors = + MenuDefaults.itemColors( + textColor = + if (currentViewType == ViewType.LIST) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ), + leadingIcon = { + Icon( + TablerIcons.LayoutList, + contentDescription = null, + ) + }, + ) + DropdownMenuItem( + text = { Text("Grid view") }, + onClick = { + setViewType(ViewType.GRID) + expanded = false + }, + colors = + MenuDefaults.itemColors( + textColor = + if (currentViewType == ViewType.GRID) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ), + leadingIcon = { + Icon( + TablerIcons.LayoutGrid, + contentDescription = null, + ) + }, + ) + DropdownMenuItem( + text = { Text("Compat view") }, + onClick = { + setViewType(ViewType.COMPACT) + expanded = false + }, + colors = + MenuDefaults.itemColors( + textColor = + if (currentViewType == ViewType.COMPACT) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ), + leadingIcon = { + Icon( + TablerIcons.LayoutRows, + contentDescription = null, + ) + }, + ) + } + } +} From 6463767fea33f3106074e0cb0c896affecc9959b Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Sat, 8 Nov 2025 18:28:39 +0530 Subject: [PATCH 5/5] Refactor view types and enhance item display with swipe functionality --- .../preference/AppPreferenceDataStore.kt | 14 + .../com/yogeshpaliyal/deepr/ui/ColorsUtils.kt | 13 + .../deepr/ui/screens/home/DeeprItem.kt | 154 +-------- .../deepr/ui/screens/home/DeeprItemCompact.kt | 166 ++++----- .../deepr/ui/screens/home/DeeprItemGrid.kt | 198 +++++------ .../ui/screens/home/DeeprItemSwipable.kt | 121 +++++++ .../deepr/ui/screens/home/Home.kt | 327 ++++++++---------- .../deepr/ui/screens/home/ViewType.kt | 17 +- .../deepr/ui/screens/home/ViewTypeMenu.kt | 5 +- .../deepr/viewmodel/AccountViewModel.kt | 11 + app/src/main/res/values/strings.xml | 1 + 11 files changed, 510 insertions(+), 517 deletions(-) create mode 100644 app/src/main/java/com/yogeshpaliyal/deepr/ui/ColorsUtils.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemSwipable.kt diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/preference/AppPreferenceDataStore.kt b/app/src/main/java/com/yogeshpaliyal/deepr/preference/AppPreferenceDataStore.kt index 66ee7ab8..2424c19e 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/preference/AppPreferenceDataStore.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/preference/AppPreferenceDataStore.kt @@ -5,9 +5,11 @@ import androidx.datastore.core.DataStore import androidx.datastore.preferences.core.Preferences import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.edit +import androidx.datastore.preferences.core.intPreferencesKey import androidx.datastore.preferences.core.longPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey import androidx.datastore.preferences.preferencesDataStore +import com.yogeshpaliyal.deepr.ui.screens.home.ViewType import com.yogeshpaliyal.deepr.viewmodel.SortType import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.map @@ -31,6 +33,7 @@ class AppPreferenceDataStore( private val DEFAULT_PAGE_FAVOURITES = booleanPreferencesKey("default_page_favourites") private val IS_THUMBNAIL_ENABLE = booleanPreferencesKey("is_thumbnail_enable") private val SERVER_PORT = stringPreferencesKey("server_port") + private val VIEW_TYPE = intPreferencesKey("view_type") } val getSortingOrder: Flow<@SortType String> = @@ -38,6 +41,11 @@ class AppPreferenceDataStore( preferences[SORTING_ORDER] ?: SortType.SORT_CREATED_BY_DESC } + val viewType: Flow<@ViewType Int> = + context.appDataStore.data.map { preferences -> + preferences[VIEW_TYPE] ?: ViewType.LIST + } + val getUseLinkBasedIcons: Flow = context.appDataStore.data.map { preferences -> preferences[USE_LINK_BASED_ICONS] ?: true // Default to link-based icons @@ -111,6 +119,12 @@ class AppPreferenceDataStore( } } + suspend fun setViewType(viewType: @ViewType Int) { + context.appDataStore.edit { prefs -> + prefs[VIEW_TYPE] = viewType + } + } + suspend fun setSyncFilePath(path: String) { context.appDataStore.edit { prefs -> prefs[SYNC_FILE_PATH] = path diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/ColorsUtils.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/ColorsUtils.kt new file mode 100644 index 00000000..23e71fef --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/ColorsUtils.kt @@ -0,0 +1,13 @@ +package com.yogeshpaliyal.deepr.ui + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.Color + +@Composable +fun getDeeprItemBackgroundColor(isFavourite: Long): Color = + if (isFavourite == 1L) MaterialTheme.colorScheme.surfaceContainerHighest else MaterialTheme.colorScheme.surfaceContainer + +@Composable +fun getDeeprItemTextColor(isFavourite: Long): Color = + if (isFavourite == 1L) MaterialTheme.colorScheme.onSurface else MaterialTheme.colorScheme.onSurfaceVariant 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 cd9b2c36..9cee74dc 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 @@ -8,19 +8,16 @@ import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.aspectRatio -import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.rounded.Star import androidx.compose.material.icons.rounded.StarBorder @@ -30,37 +27,30 @@ import androidx.compose.material3.FilterChip import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.SwipeToDismissBox -import androidx.compose.material3.SwipeToDismissBoxDefaults -import androidx.compose.material3.SwipeToDismissBoxValue import androidx.compose.material3.Text -import androidx.compose.material3.rememberSwipeToDismissBoxState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope 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.graphics.Color import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.testTag import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextOverflow -import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import coil3.compose.AsyncImage import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.R import com.yogeshpaliyal.deepr.Tags +import com.yogeshpaliyal.deepr.ui.getDeeprItemBackgroundColor +import com.yogeshpaliyal.deepr.ui.getDeeprItemTextColor import compose.icons.TablerIcons -import compose.icons.tablericons.Edit -import compose.icons.tablericons.Trash -import kotlinx.coroutines.launch +import compose.icons.tablericons.DotsVertical import java.text.DateFormat import java.text.SimpleDateFormat import java.util.Locale @@ -73,6 +63,10 @@ sealed class MenuItem( item: GetLinksAndTags, ) : MenuItem(item) + class Copy( + item: GetLinksAndTags, + ) : MenuItem(item) + class Shortcut( item: GetLinksAndTags, ) : MenuItem(item) @@ -102,39 +96,6 @@ sealed class MenuItem( ) : MenuItem(item) } -@Composable -@Preview -private fun DeeprItemPreview() { - DeeprItem( - account = - createDeeprObject( - name = "Yogesh Paliyal", - link = "https://yogeshpaliyal.com", - thumbnail = "https://yogeshpaliyal.com/og.png", - ), - {}, - {}, - listOf(), - isThumbnailEnable = true, - ) -} - -@Composable -@Preview -private fun DeeprItemPreview() { - DeeprItem( - account = - createDeeprObject( - name = "Yogesh Paliyal", - link = "https://yogeshpaliyal.com", - thumbnail = "https://yogeshpaliyal.com/og.png", - ), - {}, - {}, - listOf(), - ) -} - @OptIn(ExperimentalFoundationApi::class) @Composable fun DeeprItem( @@ -144,7 +105,6 @@ fun DeeprItem( selectedTag: List, isThumbnailEnable: Boolean, modifier: Modifier = Modifier, - dropdownMenu: (@Composable () -> Unit)? = null, analyticsManager: com.yogeshpaliyal.deepr.analytics.AnalyticsManager = org.koin.compose.koinInject(), ) { var tagsExpanded by remember { mutableStateOf(false) } @@ -154,94 +114,11 @@ fun DeeprItem( val linkCopied = stringResource(R.string.link_copied) - val dismissState = - rememberSwipeToDismissBoxState( - initialValue = SwipeToDismissBoxValue.Settled, - positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold, - ) - - val scope = rememberCoroutineScope() - - SwipeToDismissBox( - modifier = - modifier - .fillMaxSize() - .clip(RoundedCornerShape(8.dp)), - state = dismissState, - onDismiss = { - scope.launch { - dismissState.reset() - } - when (it) { - SwipeToDismissBoxValue.EndToStart -> { - onItemClick(MenuItem.Delete(account)) - false - } - - SwipeToDismissBoxValue.StartToEnd -> { - onItemClick(MenuItem.Edit(account)) - false - } - - else -> { - false - } - } - }, - 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), - ) - } - } - - else -> { - Color.White - } - } - }, - ) { + DeeprItemSwipable(account, onItemClick, modifier) { Card( colors = CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, + containerColor = getDeeprItemBackgroundColor(account.isFavourite), ), modifier = Modifier @@ -294,6 +171,7 @@ fun DeeprItem( maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.labelLarge, + color = getDeeprItemTextColor(account.isFavourite), ) } Text( @@ -301,13 +179,14 @@ fun DeeprItem( maxLines = 1, overflow = TextOverflow.Ellipsis, style = MaterialTheme.typography.bodySmall, + color = getDeeprItemTextColor(account.isFavourite), ) Spacer(modifier = Modifier.height(4.dp)) Row(verticalAlignment = Alignment.CenterVertically) { Text( text = formatDateTime(account.createdAt), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = getDeeprItemTextColor(account.isFavourite), ) } } @@ -329,12 +208,7 @@ fun DeeprItem( } else { stringResource(R.string.add_to_favourites) }, - tint = - if (account.isFavourite == 1L) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.onSurface - }, + tint = getDeeprItemTextColor(account.isFavourite), modifier = Modifier.size(28.dp), ) } @@ -356,7 +230,7 @@ fun DeeprItem( Text( text = stringResource(R.string.opened_count, account.openedCount), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = getDeeprItemTextColor(account.isFavourite), ) } } 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 4af27dd4..5d8daa5d 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 @@ -1,9 +1,5 @@ package com.yogeshpaliyal.deepr.ui.screens.home -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -16,11 +12,10 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.width import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -28,114 +23,119 @@ 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.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.Tags +import com.yogeshpaliyal.deepr.ui.getDeeprItemBackgroundColor +import com.yogeshpaliyal.deepr.ui.getDeeprItemTextColor +import compose.icons.TablerIcons +import compose.icons.tablericons.DotsVertical @OptIn(ExperimentalFoundationApi::class) @Composable fun DeeprItemCompact( account: GetLinksAndTags, onItemClick: (MenuItem) -> Unit, - onTagClick: (tag: String) -> Unit, - selectedTag: List, + isThumbnailEnable: Boolean, modifier: Modifier = Modifier, - dropdownMenu: (@Composable () -> Unit)? = null, ) { - val context = LocalContext.current - val linkCopied = stringResource(R.string.link_copied) - - Card( - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, - ), - modifier = - modifier - .fillMaxWidth() - .combinedClickable( - onClick = { onItemClick(MenuItem.Click(account)) }, - onLongClick = { - val clipboard = - context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText(linkCopied, account.link) - clipboard.setPrimaryClip(clip) - Toast.makeText(context, linkCopied, Toast.LENGTH_SHORT).show() - }, + DeeprItemSwipable(account, onItemClick, modifier) { + Card( + colors = + CardDefaults.cardColors( + containerColor = getDeeprItemBackgroundColor(account.isFavourite), ), - ) { - Row( modifier = Modifier .fillMaxWidth() - .padding(12.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, + .combinedClickable( + onClick = { onItemClick(MenuItem.Click(account)) }, + onLongClick = { + onItemClick(MenuItem.Copy(account)) + }, + ), ) { Row( - modifier = Modifier.weight(1f), + modifier = + Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - if (account.thumbnail.isNotEmpty()) { - AsyncImage( - model = account.thumbnail, - contentDescription = account.name, - modifier = - Modifier - .size(48.dp) - .clip(RoundedCornerShape(8.dp)) - .background(MaterialTheme.colorScheme.surfaceVariant), - placeholder = null, - error = null, - contentScale = ContentScale.Crop, - ) - Spacer(modifier = Modifier.width(12.dp)) - } + Row( + modifier = Modifier.weight(1f), + verticalAlignment = Alignment.CenterVertically, + ) { + if (account.thumbnail.isNotEmpty() && isThumbnailEnable) { + AsyncImage( + model = account.thumbnail, + contentDescription = account.name, + modifier = + Modifier + .size(48.dp) + .clip(RoundedCornerShape(8.dp)) + .background(MaterialTheme.colorScheme.surfaceVariant), + placeholder = null, + error = null, + contentScale = ContentScale.Crop, + ) + Spacer(modifier = Modifier.width(12.dp)) + } - Column(modifier = Modifier.weight(1f)) { - if (account.name.isNotEmpty()) { + Column(modifier = Modifier.weight(1f)) { + if (account.name.isNotEmpty()) { + Text( + text = account.name, + maxLines = 1, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleSmall, + color = getDeeprItemTextColor(account.isFavourite), + ) + } Text( - text = account.name, + text = account.link, maxLines = 1, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleSmall, + style = MaterialTheme.typography.bodySmall, + color = getDeeprItemTextColor(account.isFavourite), ) + Row(modifier = Modifier.fillMaxWidth()) { + Text( + text = stringResource(R.string.opened_count, account.openedCount), + style = MaterialTheme.typography.labelSmall, + color = getDeeprItemTextColor(account.isFavourite), + ) + Spacer(modifier = Modifier.weight(1f)) + account.tagsIds?.split(",")?.size?.let { tagsCount -> + if (tagsCount > 0) { + Text( + text = stringResource(R.string.number_tags, tagsCount), + style = MaterialTheme.typography.labelSmall, + color = getDeeprItemTextColor(account.isFavourite), + ) + } + } + } } - Text( - text = account.link, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) } - } - Row( - verticalAlignment = Alignment.CenterVertically, - ) { - if (account.isFavourite == 1L) { - Icon( - imageVector = Icons.Rounded.Star, - contentDescription = stringResource(R.string.add_to_favourites), - tint = MaterialTheme.colorScheme.primary, - modifier = Modifier.size(20.dp), - ) - Spacer(modifier = Modifier.width(4.dp)) + Row( + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton(onClick = { + onItemClick(MenuItem.MoreOptionsBottomSheet(account)) + }) { + Icon( + imageVector = TablerIcons.DotsVertical, + contentDescription = stringResource(R.string.more_options), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } } - - Text( - text = account.openedCount.toString(), - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - dropdownMenu?.invoke() } } } 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 d1dcc63f..e9bbc91f 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 @@ -1,9 +1,5 @@ package com.yogeshpaliyal.deepr.ui.screens.home -import android.content.ClipData -import android.content.ClipboardManager -import android.content.Context -import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.background import androidx.compose.foundation.combinedClickable @@ -16,147 +12,129 @@ import androidx.compose.foundation.layout.aspectRatio import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.rounded.Star import androidx.compose.material3.Card import androidx.compose.material3.CardDefaults import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable 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.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.Tags +import com.yogeshpaliyal.deepr.ui.getDeeprItemBackgroundColor +import com.yogeshpaliyal.deepr.ui.getDeeprItemTextColor +import compose.icons.TablerIcons +import compose.icons.tablericons.DotsVertical @OptIn(ExperimentalFoundationApi::class) @Composable fun DeeprItemGrid( account: GetLinksAndTags, onItemClick: (MenuItem) -> Unit, - onTagClick: (tag: String) -> Unit, - selectedTag: List, modifier: Modifier = Modifier, - dropdownMenu: (@Composable () -> Unit)? = null, + isThumbnailEnable: Boolean = true, ) { - val context = LocalContext.current - val linkCopied = stringResource(R.string.link_copied) - - Card( - colors = - CardDefaults.cardColors( - containerColor = MaterialTheme.colorScheme.surfaceContainerHighest, - ), - modifier = - modifier - .fillMaxWidth() - .combinedClickable( - onClick = { onItemClick(MenuItem.Click(account)) }, - onLongClick = { - val clipboard = - context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager - val clip = ClipData.newPlainText(linkCopied, account.link) - clipboard.setPrimaryClip(clip) - Toast.makeText(context, linkCopied, Toast.LENGTH_SHORT).show() - }, + DeeprItemSwipable(account, onItemClick, modifier) { + Card( + colors = + CardDefaults.cardColors( + containerColor = getDeeprItemBackgroundColor(account.isFavourite), ), - ) { - Column( - modifier = Modifier.fillMaxWidth(), + modifier = + Modifier + .fillMaxWidth() + .combinedClickable( + onClick = { onItemClick(MenuItem.Click(account)) }, + onLongClick = { + onItemClick(MenuItem.Copy(account)) + }, + ), ) { - Box { - if (account.thumbnail.isNotEmpty()) { - AsyncImage( - model = account.thumbnail, - contentDescription = account.name, - modifier = - Modifier - .fillMaxWidth() - .aspectRatio(1f) - .background(MaterialTheme.colorScheme.surfaceVariant), - placeholder = null, - error = null, - contentScale = ContentScale.Crop, - ) - } else { - Box( - modifier = - Modifier - .fillMaxWidth() - .aspectRatio(1f) - .background(MaterialTheme.colorScheme.surfaceVariant), - contentAlignment = Alignment.Center, - ) { - Text( - text = account.name.firstOrNull()?.uppercase() ?: "?", - style = MaterialTheme.typography.displayLarge, - color = MaterialTheme.colorScheme.onSurfaceVariant, + Column( + modifier = Modifier.fillMaxWidth(), + ) { + Box { + if (account.thumbnail.isNotEmpty() && isThumbnailEnable) { + AsyncImage( + model = account.thumbnail, + contentDescription = account.name, + modifier = + Modifier + .fillMaxWidth() + .aspectRatio(1f) + .background(MaterialTheme.colorScheme.surfaceVariant), + placeholder = null, + error = null, + contentScale = ContentScale.Crop, ) } } - // Favorite indicator overlay - if (account.isFavourite == 1L) { - Icon( - imageVector = Icons.Rounded.Star, - contentDescription = stringResource(R.string.add_to_favourites), - tint = MaterialTheme.colorScheme.primary, - modifier = - Modifier - .align(Alignment.TopEnd) - .padding(8.dp) - .size(24.dp), - ) - } - } + Column( + modifier = + Modifier + .fillMaxWidth() + .padding(12.dp), + ) { + if (account.name.isNotEmpty()) { + Text( + text = account.name, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = MaterialTheme.typography.titleMedium, + color = getDeeprItemTextColor(account.isFavourite), + ) + Spacer(modifier = Modifier.height(4.dp)) + } - Column( - modifier = - Modifier - .fillMaxWidth() - .padding(12.dp), - ) { - if (account.name.isNotEmpty()) { Text( - text = account.name, - maxLines = 2, + text = account.link, + maxLines = 1, overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.titleMedium, - ) - Spacer(modifier = Modifier.height(4.dp)) - } - - Text( - text = account.link, - maxLines = 1, - overflow = TextOverflow.Ellipsis, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, - ) - - Spacer(modifier = Modifier.height(8.dp)) - - Row( - modifier = Modifier.fillMaxWidth(), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically, - ) { - Text( - text = stringResource(R.string.opened_count, account.openedCount), style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant, + color = getDeeprItemTextColor(account.isFavourite), ) - dropdownMenu?.invoke() + Spacer(modifier = Modifier.height(8.dp)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Column(modifier = Modifier.weight(1f)) { + Text( + text = stringResource(R.string.opened_count, account.openedCount), + style = MaterialTheme.typography.bodySmall, + color = getDeeprItemTextColor(account.isFavourite), + ) + account.tagsIds?.split(",")?.size?.let { tagsCount -> + if (tagsCount > 0) { + Text( + text = stringResource(R.string.number_tags, tagsCount), + style = MaterialTheme.typography.labelSmall, + color = getDeeprItemTextColor(account.isFavourite), + ) + } + } + } + IconButton(onClick = { + onItemClick(MenuItem.MoreOptionsBottomSheet(account)) + }) { + Icon( + imageVector = TablerIcons.DotsVertical, + contentDescription = stringResource(R.string.more_options), + tint = MaterialTheme.colorScheme.onSurfaceVariant, + ) + } + } } } } 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 new file mode 100644 index 00000000..f70be059 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/DeeprItemSwipable.kt @@ -0,0 +1,121 @@ +package com.yogeshpaliyal.deepr.ui.screens.home + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Icon +import androidx.compose.material3.SwipeToDismissBox +import androidx.compose.material3.SwipeToDismissBoxDefaults +import androidx.compose.material3.SwipeToDismissBoxValue +import androidx.compose.material3.rememberSwipeToDismissBoxState +import androidx.compose.runtime.Composable +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 compose.icons.TablerIcons +import compose.icons.tablericons.Edit +import compose.icons.tablericons.Trash +import kotlinx.coroutines.launch + +@Composable +fun DeeprItemSwipable( + account: GetLinksAndTags, + onItemClick: (MenuItem) -> Unit, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val dismissState = + rememberSwipeToDismissBoxState( + initialValue = SwipeToDismissBoxValue.Settled, + positionalThreshold = SwipeToDismissBoxDefaults.positionalThreshold, + ) + + val scope = rememberCoroutineScope() + + SwipeToDismissBox( + modifier = + modifier + .fillMaxSize() + .clip(RoundedCornerShape(8.dp)), + state = dismissState, + onDismiss = { + scope.launch { + dismissState.reset() + } + when (it) { + SwipeToDismissBoxValue.EndToStart -> { + onItemClick(MenuItem.Delete(account)) + false + } + + SwipeToDismissBoxValue.StartToEnd -> { + onItemClick(MenuItem.Edit(account)) + false + } + + else -> { + false + } + } + }, + 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), + ) + } + } + + else -> { + Color.White + } + } + }, + ) { + content() + } +} 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 7c51e2f2..60b93c41 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 @@ -1,6 +1,8 @@ package com.yogeshpaliyal.deepr.ui.screens.home -import android.text.format.DateUtils.formatDateTime +import android.content.ClipData +import android.content.ClipboardManager +import android.content.Context import android.widget.Toast import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.animation.AnimatedVisibility @@ -13,6 +15,7 @@ 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.FlowRow import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer @@ -24,18 +27,19 @@ import androidx.compose.foundation.layout.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.grid.GridCells -import androidx.compose.foundation.lazy.grid.LazyVerticalGrid +import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid +import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.rememberTextFieldState -import androidx.compose.material3.AlertDialog import androidx.compose.foundation.text.selection.SelectionContainer +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.rounded.Star +import androidx.compose.material.icons.rounded.StarBorder import androidx.compose.material3.AppBarWithSearch import androidx.compose.material3.ContainedLoadingIndicator -import androidx.compose.material3.DropdownMenu -import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.FilterChip import androidx.compose.material3.FloatingToolbarDefaults import androidx.compose.material3.FloatingToolbarExitDirection import androidx.compose.material3.HorizontalFloatingToolbar @@ -46,8 +50,6 @@ import androidx.compose.material3.ListItemColors import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.LocalTextStyle import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.MenuDefaults -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.PlainTooltip import androidx.compose.material3.Scaffold @@ -81,6 +83,7 @@ import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle +import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp @@ -92,7 +95,9 @@ import com.yogeshpaliyal.deepr.GetLinksAndTags 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.ui.components.ClearInputIconButton import com.yogeshpaliyal.deepr.ui.components.CreateShortcutDialog import com.yogeshpaliyal.deepr.ui.components.DeleteConfirmationDialog @@ -100,6 +105,15 @@ import com.yogeshpaliyal.deepr.ui.components.QrCodeDialog import com.yogeshpaliyal.deepr.ui.components.ServerStatusBar import com.yogeshpaliyal.deepr.ui.screens.LocalNetworkServer import com.yogeshpaliyal.deepr.ui.screens.Settings +import com.yogeshpaliyal.deepr.ui.screens.home.MenuItem.Click +import com.yogeshpaliyal.deepr.ui.screens.home.MenuItem.Copy +import com.yogeshpaliyal.deepr.ui.screens.home.MenuItem.Delete +import com.yogeshpaliyal.deepr.ui.screens.home.MenuItem.Edit +import com.yogeshpaliyal.deepr.ui.screens.home.MenuItem.FavouriteClick +import com.yogeshpaliyal.deepr.ui.screens.home.MenuItem.MoreOptionsBottomSheet +import com.yogeshpaliyal.deepr.ui.screens.home.MenuItem.ResetCounter +import com.yogeshpaliyal.deepr.ui.screens.home.MenuItem.Shortcut +import com.yogeshpaliyal.deepr.ui.screens.home.MenuItem.ShowQrCode import com.yogeshpaliyal.deepr.util.QRScanner import com.yogeshpaliyal.deepr.util.isValidDeeplink import com.yogeshpaliyal.deepr.util.normalizeLink @@ -107,7 +121,6 @@ import com.yogeshpaliyal.deepr.util.openDeeplink import com.yogeshpaliyal.deepr.viewmodel.AccountViewModel import compose.icons.TablerIcons import compose.icons.tablericons.ArrowLeft -import compose.icons.tablericons.DotsVertical import compose.icons.tablericons.Edit import compose.icons.tablericons.Link import compose.icons.tablericons.Note @@ -147,7 +160,7 @@ fun HomeScreen( resetSharedText: () -> Unit, ) { var isTagsSelectionActive by remember { mutableStateOf(false) } - var currentViewType by remember { mutableStateOf(ViewType.LIST) } + val currentViewType by viewModel.viewType.collectAsStateWithLifecycle() var selectedLink by remember { mutableStateOf(null) } val selectedTag by viewModel.selectedTagFilter.collectAsStateWithLifecycle() @@ -269,7 +282,7 @@ fun HomeScreen( state = rememberTooltipState(), ) { ViewTypeMenu(currentViewType, { - currentViewType = it + viewModel.setViewType(it) }) } } @@ -309,7 +322,7 @@ fun HomeScreen( ServerStatusBar( onServerStatusClick = { // Navigate to LocalNetworkServer screen when status bar is clicked - analyticsManager.logEvent(com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.NAVIGATE_LOCAL_SERVER) + analyticsManager.logEvent(AnalyticsEvents.NAVIGATE_LOCAL_SERVER) if (backStack.lastOrNull() !is LocalNetworkServer) { backStack.add(LocalNetworkServer) } @@ -360,7 +373,7 @@ fun HomeScreen( colors = FloatingToolbarDefaults.standardFloatingToolbarColors(), content = { IconButton(onClick = { - analyticsManager.logEvent(com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.SCAN_QR_CODE) + analyticsManager.logEvent(AnalyticsEvents.SCAN_QR_CODE) qrScanner.launch(ScanOptions()) }) { Icon( @@ -378,7 +391,7 @@ fun HomeScreen( } IconButton(onClick = { // Settings action - analyticsManager.logEvent(com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.NAVIGATE_SETTINGS) + analyticsManager.logEvent(AnalyticsEvents.NAVIGATE_SETTINGS) backStack.add(Settings) }) { Icon( @@ -468,7 +481,7 @@ fun Content( hazeState: HazeState, selectedTag: List, contentPaddingValues: PaddingValues, - currentViewType: ViewType, + currentViewType: @ViewType Int, searchQuery: String, favouriteFilter: Int, modifier: Modifier = Modifier, @@ -522,49 +535,60 @@ fun Content( val onItemClick: (MenuItem) -> Unit = { showMoreSelectedItem = null when (it) { - is MenuItem.Click -> { + is Click -> { viewModel.incrementOpenedCount(it.item.id) openDeeplink(context, it.item.link) analyticsManager.logEvent( - com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.OPEN_LINK, - mapOf(com.yogeshpaliyal.deepr.analytics.AnalyticsParams.LINK_ID to it.item.id), + AnalyticsEvents.OPEN_LINK, + mapOf(AnalyticsParams.LINK_ID to it.item.id), ) } - is MenuItem.Delete -> { - analyticsManager.logEvent(com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.ITEM_MENU_DELETE) + is Delete -> { + analyticsManager.logEvent(AnalyticsEvents.ITEM_MENU_DELETE) showDeleteConfirmDialog = it.item } is MenuItem.Edit -> { - analyticsManager.logEvent(com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.ITEM_MENU_EDIT) + analyticsManager.logEvent(AnalyticsEvents.ITEM_MENU_EDIT) editDeepr(it.item) } - is MenuItem.FavouriteClick -> { - analyticsManager.logEvent(com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.ITEM_MENU_FAVOURITE) + is FavouriteClick -> { + analyticsManager.logEvent(AnalyticsEvents.ITEM_MENU_FAVOURITE) viewModel.toggleFavourite(it.item.id) } - is MenuItem.ResetCounter -> { - analyticsManager.logEvent(com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.ITEM_MENU_RESET_COUNTER) + is ResetCounter -> { + analyticsManager.logEvent(AnalyticsEvents.ITEM_MENU_RESET_COUNTER) viewModel.resetOpenedCount(it.item.id) Toast.makeText(context, "Opened count reset", Toast.LENGTH_SHORT).show() } - is MenuItem.Shortcut -> { - analyticsManager.logEvent(com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.ITEM_MENU_SHORTCUT) + is Shortcut -> { + analyticsManager.logEvent(AnalyticsEvents.ITEM_MENU_SHORTCUT) showShortcutDialog = it.item } - is MenuItem.ShowQrCode -> { - analyticsManager.logEvent(com.yogeshpaliyal.deepr.analytics.AnalyticsEvents.ITEM_MENU_QR_CODE) + is ShowQrCode -> { + analyticsManager.logEvent(AnalyticsEvents.ITEM_MENU_QR_CODE) showQrCodeDialog = it.item } - is MenuItem.MoreOptionsBottomSheet -> { + is MoreOptionsBottomSheet -> { showMoreSelectedItem = it.item } + + is Copy -> { + val clipboard = + context.getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager + val clip = + ClipData.newPlainText(context.getString(R.string.link_copied), it.item.link) + clipboard.setPrimaryClip(clip) + Toast + .makeText(context, context.getString(R.string.link_copied), Toast.LENGTH_SHORT) + .show() + } } } @@ -578,20 +602,25 @@ fun Content( contentPaddingValues = contentPaddingValues, accounts = accounts!!, selectedTag = selectedTag, + onTagClick = { + viewModel.setSelectedTagByName(it) + }, + isThumbnailEnable = isThumbnailEnable, + searchQuery = searchQuery, + favouriteFilter = favouriteFilter, viewType = currentViewType, - viewType = currentViewType, - onItemClick = { - when (it) { - is MenuItem.Click -> { - viewModel.incrementOpenedCount(it.item.id) - openDeeplink(context, it.item.link) - } - + onItemClick = onItemClick, + ) + } showMoreSelectedItem?.let { account -> ModalBottomSheet(sheetState = showMoreBottomSheet, onDismissRequest = { showMoreSelectedItem = null }) { val isThumbnailEnable by viewModel.isThumbnailEnable.collectAsStateWithLifecycle() + var tagsExpanded by remember { mutableStateOf(false) } + val selectedTags = + remember(account.tagsNames) { account.tagsNames?.split(",")?.toMutableList() } + LazyColumn { item { ListItem( @@ -611,7 +640,7 @@ fun Content( .padding(4.dp) .fillMaxWidth() .clickable { - onItemClick(MenuItem.Click(account)) + onItemClick(Click(account)) }, colors = ListItemDefaults.colors(containerColor = Color.Transparent), ) @@ -634,6 +663,24 @@ fun Content( } } + item { + MenuListItem( + text = + if (account.isFavourite == 1L) { + stringResource(R.string.remove_from_favourites) + } else { + stringResource( + R.string.add_to_favourites, + ) + }, + icon = if (account.isFavourite == 1L) Icons.Rounded.Star else Icons.Rounded.StarBorder, + selectable = true, + onClick = { + onItemClick(FavouriteClick(account)) + }, + ) + } + if (account.notes.isNotEmpty()) { item { MenuListItem( @@ -646,7 +693,7 @@ fun Content( item { ShortcutMenuItem(account, { - onItemClick(MenuItem.Shortcut(it)) + onItemClick(Shortcut(it)) }) } @@ -655,7 +702,7 @@ fun Content( text = stringResource(R.string.show_qr_code), icon = TablerIcons.Qrcode, onClick = { - onItemClick(MenuItem.ShowQrCode(account)) + onItemClick(ShowQrCode(account)) }, ) } @@ -665,7 +712,7 @@ fun Content( text = stringResource(R.string.reset_opened_count), icon = TablerIcons.Refresh, onClick = { - onItemClick(MenuItem.ResetCounter(account)) + onItemClick(ResetCounter(account)) }, ) } @@ -675,7 +722,7 @@ fun Content( text = stringResource(R.string.edit), icon = TablerIcons.Edit, onClick = { - onItemClick(MenuItem.Edit(account)) + onItemClick(Edit(account)) }, ) } @@ -684,7 +731,7 @@ fun Content( text = stringResource(R.string.delete), icon = TablerIcons.Trash, onClick = { - onItemClick(MenuItem.Delete(account)) + onItemClick(Delete(account)) }, colors = ListItemDefaults.colors( @@ -706,7 +753,7 @@ fun Content( ), textStyle = MaterialTheme.typography.bodySmall, onClick = { - onItemClick(MenuItem.Edit(account)) + onItemClick(Edit(account)) }, icon = null, colors = @@ -716,6 +763,53 @@ fun Content( ) } } + + item { + Column(modifier = Modifier.padding(horizontal = 12.dp)) { + // Determine max tags to show based on expanded state + val maxTagsToShow = if (tagsExpanded) selectedTags?.size ?: 0 else 9 + val visibleTags = selectedTags?.take(maxTagsToShow) ?: emptyList() + val hiddenTagsCount = (selectedTags?.size ?: 0) - visibleTags.size + + FlowRow( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + visibleTags.forEach { tag -> + val isSelected = selectedTag.any { it.name == tag.trim() } + FilterChip( + modifier = Modifier.padding(0.dp), + elevation = null, + selected = isSelected, + onClick = { + viewModel.setSelectedTagByName(tag) + showMoreSelectedItem = null + }, + label = { Text(tag.trim()) }, + ) + } + } + + // Show "Load More" or "Show Less" button if there are more than 9 tags + if ((selectedTags?.size ?: 0) > 9) { + androidx.compose.material3.TextButton( + onClick = { tagsExpanded = !tagsExpanded }, + modifier = Modifier.padding(start = 4.dp), + contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp), + ) { + Text( + text = + if (tagsExpanded) { + stringResource(R.string.show_less_tags) + } else { + stringResource(R.string.load_more_tags, hiddenTagsCount) + }, + style = MaterialTheme.typography.bodyMedium.copy(fontWeight = FontWeight.Bold), + ) + } + } + } + } } } } @@ -775,9 +869,8 @@ fun DeeprList( searchQuery: String, favouriteFilter: Int, modifier: Modifier = Modifier, - viewType: ViewType = ViewType.LIST, + viewType: @ViewType Int = ViewType.LIST, ) { - var expandedItemId by remember { mutableStateOf(null) } // Determine which empty state to show val isSearchActive = searchQuery.isNotBlank() val isFavouriteFilterActive = favouriteFilter == 1 @@ -809,12 +902,14 @@ fun DeeprList( R.string.no_search_results, R.string.no_search_results_description, ) + isTagFilterActive -> Triple( TablerIcons.Tag, R.string.no_links_with_tags, R.string.no_links_with_tags_description, ) + isFavouriteFilterActive -> Triple( TablerIcons.Link, @@ -857,8 +952,6 @@ fun DeeprList( Spacer(modifier = Modifier.weight(1f)) // Push content up } } - val dropDownMenu = { - } AnimatedVisibility( visible = accounts.isNotEmpty(), @@ -884,20 +977,19 @@ fun DeeprList( selectedTag = selectedTag, onItemClick = onItemClick, onTagClick = onTagClick, - dropdownMenu = { - DropdownMenu(account, onItemClick) - }, + isThumbnailEnable = isThumbnailEnable, ) } } } + ViewType.GRID -> { - LazyVerticalGrid( - columns = GridCells.Adaptive(minSize = 160.dp), + LazyVerticalStaggeredGrid( + columns = StaggeredGridCells.Adaptive(minSize = 160.dp), modifier = modifier, contentPadding = contentPaddingValues, horizontalArrangement = Arrangement.spacedBy(8.dp), - verticalArrangement = Arrangement.spacedBy(8.dp), + verticalItemSpacing = 8.dp, ) { items( count = accounts.size, @@ -908,16 +1000,12 @@ fun DeeprList( DeeprItemGrid( modifier = Modifier.animateItem(), account = account, - selectedTag = selectedTag, onItemClick = onItemClick, - onTagClick = onTagClick, - dropdownMenu = { - DropdownMenu(account, onItemClick) - }, ) } } } + ViewType.COMPACT -> { LazyColumn( modifier = modifier, @@ -933,12 +1021,8 @@ fun DeeprList( DeeprItemCompact( modifier = Modifier.animateItem(), account = account, - selectedTag = selectedTag, onItemClick = onItemClick, - onTagClick = onTagClick, - dropdownMenu = { - DropdownMenu(account, onItemClick) - }, + isThumbnailEnable = isThumbnailEnable, ) } } @@ -946,116 +1030,3 @@ fun DeeprList( } } } - -@Composable -fun DropdownMenu( - account: GetLinksAndTags, - onItemClick: (MenuItem) -> Unit, - modifier: Modifier = Modifier, -) { - var expanded by remember { mutableStateOf(false) } - var selectedNote by remember { mutableStateOf(null) } - - selectedNote?.let { - AlertDialog( - { - selectedNote = null - }, - title = { - Text("Note") - }, - text = { - Text(it) - }, - confirmButton = { - OutlinedButton({ - selectedNote = null - }) { - Text("Okay") - } - }, - ) - } - - Box(modifier = modifier) { - IconButton(onClick = { expanded = true }) { - Icon( - TablerIcons.DotsVertical, - contentDescription = stringResource(R.string.more_options), - ) - } - - DropdownMenu( - expanded = expanded, - onDismissRequest = { expanded = false }, - ) { - items( - count = accounts.size, - key = { index -> accounts[index].id }, - ) { index -> - val account = accounts[index] - - DeeprItem( - modifier = Modifier.animateItem(), - account = account, - selectedTag = selectedTag, - onItemClick = onItemClick, - onTagClick = onTagClick, - isThumbnailEnable = isThumbnailEnable, - ) - } - ShortcutMenuItem(account, { - onItemClick(MenuItem.Shortcut(it)) - expanded = false - }) - ShowQRCodeMenuItem(account, { - onItemClick(MenuItem.ShowQrCode(it)) - expanded = false - }) - DropdownMenuItem( - text = { Text(stringResource(R.string.reset_opened_count)) }, - onClick = { - onItemClick(MenuItem.ResetCounter(account)) - expanded = false - }, - leadingIcon = { - Icon( - TablerIcons.Refresh, - contentDescription = stringResource(R.string.reset_opened_count), - ) - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.edit)) }, - onClick = { - onItemClick(MenuItem.Edit(account)) - expanded = false - }, - leadingIcon = { - Icon( - TablerIcons.Edit, - contentDescription = stringResource(R.string.edit), - ) - }, - ) - DropdownMenuItem( - text = { Text(stringResource(R.string.delete)) }, - onClick = { - onItemClick(MenuItem.Delete(account)) - expanded = false - }, - leadingIcon = { - Icon( - TablerIcons.Trash, - contentDescription = stringResource(R.string.delete), - ) - }, - colors = - MenuDefaults.itemColors( - textColor = MaterialTheme.colorScheme.error, - leadingIconColor = MaterialTheme.colorScheme.error, - ), - ) - } - } -} diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewType.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewType.kt index 853fa973..29a309e9 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewType.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewType.kt @@ -1,7 +1,16 @@ package com.yogeshpaliyal.deepr.ui.screens.home -enum class ViewType { - LIST, - GRID, - COMPACT, +import androidx.annotation.IntDef + +@Retention(AnnotationRetention.SOURCE) +@Target( + AnnotationTarget.TYPE, +) +@IntDef(value = [ViewType.LIST, ViewType.GRID, ViewType.COMPACT]) +annotation class ViewType { + companion object { + const val LIST = 0 + const val GRID = 1 + const val COMPACT = 2 + } } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewTypeMenu.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewTypeMenu.kt index 3993bf4c..f5d7dc5d 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewTypeMenu.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/ViewTypeMenu.kt @@ -21,8 +21,8 @@ import compose.icons.tablericons.LayoutRows @Composable fun ViewTypeMenu( - currentViewType: ViewType, - setViewType: (ViewType) -> Unit, + currentViewType: @ViewType Int, + setViewType: (@ViewType Int) -> Unit, modifier: Modifier = Modifier, ) { var expanded by remember { mutableStateOf(false) } @@ -33,6 +33,7 @@ fun ViewTypeMenu( ViewType.LIST -> TablerIcons.LayoutList ViewType.GRID -> TablerIcons.LayoutGrid ViewType.COMPACT -> TablerIcons.LayoutRows + else -> TablerIcons.LayoutList }, contentDescription = "View Type", ) 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 710a564d..2818b1c4 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/viewmodel/AccountViewModel.kt @@ -19,6 +19,7 @@ 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.ui.screens.home.ViewType import com.yogeshpaliyal.deepr.util.RequestResult import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -581,6 +582,10 @@ class AccountViewModel( preferenceDataStore.getAutoBackupLocation .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), "") + val viewType = + preferenceDataStore.viewType + .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), ViewType.LIST) + val lastBackupTime = preferenceDataStore.getLastBackupTime .stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000), 0L) @@ -591,6 +596,12 @@ class AccountViewModel( } } + fun setViewType(viewType: @ViewType Int) { + viewModelScope.launch(Dispatchers.IO) { + preferenceDataStore.setViewType(viewType) + } + } + fun setAutoBackupLocation(location: String) { viewModelScope.launch(Dispatchers.IO) { preferenceDataStore.setAutoBackupLocation(location) diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8b4db7e8..1e27473c 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -55,6 +55,7 @@ Suggestions: Filter +%d more + Tags: %d Show less Add shortcut Edit shortcut