From dab3914631a6a5ff347f646ca6d35ef14f3c56ac Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Wed, 12 Nov 2025 20:08:30 +0530 Subject: [PATCH 1/6] feat: WIP Refactor navigation handling to use LocalNavigator and update screen structure --- .../com/yogeshpaliyal/deepr/MainActivity.kt | 300 ++++++++++++++---- .../yogeshpaliyal/deepr/ui/screens/AboutUs.kt | 15 +- .../deepr/ui/screens/BackupScreen.kt | 16 +- .../deepr/ui/screens/LocalNetworkServer.kt | 15 +- .../deepr/ui/screens/RestoreScreen.kt | 17 +- .../deepr/ui/screens/ScanQRVirtualScreen.kt | 17 + .../deepr/ui/screens/Settings.kt | 34 +- .../yogeshpaliyal/deepr/ui/screens/Splash.kt | 48 +++ .../ui/screens/TransferLinkLocalServer.kt | 15 +- .../deepr/ui/screens/home/Home.kt | 89 +++--- 10 files changed, 421 insertions(+), 145 deletions(-) create mode 100644 app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt create mode 100644 app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt b/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt index 24b740ad..bcac2dc0 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt @@ -3,44 +3,58 @@ package com.yogeshpaliyal.deepr import android.content.Context import android.content.Intent import android.os.Bundle +import android.widget.Toast import androidx.activity.ComponentActivity +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge -import androidx.compose.foundation.layout.Column +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.slideInVertically +import androidx.compose.animation.slideOutVertically +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.BottomAppBar +import androidx.compose.material3.BottomAppBarDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.NavigationBarItem +import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface -import androidx.compose.material3.Text import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavEntry +import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator +import com.journeyapps.barcodescanner.ScanOptions import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore -import com.yogeshpaliyal.deepr.ui.screens.AboutUs -import com.yogeshpaliyal.deepr.ui.screens.AboutUsScreen -import com.yogeshpaliyal.deepr.ui.screens.BackupScreen -import com.yogeshpaliyal.deepr.ui.screens.BackupScreenContent -import com.yogeshpaliyal.deepr.ui.screens.LocalNetworkServer -import com.yogeshpaliyal.deepr.ui.screens.LocalNetworkServerScreen -import com.yogeshpaliyal.deepr.ui.screens.RestoreScreen -import com.yogeshpaliyal.deepr.ui.screens.RestoreScreenContent +import com.yogeshpaliyal.deepr.ui.screens.ScanQRVirtualScreen import com.yogeshpaliyal.deepr.ui.screens.Settings -import com.yogeshpaliyal.deepr.ui.screens.SettingsScreen -import com.yogeshpaliyal.deepr.ui.screens.TransferLinkLocalNetworkServer -import com.yogeshpaliyal.deepr.ui.screens.TransferLinkLocalServerScreen -import com.yogeshpaliyal.deepr.ui.screens.home.Home -import com.yogeshpaliyal.deepr.ui.screens.home.HomeScreen +import com.yogeshpaliyal.deepr.ui.screens.home.Dashboard2 +import com.yogeshpaliyal.deepr.ui.screens.home.createDeeprObject import com.yogeshpaliyal.deepr.ui.theme.DeeprTheme import com.yogeshpaliyal.deepr.util.LanguageUtil +import com.yogeshpaliyal.deepr.util.QRScanner +import com.yogeshpaliyal.deepr.util.isValidDeeplink +import com.yogeshpaliyal.deepr.util.normalizeLink import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking data class SharedLink( @@ -81,7 +95,9 @@ class MainActivity : ComponentActivity() { setContent { val preferenceDataStore = remember { AppPreferenceDataStore(this) } - val themeMode by preferenceDataStore.getThemeMode.collectAsStateWithLifecycle(initialValue = "system") + val themeMode by preferenceDataStore.getThemeMode.collectAsStateWithLifecycle( + initialValue = "system" + ) DeeprTheme(themeMode = themeMode) { Surface { @@ -123,18 +139,146 @@ class MainActivity : ComponentActivity() { } } +val LocalNavigator = + compositionLocalOf> { TopLevelBackStack(Dashboard2 {}) } + +interface Screen : NavKey { + @Composable + fun Content() +} + +interface TopLevelRoute : Screen { + val icon: ImageVector +} + + +private val TOP_LEVEL_ROUTES: List = listOf(Dashboard2 {}, ScanQRVirtualScreen(), Settings) + +class TopLevelBackStack(startKey: T) { + + // Maintain a stack for each top level route + private var topLevelStacks: LinkedHashMap> = linkedMapOf( + startKey to mutableStateListOf(startKey) + ) + + // Expose the current top level route for consumers + var topLevelKey by mutableStateOf(startKey) + private set + + // Expose the back stack so it can be rendered by the NavDisplay + val backStack = mutableStateListOf(startKey) + + private fun updateBackStack() = + backStack.apply { + clear() + addAll(topLevelStacks.flatMap { it.value }) + } + + + fun clearStackAndAdd(key: T) { + topLevelStacks.clear() + addTopLevel(key) + } + + fun addTopLevel(key: T) { + + // If the top level doesn't exist, add it + if (topLevelStacks[key] == null) { + topLevelStacks.put(key, mutableStateListOf(key)) + } else { + // Otherwise just move it to the end of the stacks + topLevelStacks.apply { + remove(key)?.let { + put(key, it) + } + } + } + topLevelKey = key + updateBackStack() + } + + fun add(key: T) { + topLevelStacks[topLevelKey]?.add(key) + updateBackStack() + } + + fun getLast() = backStack.last() + + fun removeLast() { + val removedKey = topLevelStacks[topLevelKey]?.removeLastOrNull() + // If the removed key was a top level key, remove the associated top level stack + topLevelStacks.remove(removedKey) + topLevelKey = topLevelStacks.keys.last() + updateBackStack() + } +} + +@OptIn(ExperimentalMaterial3Api::class) @Composable fun Dashboard( modifier: Modifier = Modifier, sharedText: SharedLink? = null, resetSharedText: () -> Unit, ) { - val backStack = remember(sharedText) { mutableStateListOf(Home) } + val backStack = remember { TopLevelBackStack(Dashboard2(sharedText = sharedText, resetSharedText = resetSharedText)) } + val current = backStack.getLast() + val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior() + val hapticFeedback = LocalHapticFeedback.current + val context = LocalContext.current + + val qrScanner = + rememberLauncherForActivityResult( + QRScanner(), + ) { result -> + if (result.contents == null) { + Toast.makeText(context, "No Data found", Toast.LENGTH_SHORT).show() + } else { + val normalizedLink = normalizeLink(result.contents) + if (isValidDeeplink(normalizedLink)) { + backStack.add(Dashboard2(mSelectedLink = createDeeprObject(link = normalizedLink), sharedText, resetSharedText)) + } else { + Toast.makeText(context, "Invalid deeplink", Toast.LENGTH_SHORT).show() + } + } + } + + CompositionLocalProvider(LocalNavigator provides backStack) { - Column(modifier = modifier) { - NavDisplay( - backStack = backStack, - entryDecorators = + Scaffold( + bottomBar = { + AnimatedVisibility( + (TOP_LEVEL_ROUTES.any { it::class == current::class }), + enter = slideInVertically(initialOffsetY = { it }), + exit = slideOutVertically(targetOffsetY = { it }) + ) { + BottomAppBar(scrollBehavior = scrollBehavior) { + TOP_LEVEL_ROUTES.forEach { topLevelRoute -> + val isSelected = topLevelRoute::class == backStack.topLevelKey::class + NavigationBarItem( + selected = isSelected, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick) + when (topLevelRoute) { + is ScanQRVirtualScreen -> qrScanner.launch(ScanOptions()) + else -> backStack.addTopLevel(topLevelRoute) + } + }, + icon = { + Icon( + imageVector = topLevelRoute.icon, + contentDescription = null + ) + } + ) + } + } + } + } + ) { contentPadding -> + NavDisplay( + modifier = Modifier.padding(contentPadding), + backStack = backStack.backStack, + entryDecorators = listOf( // Add the default decorators for managing scenes and saving state rememberSceneSetupNavEntryDecorator(), @@ -142,51 +286,75 @@ fun Dashboard( // Then add the view model store decorator rememberViewModelStoreNavEntryDecorator(), ), - onBack = { backStack.removeLastOrNull() }, - entryProvider = { key -> - when (key) { - is Home -> - NavEntry(key) { - HomeScreen( - backStack, - sharedText = sharedText, - resetSharedText = resetSharedText, - ) - } - - is Settings -> - NavEntry(key) { - SettingsScreen(backStack) - } - - is AboutUs -> - NavEntry(key) { - AboutUsScreen(backStack) - } - - is LocalNetworkServer -> - NavEntry(key) { - LocalNetworkServerScreen(backStack) - } - - is TransferLinkLocalNetworkServer -> - NavEntry(key) { - TransferLinkLocalServerScreen(backStack) - } - - is BackupScreen -> - NavEntry(key) { - BackupScreenContent(backStack) - } - - is RestoreScreen -> - NavEntry(key) { - RestoreScreenContent(backStack) - } - - else -> NavEntry(Unit) { Text("Unknown route") } + onBack = { + backStack.removeLast() + }, + entryProvider = { + NavEntry(it) { + it.Content() + } } - }, - ) + ) + } } + +// +// Column(modifier = modifier) { +// NavDisplay( +// backStack = backStack, +// entryDecorators = +// listOf( +// // Add the default decorators for managing scenes and saving state +// rememberSceneSetupNavEntryDecorator(), +// rememberSavedStateNavEntryDecorator(), +// // Then add the view model store decorator +// rememberViewModelStoreNavEntryDecorator(), +// ), +// onBack = { backStack.removeLastOrNull() }, +// entryProvider = { key -> +// when (key) { +// is Home -> +// NavEntry(key) { +// HomeScreen( +// backStack, +// sharedText = sharedText, +// resetSharedText = resetSharedText, +// ) +// } +// +// is Settings -> +// NavEntry(key) { +// SettingsScreen(backStack) +// } +// +// is AboutUs -> +// NavEntry(key) { +// AboutUsScreen(backStack) +// } +// +// is LocalNetworkServer -> +// NavEntry(key) { +// LocalNetworkServerScreen(backStack) +// } +// +// is TransferLinkLocalNetworkServer -> +// NavEntry(key) { +// TransferLinkLocalServerScreen(backStack) +// } +// +// is BackupScreen -> +// NavEntry(key) { +// BackupScreenContent(backStack) +// } +// +// is RestoreScreen -> +// NavEntry(key) { +// RestoreScreenContent(backStack) +// } +// +// else -> NavEntry(Unit) { Text("Unknown route") } +// } +// }, +// ) +// } } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt index cd0b4cf6..f778d5a9 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt @@ -24,7 +24,6 @@ import androidx.compose.material3.Scaffold import androidx.compose.material3.Text import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip @@ -37,21 +36,29 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.yogeshpaliyal.deepr.BuildConfig +import com.yogeshpaliyal.deepr.LocalNavigator import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.Screen import compose.icons.TablerIcons import compose.icons.tablericons.ArrowLeft import compose.icons.tablericons.BrandGithub import compose.icons.tablericons.BrandLinkedin import compose.icons.tablericons.BrandTwitter -data object AboutUs +object AboutUs: Screen { + + @Composable + override fun Content() { + AboutUsScreen() + } +} @OptIn(ExperimentalMaterial3Api::class) @Composable fun AboutUsScreen( - backStack: SnapshotStateList, modifier: Modifier = Modifier, ) { + val backStack = LocalNavigator.current val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl Scaffold(modifier = modifier.fillMaxSize(), topBar = { @@ -62,7 +69,7 @@ fun AboutUsScreen( }, navigationIcon = { IconButton(onClick = { - backStack.removeLastOrNull() + backStack.removeLast() }) { Icon( TablerIcons.ArrowLeft, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt index d21e9c93..1675072c 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.material3.TopAppBar import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext @@ -33,7 +32,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.yogeshpaliyal.deepr.LocalNavigator import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.Screen import com.yogeshpaliyal.deepr.ui.components.ServerStatusBar import com.yogeshpaliyal.deepr.ui.components.SettingsItem import com.yogeshpaliyal.deepr.ui.components.SettingsSection @@ -50,15 +51,20 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -data object BackupScreen +object BackupScreen: Screen { + @Composable + override fun Content() { + BackupScreenContent() + } +} @OptIn(ExperimentalMaterial3Api::class) @Composable fun BackupScreenContent( - backStack: SnapshotStateList, modifier: Modifier = Modifier, viewModel: AccountViewModel = koinViewModel(), ) { + val backStack = LocalNavigator.current val context = LocalContext.current // Launcher for picking CSV export location @@ -137,7 +143,7 @@ fun BackupScreenContent( val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl IconButton(onClick = { - backStack.removeLastOrNull() + backStack.removeLast() }) { Icon( TablerIcons.ArrowLeft, @@ -154,7 +160,7 @@ fun BackupScreenContent( ) ServerStatusBar( onServerStatusClick = { - if (backStack.lastOrNull() !is LocalNetworkServer) { + if (backStack.getLast() !is LocalNetworkServer) { backStack.add(LocalNetworkServer) } }, 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 b25ceab0..ef3e5f84 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 @@ -39,7 +39,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale @@ -61,7 +60,9 @@ import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.lightspark.composeqr.QrCodeView +import com.yogeshpaliyal.deepr.LocalNavigator import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.Screen import com.yogeshpaliyal.deepr.server.LocalServerService import com.yogeshpaliyal.deepr.viewmodel.LocalServerViewModel import compose.icons.TablerIcons @@ -77,16 +78,22 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel -data object LocalNetworkServer +object LocalNetworkServer: Screen { + @Composable + override fun Content() { + LocalNetworkServerScreen() + } + +} @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable fun LocalNetworkServerScreen( - backStack: SnapshotStateList, modifier: Modifier = Modifier, viewModel: LocalServerViewModel = koinViewModel(), ) { val context = LocalContext.current + val navigatorContext = LocalNavigator.current val hapticFeedback = LocalHapticFeedback.current val isRunning by viewModel.isRunning.collectAsStateWithLifecycle() val serverUrl by viewModel.serverUrl.collectAsStateWithLifecycle() @@ -130,7 +137,7 @@ fun LocalNetworkServerScreen( navigationIcon = { IconButton(onClick = { hapticFeedback.performHapticFeedback(HapticFeedbackType.LongPress) - backStack.removeLastOrNull() + navigatorContext.removeLast() }) { Icon( TablerIcons.ArrowLeft, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt index a2132643..54e734a2 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt @@ -24,7 +24,6 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.platform.LocalContext @@ -32,7 +31,9 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp +import com.yogeshpaliyal.deepr.LocalNavigator import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.Screen import com.yogeshpaliyal.deepr.ui.components.ServerStatusBar import com.yogeshpaliyal.deepr.ui.components.SettingsItem import com.yogeshpaliyal.deepr.ui.components.SettingsSection @@ -43,16 +44,22 @@ import compose.icons.tablericons.Download import kotlinx.coroutines.flow.collectLatest import org.koin.androidx.compose.koinViewModel -data object RestoreScreen +object RestoreScreen: Screen { + @Composable + override fun Content() { + RestoreScreenContent() + } + +} @OptIn(ExperimentalMaterial3Api::class) @Composable fun RestoreScreenContent( - backStack: SnapshotStateList, modifier: Modifier = Modifier, viewModel: AccountViewModel = koinViewModel(), ) { val context = LocalContext.current + val backStack = LocalNavigator.current // Get available importers from the view model val availableImporters = remember { viewModel.getAvailableImporters() } @@ -96,7 +103,7 @@ fun RestoreScreenContent( val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl IconButton(onClick = { - backStack.removeLastOrNull() + backStack.removeLast() }) { Icon( TablerIcons.ArrowLeft, @@ -113,7 +120,7 @@ fun RestoreScreenContent( ) ServerStatusBar( onServerStatusClick = { - if (backStack.lastOrNull() !is LocalNetworkServer) { + if (backStack.getLast() !is LocalNetworkServer) { backStack.add(LocalNetworkServer) } }, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt new file mode 100644 index 00000000..fa296346 --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt @@ -0,0 +1,17 @@ +package com.yogeshpaliyal.deepr.ui.screens + +import androidx.compose.runtime.Composable +import androidx.compose.ui.graphics.vector.ImageVector +import com.yogeshpaliyal.deepr.TopLevelRoute +import compose.icons.TablerIcons +import compose.icons.tablericons.Qrcode + +class ScanQRVirtualScreen: TopLevelRoute { + @Composable + override fun Content() { + + } + + override val icon: ImageVector + get() = TablerIcons.Qrcode +} \ No newline at end of file diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt index b7fedbcb..9a1a4a8f 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt @@ -6,6 +6,7 @@ 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.WindowInsets import androidx.compose.foundation.layout.consumeWindowInsets import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth @@ -32,10 +33,10 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.graphicsLayer +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource @@ -47,8 +48,10 @@ import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.yogeshpaliyal.deepr.BuildConfig +import com.yogeshpaliyal.deepr.LocalNavigator import com.yogeshpaliyal.deepr.MainActivity import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.TopLevelRoute import com.yogeshpaliyal.deepr.ui.components.LanguageSelectionDialog import com.yogeshpaliyal.deepr.ui.components.ServerStatusBar import com.yogeshpaliyal.deepr.ui.components.ThemeSelectionDialog @@ -70,16 +73,25 @@ import compose.icons.tablericons.Star import compose.icons.tablericons.Upload import org.koin.androidx.compose.koinViewModel -data object Settings +object Settings: TopLevelRoute { + @Composable + override fun Content() { + SettingsScreen() + } + + override val icon: ImageVector + get() = TablerIcons.Settings + +} @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable fun SettingsScreen( - backStack: SnapshotStateList, modifier: Modifier = Modifier, viewModel: AccountViewModel = koinViewModel(), ) { val context = LocalContext.current + val navigatorContext = LocalNavigator.current // Collect the shortcut icon preference state val useLinkBasedIcons by viewModel.useLinkBasedIcons.collectAsStateWithLifecycle() @@ -97,10 +109,12 @@ fun SettingsScreen( val isThumbnailEnable by viewModel.isThumbnailEnable.collectAsStateWithLifecycle() Scaffold( + contentWindowInsets = WindowInsets(), modifier = modifier.fillMaxSize(), topBar = { Column { TopAppBar( + windowInsets = WindowInsets(), title = { Text(stringResource(R.string.settings)) }, @@ -108,7 +122,7 @@ fun SettingsScreen( val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl IconButton(onClick = { - backStack.removeLastOrNull() + navigatorContext.removeLast() }) { Icon( TablerIcons.ArrowLeft, @@ -126,8 +140,8 @@ fun SettingsScreen( ServerStatusBar( onServerStatusClick = { // Navigate to LocalNetworkServer screen when status bar is clicked - if (backStack.lastOrNull() !is LocalNetworkServer) { - backStack.add(LocalNetworkServer) + if (navigatorContext.getLast() !is LocalNetworkServer) { + navigatorContext.add(LocalNetworkServer) } }, ) @@ -150,7 +164,7 @@ fun SettingsScreen( title = stringResource(R.string.backup), description = "Export to CSV, Local file sync, Auto backup", onClick = { - backStack.add(BackupScreen) + navigatorContext.add(BackupScreen) }, ) @@ -159,7 +173,7 @@ fun SettingsScreen( title = stringResource(R.string.restore), description = "Import from CSV, Bookmarks, and other formats", onClick = { - backStack.add(RestoreScreen) + navigatorContext.add(RestoreScreen) }, ) } @@ -169,7 +183,7 @@ fun SettingsScreen( TablerIcons.Server, title = stringResource(R.string.local_network_server), onClick = { - backStack.add(LocalNetworkServer) + navigatorContext.add(LocalNetworkServer) }, ) @@ -332,7 +346,7 @@ fun SettingsScreen( TablerIcons.InfoCircle, title = stringResource(R.string.about_us), onClick = { - backStack.add(AboutUs) + navigatorContext.add(AboutUs) }, ) } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt new file mode 100644 index 00000000..e42531ce --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt @@ -0,0 +1,48 @@ +package com.yogeshpaliyal.deepr.ui.screens + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.ContainedLoadingIndicator +import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import com.yogeshpaliyal.deepr.LocalNavigator +import com.yogeshpaliyal.deepr.Screen +import com.yogeshpaliyal.deepr.ui.screens.home.Dashboard2 +import kotlinx.serialization.Serializable + +@Serializable +@OptIn(ExperimentalMaterial3ExpressiveApi::class) +object Splash : Screen { + @Composable + override fun Content() { + val navigator = LocalNavigator.current + val context = LocalContext.current + + LaunchedEffect(Unit) { +// // Check if user is opening the app for the first time +// val isFirstTime = context.isFirstTimeUser() +// +// // Navigate to appropriate screen +// if (isFirstTime) { +// navigator.add(IntroScreen) +// } else { + navigator.clearStackAndAdd(Dashboard2(){}) +// } + } + + // Loading screen while checking user status + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + ContainedLoadingIndicator() + } + } + +} diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt index 16185b7b..867cd51a 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt @@ -39,7 +39,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.scale @@ -57,7 +56,9 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState import com.journeyapps.barcodescanner.ScanOptions import com.lightspark.composeqr.QrCodeView +import com.yogeshpaliyal.deepr.LocalNavigator import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.Screen import com.yogeshpaliyal.deepr.server.LocalServerService import com.yogeshpaliyal.deepr.server.LocalServerTransferLink import com.yogeshpaliyal.deepr.util.QRScanner @@ -72,16 +73,22 @@ import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject -data object TransferLinkLocalNetworkServer +object TransferLinkLocalNetworkServer: Screen { + @Composable + override fun Content() { + TransferLinkLocalServerScreen() + } + +} @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) @Composable fun TransferLinkLocalServerScreen( - backStack: SnapshotStateList, modifier: Modifier = Modifier, viewModel: TransferLinkLocalServerViewModel = koinViewModel(), ) { val context = LocalContext.current + val backStack = LocalNavigator.current val localServerInstance = koinInject() val isRunning by localServerInstance.isRunning.collectAsStateWithLifecycle() val serverUrl by localServerInstance.serverUrl.collectAsStateWithLifecycle() @@ -136,7 +143,7 @@ fun TransferLinkLocalServerScreen( }, navigationIcon = { IconButton(onClick = { - backStack.removeLastOrNull() + backStack.removeLast() }) { Icon( TablerIcons.ArrowLeft, 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 60b93c41..9584fbd4 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 @@ -19,11 +19,11 @@ 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.WindowInsets 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.navigationBarsPadding import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn @@ -72,7 +72,6 @@ import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -92,9 +91,11 @@ import coil3.compose.AsyncImage import com.journeyapps.barcodescanner.ScanOptions import com.yogeshpaliyal.deepr.DeeprQueries import com.yogeshpaliyal.deepr.GetLinksAndTags +import com.yogeshpaliyal.deepr.LocalNavigator import com.yogeshpaliyal.deepr.R import com.yogeshpaliyal.deepr.SharedLink import com.yogeshpaliyal.deepr.Tags +import com.yogeshpaliyal.deepr.TopLevelRoute import com.yogeshpaliyal.deepr.analytics.AnalyticsEvents import com.yogeshpaliyal.deepr.analytics.AnalyticsManager import com.yogeshpaliyal.deepr.analytics.AnalyticsParams @@ -104,7 +105,6 @@ import com.yogeshpaliyal.deepr.ui.components.DeleteConfirmationDialog 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 @@ -122,13 +122,13 @@ import com.yogeshpaliyal.deepr.viewmodel.AccountViewModel import compose.icons.TablerIcons import compose.icons.tablericons.ArrowLeft import compose.icons.tablericons.Edit +import compose.icons.tablericons.Home 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 @@ -144,6 +144,31 @@ import org.koin.compose.koinInject data object Home + +@OptIn( + ExperimentalMaterial3Api::class, + ExperimentalHazeMaterialsApi::class, + ExperimentalMaterial3ExpressiveApi::class, +) +class Dashboard2( + val mSelectedLink: GetLinksAndTags? = null, + val sharedText: SharedLink? = null, + val resetSharedText: () -> Unit, +) : TopLevelRoute { + override val icon: ImageVector + get() = TablerIcons.Home + + @Composable + override fun Content() { + HomeScreen( + mSelectedLink = mSelectedLink, + sharedText = sharedText, + resetSharedText = resetSharedText + ) + } + +} + @OptIn( ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, @@ -151,18 +176,19 @@ data object Home ) @Composable fun HomeScreen( - backStack: SnapshotStateList, modifier: Modifier = Modifier, viewModel: AccountViewModel = koinViewModel(), deeprQueries: DeeprQueries = koinInject(), analyticsManager: AnalyticsManager = koinInject(), + mSelectedLink: GetLinksAndTags? = null, sharedText: SharedLink? = null, resetSharedText: () -> Unit, ) { var isTagsSelectionActive by remember { mutableStateOf(false) } val currentViewType by viewModel.viewType.collectAsStateWithLifecycle() + val localNavigator = LocalNavigator.current - var selectedLink by remember { mutableStateOf(null) } + var selectedLink by remember { mutableStateOf(mSelectedLink) } val selectedTag by viewModel.selectedTagFilter.collectAsStateWithLifecycle() val hazeState = rememberHazeState(blurEnabled = true) val context = LocalContext.current @@ -175,21 +201,6 @@ fun HomeScreen( val allTagsWithCount by viewModel.allTagsWithCount.collectAsStateWithLifecycle() val favouriteFilter by viewModel.favouriteFilter.collectAsStateWithLifecycle() - val qrScanner = - rememberLauncherForActivityResult( - QRScanner(), - ) { result -> - if (result.contents == null) { - Toast.makeText(context, "No Data found", Toast.LENGTH_SHORT).show() - } else { - val normalizedLink = normalizeLink(result.contents) - if (isValidDeeplink(normalizedLink)) { - selectedLink = createDeeprObject(link = normalizedLink) - } else { - Toast.makeText(context, "Invalid deeplink", Toast.LENGTH_SHORT).show() - } - } - } // Handle shared text from other apps LaunchedEffect(sharedText) { @@ -300,6 +311,7 @@ fun HomeScreen( } Scaffold( + contentWindowInsets = WindowInsets(), modifier = modifier.fillMaxSize(), topBar = { Column( @@ -308,9 +320,11 @@ fun HomeScreen( .hazeEffect( state = hazeState, style = HazeMaterials.ultraThin(), - ).fillMaxWidth(), + ) + .fillMaxWidth(), ) { AppBarWithSearch( + windowInsets = WindowInsets(), scrollBehavior = scrollBehavior, state = searchBarState, inputField = inputField, @@ -323,8 +337,8 @@ fun HomeScreen( onServerStatusClick = { // Navigate to LocalNetworkServer screen when status bar is clicked analyticsManager.logEvent(AnalyticsEvents.NAVIGATE_LOCAL_SERVER) - if (backStack.lastOrNull() !is LocalNetworkServer) { - backStack.add(LocalNetworkServer) + if (localNavigator.getLast() !is LocalNetworkServer) { + localNavigator.add(LocalNetworkServer) } }, ) @@ -363,8 +377,7 @@ fun HomeScreen( Box( modifier = Modifier - .fillMaxWidth() - .navigationBarsPadding(), + .fillMaxWidth(), contentAlignment = Alignment.Center, ) { HorizontalFloatingToolbar( @@ -372,15 +385,6 @@ fun HomeScreen( scrollBehavior = FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = FloatingToolbarExitDirection.Bottom), colors = FloatingToolbarDefaults.standardFloatingToolbarColors(), content = { - IconButton(onClick = { - analyticsManager.logEvent(AnalyticsEvents.SCAN_QR_CODE) - qrScanner.launch(ScanOptions()) - }) { - Icon( - TablerIcons.Qrcode, - contentDescription = stringResource(R.string.qr_scanner), - ) - } IconButton(onClick = { isTagsSelectionActive = true }) { @@ -389,16 +393,7 @@ fun HomeScreen( contentDescription = stringResource(R.string.tags), ) } - IconButton(onClick = { - // Settings action - analyticsManager.logEvent(AnalyticsEvents.NAVIGATE_SETTINGS) - backStack.add(Settings) - }) { - Icon( - TablerIcons.Settings, - contentDescription = stringResource(R.string.settings), - ) - } + }, floatingActionButton = { FloatingToolbarDefaults.VibrantFloatingActionButton(onClick = { @@ -549,7 +544,7 @@ fun Content( showDeleteConfirmDialog = it.item } - is MenuItem.Edit -> { + is Edit -> { analyticsManager.logEvent(AnalyticsEvents.ITEM_MENU_EDIT) editDeepr(it.item) } @@ -598,7 +593,7 @@ fun Content( Modifier .weight(1f) .hazeSource(state = hazeState) - .padding(8.dp), + .padding(horizontal = 8.dp), contentPaddingValues = contentPaddingValues, accounts = accounts!!, selectedTag = selectedTag, From d04c3b2b6b05ecf15250d825c580ffecfe945572 Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Wed, 12 Nov 2025 21:17:43 +0530 Subject: [PATCH 2/6] feat: WIP Refactor navigation handling to use LocalNavigator and update screen structure --- .../com/yogeshpaliyal/deepr/MainActivity.kt | 42 +++++++++---------- .../yogeshpaliyal/deepr/ui/screens/AboutUs.kt | 7 +--- .../deepr/ui/screens/BackupScreen.kt | 2 +- .../deepr/ui/screens/LocalNetworkServer.kt | 3 +- .../deepr/ui/screens/RestoreScreen.kt | 3 +- .../deepr/ui/screens/ScanQRVirtualScreen.kt | 5 +-- .../deepr/ui/screens/Settings.kt | 3 +- .../yogeshpaliyal/deepr/ui/screens/Splash.kt | 3 +- .../ui/screens/TransferLinkLocalServer.kt | 3 +- .../deepr/ui/screens/home/Home.kt | 12 +----- 10 files changed, 32 insertions(+), 51 deletions(-) diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt b/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt index bcac2dc0..5354bac1 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt @@ -96,7 +96,7 @@ class MainActivity : ComponentActivity() { setContent { val preferenceDataStore = remember { AppPreferenceDataStore(this) } val themeMode by preferenceDataStore.getThemeMode.collectAsStateWithLifecycle( - initialValue = "system" + initialValue = "system", ) DeeprTheme(themeMode = themeMode) { @@ -151,15 +151,16 @@ interface TopLevelRoute : Screen { val icon: ImageVector } - private val TOP_LEVEL_ROUTES: List = listOf(Dashboard2 {}, ScanQRVirtualScreen(), Settings) -class TopLevelBackStack(startKey: T) { - +class TopLevelBackStack( + startKey: T, +) { // Maintain a stack for each top level route - private var topLevelStacks: LinkedHashMap> = linkedMapOf( - startKey to mutableStateListOf(startKey) - ) + private var topLevelStacks: LinkedHashMap> = + linkedMapOf( + startKey to mutableStateListOf(startKey), + ) // Expose the current top level route for consumers var topLevelKey by mutableStateOf(startKey) @@ -174,14 +175,12 @@ class TopLevelBackStack(startKey: T) { addAll(topLevelStacks.flatMap { it.value }) } - fun clearStackAndAdd(key: T) { topLevelStacks.clear() addTopLevel(key) } fun addTopLevel(key: T) { - // If the top level doesn't exist, add it if (topLevelStacks[key] == null) { topLevelStacks.put(key, mutableStateListOf(key)) @@ -243,13 +242,12 @@ fun Dashboard( } CompositionLocalProvider(LocalNavigator provides backStack) { - Scaffold( bottomBar = { AnimatedVisibility( (TOP_LEVEL_ROUTES.any { it::class == current::class }), enter = slideInVertically(initialOffsetY = { it }), - exit = slideOutVertically(targetOffsetY = { it }) + exit = slideOutVertically(targetOffsetY = { it }), ) { BottomAppBar(scrollBehavior = scrollBehavior) { TOP_LEVEL_ROUTES.forEach { topLevelRoute -> @@ -266,26 +264,26 @@ fun Dashboard( icon = { Icon( imageVector = topLevelRoute.icon, - contentDescription = null + contentDescription = null, ) - } + }, ) } } } - } + }, ) { contentPadding -> NavDisplay( modifier = Modifier.padding(contentPadding), backStack = backStack.backStack, entryDecorators = - listOf( - // Add the default decorators for managing scenes and saving state - rememberSceneSetupNavEntryDecorator(), - rememberSavedStateNavEntryDecorator(), - // Then add the view model store decorator - rememberViewModelStoreNavEntryDecorator(), - ), + listOf( + // Add the default decorators for managing scenes and saving state + rememberSceneSetupNavEntryDecorator(), + rememberSavedStateNavEntryDecorator(), + // Then add the view model store decorator + rememberViewModelStoreNavEntryDecorator(), + ), onBack = { backStack.removeLast() }, @@ -293,7 +291,7 @@ fun Dashboard( NavEntry(it) { it.Content() } - } + }, ) } } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt index f778d5a9..cfbe8ff4 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt @@ -45,8 +45,7 @@ import compose.icons.tablericons.BrandGithub import compose.icons.tablericons.BrandLinkedin import compose.icons.tablericons.BrandTwitter -object AboutUs: Screen { - +object AboutUs : Screen { @Composable override fun Content() { AboutUsScreen() @@ -55,9 +54,7 @@ object AboutUs: Screen { @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AboutUsScreen( - modifier: Modifier = Modifier, -) { +fun AboutUsScreen(modifier: Modifier = Modifier) { val backStack = LocalNavigator.current val isRtl = LocalLayoutDirection.current == LayoutDirection.Rtl diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt index 1675072c..c4b079b9 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt @@ -51,7 +51,7 @@ import java.text.SimpleDateFormat import java.util.Date import java.util.Locale -object BackupScreen: Screen { +object BackupScreen : Screen { @Composable override fun Content() { BackupScreenContent() 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 ef3e5f84..637fee61 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 @@ -78,12 +78,11 @@ import kotlinx.coroutines.delay import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel -object LocalNetworkServer: Screen { +object LocalNetworkServer : Screen { @Composable override fun Content() { LocalNetworkServerScreen() } - } @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt index 54e734a2..d0fae136 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt @@ -44,12 +44,11 @@ import compose.icons.tablericons.Download import kotlinx.coroutines.flow.collectLatest import org.koin.androidx.compose.koinViewModel -object RestoreScreen: Screen { +object RestoreScreen : Screen { @Composable override fun Content() { RestoreScreenContent() } - } @OptIn(ExperimentalMaterial3Api::class) diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt index fa296346..1e0b002c 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt @@ -6,12 +6,11 @@ import com.yogeshpaliyal.deepr.TopLevelRoute import compose.icons.TablerIcons import compose.icons.tablericons.Qrcode -class ScanQRVirtualScreen: TopLevelRoute { +class ScanQRVirtualScreen : TopLevelRoute { @Composable override fun Content() { - } override val icon: ImageVector get() = TablerIcons.Qrcode -} \ No newline at end of file +} diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt index 9a1a4a8f..1cf9cca8 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt @@ -73,7 +73,7 @@ import compose.icons.tablericons.Star import compose.icons.tablericons.Upload import org.koin.androidx.compose.koinViewModel -object Settings: TopLevelRoute { +object Settings : TopLevelRoute { @Composable override fun Content() { SettingsScreen() @@ -81,7 +81,6 @@ object Settings: TopLevelRoute { override val icon: ImageVector get() = TablerIcons.Settings - } @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt index e42531ce..ec3caf12 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt @@ -31,7 +31,7 @@ object Splash : Screen { // if (isFirstTime) { // navigator.add(IntroScreen) // } else { - navigator.clearStackAndAdd(Dashboard2(){}) + navigator.clearStackAndAdd(Dashboard2 {}) // } } @@ -44,5 +44,4 @@ object Splash : Screen { ContainedLoadingIndicator() } } - } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt index 867cd51a..ae554717 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt @@ -73,12 +73,11 @@ import kotlinx.coroutines.launch import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject -object TransferLinkLocalNetworkServer: Screen { +object TransferLinkLocalNetworkServer : Screen { @Composable override fun Content() { TransferLinkLocalServerScreen() } - } @OptIn(ExperimentalPermissionsApi::class, ExperimentalMaterial3Api::class) 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 9584fbd4..db2fbcfd 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 @@ -4,7 +4,6 @@ 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 import androidx.compose.animation.expandVertically import androidx.compose.animation.scaleIn @@ -88,7 +87,6 @@ import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage -import com.journeyapps.barcodescanner.ScanOptions import com.yogeshpaliyal.deepr.DeeprQueries import com.yogeshpaliyal.deepr.GetLinksAndTags import com.yogeshpaliyal.deepr.LocalNavigator @@ -114,7 +112,6 @@ 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 import com.yogeshpaliyal.deepr.util.openDeeplink @@ -144,7 +141,6 @@ import org.koin.compose.koinInject data object Home - @OptIn( ExperimentalMaterial3Api::class, ExperimentalHazeMaterialsApi::class, @@ -163,10 +159,9 @@ class Dashboard2( HomeScreen( mSelectedLink = mSelectedLink, sharedText = sharedText, - resetSharedText = resetSharedText + resetSharedText = resetSharedText, ) } - } @OptIn( @@ -201,7 +196,6 @@ fun HomeScreen( val allTagsWithCount by viewModel.allTagsWithCount.collectAsStateWithLifecycle() val favouriteFilter by viewModel.favouriteFilter.collectAsStateWithLifecycle() - // Handle shared text from other apps LaunchedEffect(sharedText) { if (!sharedText?.url.isNullOrBlank()) { @@ -320,8 +314,7 @@ fun HomeScreen( .hazeEffect( state = hazeState, style = HazeMaterials.ultraThin(), - ) - .fillMaxWidth(), + ).fillMaxWidth(), ) { AppBarWithSearch( windowInsets = WindowInsets(), @@ -393,7 +386,6 @@ fun HomeScreen( contentDescription = stringResource(R.string.tags), ) } - }, floatingActionButton = { FloatingToolbarDefaults.VibrantFloatingActionButton(onClick = { From 3db21240e0133b7322fea1f3c32af11e77ac3243 Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Wed, 12 Nov 2025 22:25:13 +0530 Subject: [PATCH 3/6] feat: Revamp bottom bar navigation and update screen structure --- .../yogeshpaliyal/deepr/DeeprApplication.kt | 3 +- .../com/yogeshpaliyal/deepr/MainActivity.kt | 193 ++--------- .../com/yogeshpaliyal/deepr/ui/Navigation.kt | 87 +++++ .../yogeshpaliyal/deepr/ui/screens/AboutUs.kt | 4 +- .../deepr/ui/screens/BackupScreen.kt | 4 +- .../deepr/ui/screens/LocalNetworkServer.kt | 4 +- .../deepr/ui/screens/RestoreScreen.kt | 4 +- .../deepr/ui/screens/ScanQRVirtualScreen.kt | 55 ++- .../deepr/ui/screens/Settings.kt | 8 +- .../yogeshpaliyal/deepr/ui/screens/Splash.kt | 4 +- .../ui/screens/TransferLinkLocalServer.kt | 4 +- .../deepr/ui/screens/home/Home.kt | 83 +---- .../screens/home/TagSelectionBottomSheet.kt | 327 +++++++++--------- app/src/main/res/values/strings.xml | 1 + 14 files changed, 360 insertions(+), 421 deletions(-) create mode 100644 app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt b/app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt index 171a227d..40fd01da 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt @@ -33,6 +33,7 @@ import kotlinx.serialization.json.Json import org.koin.android.ext.koin.androidContext import org.koin.core.context.startKoin import org.koin.core.module.dsl.viewModel +import org.koin.core.module.dsl.viewModelOf import org.koin.dsl.module class DeeprApplication : Application() { @@ -90,7 +91,7 @@ class DeeprApplication : Application() { } } - viewModel { AccountViewModel(get(), get(), get(), get(), get(), get(), get()) } + viewModelOf(::AccountViewModel) single { HtmlParser() diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt b/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt index 5354bac1..d6c93875 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt @@ -3,9 +3,7 @@ package com.yogeshpaliyal.deepr import android.content.Context import android.content.Intent import android.os.Bundle -import android.widget.Toast import androidx.activity.ComponentActivity -import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedVisibility @@ -19,42 +17,34 @@ import androidx.compose.material3.Icon import androidx.compose.material3.NavigationBarItem import androidx.compose.material3.Scaffold import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.snapshots.SnapshotStateList import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.hapticfeedback.HapticFeedbackType -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator import androidx.navigation3.runtime.NavEntry -import androidx.navigation3.runtime.NavKey import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator -import com.journeyapps.barcodescanner.ScanOptions import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore -import com.yogeshpaliyal.deepr.ui.screens.ScanQRVirtualScreen +import com.yogeshpaliyal.deepr.ui.LocalNavigator +import com.yogeshpaliyal.deepr.ui.Screen +import com.yogeshpaliyal.deepr.ui.TopLevelBackStack +import com.yogeshpaliyal.deepr.ui.TopLevelRoute import com.yogeshpaliyal.deepr.ui.screens.Settings import com.yogeshpaliyal.deepr.ui.screens.home.Dashboard2 -import com.yogeshpaliyal.deepr.ui.screens.home.createDeeprObject +import com.yogeshpaliyal.deepr.ui.screens.home.TagSelectionScreen import com.yogeshpaliyal.deepr.ui.theme.DeeprTheme import com.yogeshpaliyal.deepr.util.LanguageUtil -import com.yogeshpaliyal.deepr.util.QRScanner -import com.yogeshpaliyal.deepr.util.isValidDeeplink -import com.yogeshpaliyal.deepr.util.normalizeLink import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.update -import kotlinx.coroutines.launch import kotlinx.coroutines.runBlocking data class SharedLink( @@ -139,78 +129,10 @@ class MainActivity : ComponentActivity() { } } -val LocalNavigator = - compositionLocalOf> { TopLevelBackStack(Dashboard2 {}) } -interface Screen : NavKey { - @Composable - fun Content() -} - -interface TopLevelRoute : Screen { - val icon: ImageVector -} - -private val TOP_LEVEL_ROUTES: List = listOf(Dashboard2 {}, ScanQRVirtualScreen(), Settings) - -class TopLevelBackStack( - startKey: T, -) { - // Maintain a stack for each top level route - private var topLevelStacks: LinkedHashMap> = - linkedMapOf( - startKey to mutableStateListOf(startKey), - ) - - // Expose the current top level route for consumers - var topLevelKey by mutableStateOf(startKey) - private set - - // Expose the back stack so it can be rendered by the NavDisplay - val backStack = mutableStateListOf(startKey) +private val TOP_LEVEL_ROUTES: List = + listOf(Dashboard2 {}, TagSelectionScreen, Settings) - private fun updateBackStack() = - backStack.apply { - clear() - addAll(topLevelStacks.flatMap { it.value }) - } - - fun clearStackAndAdd(key: T) { - topLevelStacks.clear() - addTopLevel(key) - } - - fun addTopLevel(key: T) { - // If the top level doesn't exist, add it - if (topLevelStacks[key] == null) { - topLevelStacks.put(key, mutableStateListOf(key)) - } else { - // Otherwise just move it to the end of the stacks - topLevelStacks.apply { - remove(key)?.let { - put(key, it) - } - } - } - topLevelKey = key - updateBackStack() - } - - fun add(key: T) { - topLevelStacks[topLevelKey]?.add(key) - updateBackStack() - } - - fun getLast() = backStack.last() - - fun removeLast() { - val removedKey = topLevelStacks[topLevelKey]?.removeLastOrNull() - // If the removed key was a top level key, remove the associated top level stack - topLevelStacks.remove(removedKey) - topLevelKey = topLevelStacks.keys.last() - updateBackStack() - } -} @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -219,30 +141,21 @@ fun Dashboard( sharedText: SharedLink? = null, resetSharedText: () -> Unit, ) { - val backStack = remember { TopLevelBackStack(Dashboard2(sharedText = sharedText, resetSharedText = resetSharedText)) } + val backStack = remember { + TopLevelBackStack( + Dashboard2( + sharedText = sharedText, + resetSharedText = resetSharedText + ) + ) + } val current = backStack.getLast() val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior() val hapticFeedback = LocalHapticFeedback.current - val context = LocalContext.current - - val qrScanner = - rememberLauncherForActivityResult( - QRScanner(), - ) { result -> - if (result.contents == null) { - Toast.makeText(context, "No Data found", Toast.LENGTH_SHORT).show() - } else { - val normalizedLink = normalizeLink(result.contents) - if (isValidDeeplink(normalizedLink)) { - backStack.add(Dashboard2(mSelectedLink = createDeeprObject(link = normalizedLink), sharedText, resetSharedText)) - } else { - Toast.makeText(context, "Invalid deeplink", Toast.LENGTH_SHORT).show() - } - } - } CompositionLocalProvider(LocalNavigator provides backStack) { Scaffold( + modifier = modifier, bottomBar = { AnimatedVisibility( (TOP_LEVEL_ROUTES.any { it::class == current::class }), @@ -256,10 +169,10 @@ fun Dashboard( selected = isSelected, onClick = { hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick) - when (topLevelRoute) { - is ScanQRVirtualScreen -> qrScanner.launch(ScanOptions()) - else -> backStack.addTopLevel(topLevelRoute) - } + backStack.addTopLevel(topLevelRoute) + }, + label = { + Text(stringResource(topLevelRoute.label)) }, icon = { Icon( @@ -295,64 +208,4 @@ fun Dashboard( ) } } - -// -// Column(modifier = modifier) { -// NavDisplay( -// backStack = backStack, -// entryDecorators = -// listOf( -// // Add the default decorators for managing scenes and saving state -// rememberSceneSetupNavEntryDecorator(), -// rememberSavedStateNavEntryDecorator(), -// // Then add the view model store decorator -// rememberViewModelStoreNavEntryDecorator(), -// ), -// onBack = { backStack.removeLastOrNull() }, -// entryProvider = { key -> -// when (key) { -// is Home -> -// NavEntry(key) { -// HomeScreen( -// backStack, -// sharedText = sharedText, -// resetSharedText = resetSharedText, -// ) -// } -// -// is Settings -> -// NavEntry(key) { -// SettingsScreen(backStack) -// } -// -// is AboutUs -> -// NavEntry(key) { -// AboutUsScreen(backStack) -// } -// -// is LocalNetworkServer -> -// NavEntry(key) { -// LocalNetworkServerScreen(backStack) -// } -// -// is TransferLinkLocalNetworkServer -> -// NavEntry(key) { -// TransferLinkLocalServerScreen(backStack) -// } -// -// is BackupScreen -> -// NavEntry(key) { -// BackupScreenContent(backStack) -// } -// -// is RestoreScreen -> -// NavEntry(key) { -// RestoreScreenContent(backStack) -// } -// -// else -> NavEntry(Unit) { Text("Unknown route") } -// } -// }, -// ) -// } -} +} \ No newline at end of file diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt new file mode 100644 index 00000000..cfbd576b --- /dev/null +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt @@ -0,0 +1,87 @@ +package com.yogeshpaliyal.deepr.ui + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.setValue +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.ui.graphics.vector.ImageVector +import androidx.navigation3.runtime.NavKey +import com.yogeshpaliyal.deepr.ui.screens.home.Dashboard2 +import kotlin.collections.remove + + +val LocalNavigator = + compositionLocalOf> { TopLevelBackStack(Dashboard2 {}) } + +interface Screen : NavKey { + @Composable + fun Content() +} + +interface TopLevelRoute : Screen { + val icon: ImageVector + + val label: Int +} + +class TopLevelBackStack( + startKey: T, +) { + // Maintain a stack for each top level route + private var topLevelStacks: LinkedHashMap> = + linkedMapOf( + startKey to mutableStateListOf(startKey), + ) + + // Expose the current top level route for consumers + var topLevelKey by mutableStateOf(startKey) + private set + + // Expose the back stack so it can be rendered by the NavDisplay + val backStack = mutableStateListOf(startKey) + + private fun updateBackStack() = + backStack.apply { + clear() + addAll(topLevelStacks.flatMap { it.value }) + } + + fun clearStackAndAdd(key: T) { + topLevelStacks.clear() + addTopLevel(key) + } + + fun addTopLevel(key: T) { + // If the top level doesn't exist, add it + if (topLevelStacks[key] == null) { + topLevelStacks.put(key, mutableStateListOf(key)) + } else { + // Otherwise just move it to the end of the stacks + topLevelStacks.apply { + remove(key)?.let { + put(key, it) + } + } + } + topLevelKey = key + updateBackStack() + } + + fun add(key: T) { + topLevelStacks[topLevelKey]?.add(key) + updateBackStack() + } + + fun getLast() = backStack.last() + + fun removeLast() { + val removedKey = topLevelStacks[topLevelKey]?.removeLastOrNull() + // If the removed key was a top level key, remove the associated top level stack + topLevelStacks.remove(removedKey) + topLevelKey = topLevelStacks.keys.last() + updateBackStack() + } +} diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt index cfbe8ff4..0bab9df8 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt @@ -36,9 +36,9 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.yogeshpaliyal.deepr.BuildConfig -import com.yogeshpaliyal.deepr.LocalNavigator +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R -import com.yogeshpaliyal.deepr.Screen +import com.yogeshpaliyal.deepr.ui.Screen import compose.icons.TablerIcons import compose.icons.tablericons.ArrowLeft import compose.icons.tablericons.BrandGithub diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt index c4b079b9..18e989fa 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt @@ -32,9 +32,9 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.yogeshpaliyal.deepr.LocalNavigator +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R -import com.yogeshpaliyal.deepr.Screen +import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.ui.components.ServerStatusBar import com.yogeshpaliyal.deepr.ui.components.SettingsItem import com.yogeshpaliyal.deepr.ui.components.SettingsSection 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 637fee61..1648fa92 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 @@ -60,9 +60,9 @@ import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.lightspark.composeqr.QrCodeView -import com.yogeshpaliyal.deepr.LocalNavigator +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R -import com.yogeshpaliyal.deepr.Screen +import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.server.LocalServerService import com.yogeshpaliyal.deepr.viewmodel.LocalServerViewModel import compose.icons.TablerIcons diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt index d0fae136..787fd918 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt @@ -31,9 +31,9 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import com.yogeshpaliyal.deepr.LocalNavigator +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R -import com.yogeshpaliyal.deepr.Screen +import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.ui.components.ServerStatusBar import com.yogeshpaliyal.deepr.ui.components.SettingsItem import com.yogeshpaliyal.deepr.ui.components.SettingsSection diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt index 1e0b002c..3e4dc6fc 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt @@ -1,16 +1,55 @@ package com.yogeshpaliyal.deepr.ui.screens +import android.widget.Toast +import androidx.activity.compose.rememberLauncherForActivityResult import androidx.compose.runtime.Composable -import androidx.compose.ui.graphics.vector.ImageVector -import com.yogeshpaliyal.deepr.TopLevelRoute +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.res.stringResource +import com.journeyapps.barcodescanner.ScanOptions +import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.ui.LocalNavigator +import com.yogeshpaliyal.deepr.ui.components.SettingsItem +import com.yogeshpaliyal.deepr.ui.screens.home.Dashboard2 +import com.yogeshpaliyal.deepr.ui.screens.home.createDeeprObject +import com.yogeshpaliyal.deepr.util.QRScanner +import com.yogeshpaliyal.deepr.util.isValidDeeplink +import com.yogeshpaliyal.deepr.util.normalizeLink import compose.icons.TablerIcons import compose.icons.tablericons.Qrcode -class ScanQRVirtualScreen : TopLevelRoute { - @Composable - override fun Content() { - } - override val icon: ImageVector - get() = TablerIcons.Qrcode +@Composable +fun ScanQRCode() { + val navigatorContext = LocalNavigator.current + val context = LocalContext.current + + val qrScanner = + rememberLauncherForActivityResult( + QRScanner(), + ) { result -> + if (result.contents == null) { + Toast.makeText(context, "No Data found", Toast.LENGTH_SHORT).show() + } else { + val normalizedLink = normalizeLink(result.contents) + if (isValidDeeplink(normalizedLink)) { + navigatorContext.clearStackAndAdd( + Dashboard2( + mSelectedLink = createDeeprObject( + link = normalizedLink + ) + ) {}) + } else { + Toast.makeText(context, "Invalid deeplink", Toast.LENGTH_SHORT).show() + } + } + } + + SettingsItem( + TablerIcons.Qrcode, + title = stringResource(R.string.scan_qr_code), + onClick = { + qrScanner.launch(ScanOptions()) + }, + ) + } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt index 1cf9cca8..39a5bea4 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt @@ -48,10 +48,10 @@ import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.yogeshpaliyal.deepr.BuildConfig -import com.yogeshpaliyal.deepr.LocalNavigator +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.MainActivity import com.yogeshpaliyal.deepr.R -import com.yogeshpaliyal.deepr.TopLevelRoute +import com.yogeshpaliyal.deepr.ui.TopLevelRoute import com.yogeshpaliyal.deepr.ui.components.LanguageSelectionDialog import com.yogeshpaliyal.deepr.ui.components.ServerStatusBar import com.yogeshpaliyal.deepr.ui.components.ThemeSelectionDialog @@ -81,6 +81,8 @@ object Settings : TopLevelRoute { override val icon: ImageVector get() = TablerIcons.Settings + override val label: Int + get() = R.string.settings } @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @@ -186,6 +188,8 @@ fun SettingsScreen( }, ) + ScanQRCode() + SettingsItem( TablerIcons.Settings, title = stringResource(R.string.shortcut_icon), diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt index ec3caf12..809efdbe 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt @@ -10,8 +10,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext -import com.yogeshpaliyal.deepr.LocalNavigator -import com.yogeshpaliyal.deepr.Screen +import com.yogeshpaliyal.deepr.ui.LocalNavigator +import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.ui.screens.home.Dashboard2 import kotlinx.serialization.Serializable diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt index ae554717..fd277b7e 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt @@ -56,9 +56,9 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState import com.journeyapps.barcodescanner.ScanOptions import com.lightspark.composeqr.QrCodeView -import com.yogeshpaliyal.deepr.LocalNavigator +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R -import com.yogeshpaliyal.deepr.Screen +import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.server.LocalServerService import com.yogeshpaliyal.deepr.server.LocalServerTransferLink import com.yogeshpaliyal.deepr.util.QRScanner 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 db2fbcfd..3c8e0c28 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 @@ -39,6 +39,7 @@ import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi import androidx.compose.material3.FilterChip +import androidx.compose.material3.FloatingActionButton import androidx.compose.material3.FloatingToolbarDefaults import androidx.compose.material3.FloatingToolbarExitDirection import androidx.compose.material3.HorizontalFloatingToolbar @@ -89,11 +90,11 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.yogeshpaliyal.deepr.DeeprQueries import com.yogeshpaliyal.deepr.GetLinksAndTags -import com.yogeshpaliyal.deepr.LocalNavigator +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R import com.yogeshpaliyal.deepr.SharedLink import com.yogeshpaliyal.deepr.Tags -import com.yogeshpaliyal.deepr.TopLevelRoute +import com.yogeshpaliyal.deepr.ui.TopLevelRoute import com.yogeshpaliyal.deepr.analytics.AnalyticsEvents import com.yogeshpaliyal.deepr.analytics.AnalyticsManager import com.yogeshpaliyal.deepr.analytics.AnalyticsParams @@ -135,9 +136,8 @@ import dev.chrisbanes.haze.materials.ExperimentalHazeMaterialsApi import dev.chrisbanes.haze.materials.HazeMaterials import dev.chrisbanes.haze.rememberHazeState import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking -import org.koin.androidx.compose.koinViewModel import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinActivityViewModel data object Home @@ -153,6 +153,8 @@ class Dashboard2( ) : TopLevelRoute { override val icon: ImageVector get() = TablerIcons.Home + override val label: Int + get() = R.string.home @Composable override fun Content() { @@ -172,14 +174,13 @@ class Dashboard2( @Composable fun HomeScreen( modifier: Modifier = Modifier, - viewModel: AccountViewModel = koinViewModel(), deeprQueries: DeeprQueries = koinInject(), analyticsManager: AnalyticsManager = koinInject(), mSelectedLink: GetLinksAndTags? = null, sharedText: SharedLink? = null, resetSharedText: () -> Unit, ) { - var isTagsSelectionActive by remember { mutableStateOf(false) } + val viewModel: AccountViewModel = koinActivityViewModel() val currentViewType by viewModel.viewType.collectAsStateWithLifecycle() val localNavigator = LocalNavigator.current @@ -193,7 +194,6 @@ fun HomeScreen( val scope = rememberCoroutineScope() val totalLinks by viewModel.countOfLinks.collectAsStateWithLifecycle() val favouriteLinks by viewModel.countOfFavouriteLinks.collectAsStateWithLifecycle() - val allTagsWithCount by viewModel.allTagsWithCount.collectAsStateWithLifecycle() val favouriteFilter by viewModel.favouriteFilter.collectAsStateWithLifecycle() // Handle shared text from other apps @@ -366,40 +366,16 @@ fun HomeScreen( } } }, - bottomBar = { - Box( - modifier = - Modifier - .fillMaxWidth(), - contentAlignment = Alignment.Center, - ) { - HorizontalFloatingToolbar( - expanded = true, - scrollBehavior = FloatingToolbarDefaults.exitAlwaysScrollBehavior(exitDirection = FloatingToolbarExitDirection.Bottom), - colors = FloatingToolbarDefaults.standardFloatingToolbarColors(), - content = { - IconButton(onClick = { - isTagsSelectionActive = true - }) { - Icon( - TablerIcons.Tag, - contentDescription = stringResource(R.string.tags), - ) - } - }, - floatingActionButton = { - FloatingToolbarDefaults.VibrantFloatingActionButton(onClick = { - selectedLink = createDeeprObject() - }) { - Icon( - TablerIcons.Plus, - contentDescription = stringResource(R.string.add_link), - ) - } - }, + floatingActionButton = { + FloatingActionButton(onClick = { + selectedLink = createDeeprObject() + }) { + Icon( + TablerIcons.Plus, + contentDescription = stringResource(R.string.add_link), ) } - }, + } ) { contentPadding -> Box( modifier = @@ -407,6 +383,7 @@ fun HomeScreen( .fillMaxSize(), ) { Content( + viewModel = viewModel, hazeState = hazeState, contentPaddingValues = contentPadding, selectedTag = selectedTag, @@ -433,32 +410,6 @@ fun HomeScreen( resetSharedText() } } - - if (isTagsSelectionActive) { - TagSelectionBottomSheet( - tagsWithCount = allTagsWithCount, - selectedTag = selectedTag, - dismissBottomSheet = { - isTagsSelectionActive = false - }, - setTagFilter = { viewModel.setTagFilter(it) }, - editTag = { tag -> - runBlocking { - try { - viewModel.updateTag(tag) - Result.success(true) - } catch (e: Exception) { - return@runBlocking Result.failure(e) - } - } - }, - deleteTag = { - viewModel.deleteTag(it.id) - Result.success(true) - }, - deeprQueries = deeprQueries, - ) - } } } @@ -472,7 +423,7 @@ fun Content( searchQuery: String, favouriteFilter: Int, modifier: Modifier = Modifier, - viewModel: AccountViewModel = koinViewModel(), + viewModel: AccountViewModel, editDeepr: (GetLinksAndTags) -> Unit = {}, ) { val accounts by viewModel.accounts.collectAsStateWithLifecycle() diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionBottomSheet.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionBottomSheet.kt index 24ab197d..9f601f6a 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionBottomSheet.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionBottomSheet.kt @@ -24,14 +24,12 @@ import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem import androidx.compose.material3.ListItemDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.ModalBottomSheet import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField import androidx.compose.material3.Text import androidx.compose.material3.TextField import androidx.compose.material3.TopAppBar import androidx.compose.material3.TopAppBarDefaults -import androidx.compose.material3.rememberModalBottomSheetState import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -40,6 +38,7 @@ import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle @@ -47,175 +46,44 @@ import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle +import com.yogeshpaliyal.deepr.DeeprQueries import com.yogeshpaliyal.deepr.GetAllTagsWithCount import com.yogeshpaliyal.deepr.R import com.yogeshpaliyal.deepr.Tags +import com.yogeshpaliyal.deepr.ui.TopLevelRoute import com.yogeshpaliyal.deepr.ui.components.ClearInputIconButton +import com.yogeshpaliyal.deepr.viewmodel.AccountViewModel import compose.icons.TablerIcons import compose.icons.tablericons.Edit import compose.icons.tablericons.Plus +import compose.icons.tablericons.Tag import compose.icons.tablericons.Trash +import kotlinx.coroutines.runBlocking +import org.koin.compose.koinInject +import org.koin.compose.viewmodel.koinActivityViewModel -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun TagSelectionBottomSheet( - tagsWithCount: List, - selectedTag: List, - dismissBottomSheet: () -> Unit, - setTagFilter: (Tags?) -> Unit, - editTag: (Tags) -> Result, - deleteTag: (Tags) -> Result, - deeprQueries: com.yogeshpaliyal.deepr.DeeprQueries, - modifier: Modifier = Modifier, -) { - val modalBottomSheetState = rememberModalBottomSheetState(skipPartiallyExpanded = true) - var isTagEditEnable by remember { mutableStateOf(null) } - var isTagDeleteEnable by remember { mutableStateOf(null) } - var tagEditError by remember { mutableStateOf(null) } - val context = LocalContext.current - var newTagName by remember { mutableStateOf("") } - isTagEditEnable?.let { tag -> - AlertDialog( - onDismissRequest = { - isTagEditEnable = null - tagEditError = null - }, - title = { - Text(text = stringResource(R.string.edit_tag)) - }, - text = { - Column { - TextField( - value = tag.name, - onValueChange = { - isTagEditEnable = tag.copy(name = it) - }, - isError = tagEditError != null, - supportingText = { - tagEditError?.let { - Text(text = it) - } - }, - suffix = - if (isTagEditEnable?.name.isNullOrEmpty()) { - null - } else { - { - ClearInputIconButton( - onClick = { - isTagEditEnable = tag.copy(name = "") - }, - ) - } - }, - ) - } - }, - confirmButton = { - Button(onClick = { - val result = editTag(Tags(tag.id, tag.name)) - if (result.isFailure) { - val exception = result.exceptionOrNull() - tagEditError = - when (exception) { - is SQLiteConstraintException -> { - context.getString(R.string.tag_name_exists) - } - - else -> { - context.getString(R.string.failed_to_edit_tag) - } - } - } else { - isTagEditEnable = null - tagEditError = null - Toast - .makeText( - context, - context.getString(R.string.tag_edited_successfully), - Toast.LENGTH_SHORT, - ).show() - } - }) { - Text(stringResource(R.string.edit)) - } - }, - dismissButton = { - Button(onClick = { - isTagEditEnable = null - tagEditError = null - }) { - Text(stringResource(R.string.cancel)) - } - }, - ) - } +object TagSelectionScreen : TopLevelRoute { + override val icon: ImageVector + get() = TablerIcons.Tag + override val label: Int + get() = R.string.tags - isTagDeleteEnable?.let { tag -> - AlertDialog( - onDismissRequest = { - isTagDeleteEnable = null - }, - title = { - Text(text = stringResource(R.string.delete_tag)) - }, - text = { - val message = - buildAnnotatedString { - append("Are you sure you want to delete ") - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append("'${tag.name}'") - } - append(" tag?") - } - Text(text = message) - }, - confirmButton = { - Button( - onClick = { - val result = deleteTag(Tags(tag.id, tag.name)) - if (result.isFailure) { - Toast - .makeText( - context, - context.getString( - R.string.failed_to_delete_tag, - result.exceptionOrNull(), - ), - Toast.LENGTH_SHORT, - ).show() - } else { - isTagDeleteEnable = null - Toast - .makeText( - context, - context.getString(R.string.tag_deleted_successfully), - Toast.LENGTH_SHORT, - ).show() - } - }, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), - ) { - Text(stringResource(R.string.delete)) - } - }, - dismissButton = { - OutlinedButton(onClick = { - isTagDeleteEnable = null - }) { - Text(stringResource(R.string.cancel)) - } - }, - ) - } + @OptIn(ExperimentalMaterial3Api::class) + @Composable + override fun Content() { + val viewModel: AccountViewModel = koinActivityViewModel() + val selectedTag by viewModel.selectedTagFilter.collectAsStateWithLifecycle() + var newTagName by remember { mutableStateOf("") } + val tagsWithCount by viewModel.allTagsWithCount.collectAsStateWithLifecycle() + val context = LocalContext.current + val deeprQueries: DeeprQueries = koinInject() + var isTagEditEnable by remember { mutableStateOf(null) } + var isTagDeleteEnable by remember { mutableStateOf(null) } + var tagEditError by remember { mutableStateOf(null) } - ModalBottomSheet(sheetState = modalBottomSheetState, onDismissRequest = dismissBottomSheet) { - Column(modifier) { + Column { TopAppBar( title = { Row( @@ -300,7 +168,7 @@ fun TagSelectionBottomSheet( ListItem( modifier = Modifier.clickable { - setTagFilter(null) + viewModel.setTagFilter(null) }, headlineContent = { Text( @@ -336,14 +204,14 @@ fun TagSelectionBottomSheet( ListItem( modifier = Modifier.clickable { - setTagFilter(Tags(tag.id, tag.name)) + viewModel.setTagFilter(Tags(tag.id, tag.name)) // Don't dismiss to allow multi-selection }, leadingContent = { androidx.compose.material3.Checkbox( checked = isSelected, onCheckedChange = { - setTagFilter(Tags(tag.id, tag.name)) + viewModel.setTagFilter(Tags(tag.id, tag.name)) }, ) }, @@ -381,5 +249,140 @@ fun TagSelectionBottomSheet( } } } + + isTagEditEnable?.let { tag -> + AlertDialog( + onDismissRequest = { + isTagEditEnable = null + tagEditError = null + }, + title = { + Text(text = stringResource(R.string.edit_tag)) + }, + text = { + Column { + TextField( + value = tag.name, + onValueChange = { + isTagEditEnable = tag.copy(name = it) + }, + isError = tagEditError != null, + supportingText = { + tagEditError?.let { + Text(text = it) + } + }, + suffix = + if (isTagEditEnable?.name.isNullOrEmpty()) { + null + } else { + { + ClearInputIconButton( + onClick = { + isTagEditEnable = tag.copy(name = "") + }, + ) + } + }, + ) + } + }, + confirmButton = { + Button(onClick = { + val result = runBlocking { + try { + viewModel.updateTag(Tags(tag.id, tag.name)) + Result.success(true) + } catch (e: Exception) { + return@runBlocking Result.failure(e) + } + } + if (result.isFailure) { + val exception = result.exceptionOrNull() + tagEditError = + when (exception) { + is SQLiteConstraintException -> { + context.getString(R.string.tag_name_exists) + } + + else -> { + context.getString(R.string.failed_to_edit_tag) + } + } + } else { + isTagEditEnable = null + tagEditError = null + Toast + .makeText( + context, + context.getString(R.string.tag_edited_successfully), + Toast.LENGTH_SHORT, + ).show() + } + }) { + Text(stringResource(R.string.edit)) + } + }, + dismissButton = { + Button(onClick = { + isTagEditEnable = null + tagEditError = null + }) { + Text(stringResource(R.string.cancel)) + } + }, + ) + } + + isTagDeleteEnable?.let { tag -> + AlertDialog( + onDismissRequest = { + isTagDeleteEnable = null + }, + title = { + Text(text = stringResource(R.string.delete_tag)) + }, + text = { + val message = + buildAnnotatedString { + append("Are you sure you want to delete ") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append("'${tag.name}'") + } + append(" tag?") + } + Text(text = message) + }, + confirmButton = { + Button( + onClick = { + viewModel.deleteTag(tag.id) + + isTagDeleteEnable = null + Toast + .makeText( + context, + context.getString(R.string.tag_deleted_successfully), + Toast.LENGTH_SHORT, + ).show() + }, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.errorContainer, + contentColor = MaterialTheme.colorScheme.onErrorContainer, + ), + ) { + Text(stringResource(R.string.delete)) + } + }, + dismissButton = { + OutlinedButton(onClick = { + isTagDeleteEnable = null + }) { + Text(stringResource(R.string.cancel)) + } + }, + ) + } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1e27473c..19ca581b 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -54,6 +54,7 @@ Tags: Suggestions: Filter + Home +%d more Tags: %d Show less From 4bae1f0941c1772d4da17f679cff4fa58c85978d Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Wed, 12 Nov 2025 22:41:39 +0530 Subject: [PATCH 4/6] feat: Integrate LocalNavigator into various screens and update UI components --- .editorconfig | 1 + .../com/yogeshpaliyal/deepr/MainActivity.kt | 19 +++--- .../com/yogeshpaliyal/deepr/ui/Navigation.kt | 1 - .../yogeshpaliyal/deepr/ui/screens/AboutUs.kt | 2 +- .../deepr/ui/screens/BackupScreen.kt | 2 +- .../deepr/ui/screens/LocalNetworkServer.kt | 4 +- .../deepr/ui/screens/RestoreScreen.kt | 2 +- .../deepr/ui/screens/ScanQRVirtualScreen.kt | 12 ++-- .../deepr/ui/screens/Settings.kt | 2 +- .../ui/screens/TransferLinkLocalServer.kt | 4 +- .../deepr/ui/screens/home/Home.kt | 65 ++++++++++++++----- ...onBottomSheet.kt => TagSelectionScreen.kt} | 16 ++--- 12 files changed, 80 insertions(+), 50 deletions(-) rename app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/{TagSelectionBottomSheet.kt => TagSelectionScreen.kt} (97%) diff --git a/.editorconfig b/.editorconfig index 310628a0..63ea01f7 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,2 +1,3 @@ [*.{kt,kts}] ktlint_function_naming_ignore_when_annotated_with = Composable +compose_allowed_composition_locals = LocalNavigator diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt b/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt index d6c93875..d38d833e 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt @@ -129,11 +129,9 @@ class MainActivity : ComponentActivity() { } } - private val TOP_LEVEL_ROUTES: List = listOf(Dashboard2 {}, TagSelectionScreen, Settings) - @OptIn(ExperimentalMaterial3Api::class) @Composable fun Dashboard( @@ -141,14 +139,15 @@ fun Dashboard( sharedText: SharedLink? = null, resetSharedText: () -> Unit, ) { - val backStack = remember { - TopLevelBackStack( - Dashboard2( - sharedText = sharedText, - resetSharedText = resetSharedText + val backStack = + remember { + TopLevelBackStack( + Dashboard2( + sharedText = sharedText, + resetSharedText = resetSharedText, + ), ) - ) - } + } val current = backStack.getLast() val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior() val hapticFeedback = LocalHapticFeedback.current @@ -208,4 +207,4 @@ fun Dashboard( ) } } -} \ No newline at end of file +} diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt index cfbd576b..f1f1dc12 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt @@ -12,7 +12,6 @@ import androidx.navigation3.runtime.NavKey import com.yogeshpaliyal.deepr.ui.screens.home.Dashboard2 import kotlin.collections.remove - val LocalNavigator = compositionLocalOf> { TopLevelBackStack(Dashboard2 {}) } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt index 0bab9df8..5c589967 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/AboutUs.kt @@ -36,8 +36,8 @@ import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import com.yogeshpaliyal.deepr.BuildConfig -import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.ui.Screen import compose.icons.TablerIcons import compose.icons.tablericons.ArrowLeft diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt index 18e989fa..d1600207 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/BackupScreen.kt @@ -32,8 +32,8 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.ui.components.ServerStatusBar import com.yogeshpaliyal.deepr.ui.components.SettingsItem 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 1648fa92..27f4f7a9 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 @@ -60,10 +60,10 @@ import com.google.accompanist.permissions.PermissionState import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState import com.lightspark.composeqr.QrCodeView -import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R -import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.server.LocalServerService +import com.yogeshpaliyal.deepr.ui.LocalNavigator +import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.viewmodel.LocalServerViewModel import compose.icons.TablerIcons import compose.icons.tablericons.ArrowLeft diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt index 787fd918..04facd8e 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/RestoreScreen.kt @@ -31,8 +31,8 @@ import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.dp -import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.ui.components.ServerStatusBar import com.yogeshpaliyal.deepr.ui.components.SettingsItem diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt index 3e4dc6fc..0e5f0898 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt @@ -17,7 +17,6 @@ import com.yogeshpaliyal.deepr.util.normalizeLink import compose.icons.TablerIcons import compose.icons.tablericons.Qrcode - @Composable fun ScanQRCode() { val navigatorContext = LocalNavigator.current @@ -34,10 +33,12 @@ fun ScanQRCode() { if (isValidDeeplink(normalizedLink)) { navigatorContext.clearStackAndAdd( Dashboard2( - mSelectedLink = createDeeprObject( - link = normalizedLink - ) - ) {}) + mSelectedLink = + createDeeprObject( + link = normalizedLink, + ), + ) {}, + ) } else { Toast.makeText(context, "Invalid deeplink", Toast.LENGTH_SHORT).show() } @@ -51,5 +52,4 @@ fun ScanQRCode() { qrScanner.launch(ScanOptions()) }, ) - } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt index 39a5bea4..c4719a6b 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt @@ -48,9 +48,9 @@ import androidx.core.net.toUri import androidx.lifecycle.compose.collectAsStateWithLifecycle import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.yogeshpaliyal.deepr.BuildConfig -import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.MainActivity import com.yogeshpaliyal.deepr.R +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.ui.TopLevelRoute import com.yogeshpaliyal.deepr.ui.components.LanguageSelectionDialog import com.yogeshpaliyal.deepr.ui.components.ServerStatusBar diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt index fd277b7e..69077208 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/TransferLinkLocalServer.kt @@ -56,11 +56,11 @@ import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.rememberPermissionState import com.journeyapps.barcodescanner.ScanOptions import com.lightspark.composeqr.QrCodeView -import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R -import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.server.LocalServerService import com.yogeshpaliyal.deepr.server.LocalServerTransferLink +import com.yogeshpaliyal.deepr.ui.LocalNavigator +import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.util.QRScanner import com.yogeshpaliyal.deepr.viewmodel.TransferLinkLocalServerViewModel import compose.icons.TablerIcons 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 3c8e0c28..247c9bb4 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 @@ -11,6 +11,7 @@ import androidx.compose.animation.scaleOut import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.background import androidx.compose.foundation.clickable +import androidx.compose.foundation.gestures.ScrollableState import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -26,8 +27,12 @@ import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.LazyListState +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells +import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.selection.SelectionContainer @@ -38,11 +43,8 @@ import androidx.compose.material3.AppBarWithSearch import androidx.compose.material3.ContainedLoadingIndicator import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi +import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilterChip -import androidx.compose.material3.FloatingActionButton -import androidx.compose.material3.FloatingToolbarDefaults -import androidx.compose.material3.FloatingToolbarExitDirection -import androidx.compose.material3.HorizontalFloatingToolbar import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.ListItem @@ -59,6 +61,7 @@ import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow import androidx.compose.material3.Text +import androidx.compose.material3.TextButton import androidx.compose.material3.TooltipAnchorPosition import androidx.compose.material3.TooltipBox import androidx.compose.material3.TooltipDefaults @@ -67,6 +70,7 @@ import androidx.compose.material3.rememberSearchBarState import androidx.compose.material3.rememberTooltipState import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.derivedStateOf import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember @@ -90,14 +94,14 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.yogeshpaliyal.deepr.DeeprQueries import com.yogeshpaliyal.deepr.GetLinksAndTags -import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.R import com.yogeshpaliyal.deepr.SharedLink import com.yogeshpaliyal.deepr.Tags -import com.yogeshpaliyal.deepr.ui.TopLevelRoute import com.yogeshpaliyal.deepr.analytics.AnalyticsEvents import com.yogeshpaliyal.deepr.analytics.AnalyticsManager import com.yogeshpaliyal.deepr.analytics.AnalyticsParams +import com.yogeshpaliyal.deepr.ui.LocalNavigator +import com.yogeshpaliyal.deepr.ui.TopLevelRoute import com.yogeshpaliyal.deepr.ui.components.ClearInputIconButton import com.yogeshpaliyal.deepr.ui.components.CreateShortcutDialog import com.yogeshpaliyal.deepr.ui.components.DeleteConfirmationDialog @@ -195,6 +199,19 @@ fun HomeScreen( val totalLinks by viewModel.countOfLinks.collectAsStateWithLifecycle() val favouriteLinks by viewModel.countOfFavouriteLinks.collectAsStateWithLifecycle() val favouriteFilter by viewModel.favouriteFilter.collectAsStateWithLifecycle() + val listState = if (currentViewType == ViewType.GRID) rememberLazyStaggeredGridState() else rememberLazyListState() + val isExpanded by remember(listState) { + // Example: expanded only when at the very top of the list + derivedStateOf { + if (listState is LazyStaggeredGridState) { + listState.firstVisibleItemIndex == 0 + } else if (listState is LazyListState) { + listState.firstVisibleItemIndex == 0 + } else { + true + } + } + } // Handle shared text from other apps LaunchedEffect(sharedText) { @@ -367,15 +384,22 @@ fun HomeScreen( } }, floatingActionButton = { - FloatingActionButton(onClick = { - selectedLink = createDeeprObject() - }) { - Icon( - TablerIcons.Plus, - contentDescription = stringResource(R.string.add_link), - ) - } - } + ExtendedFloatingActionButton( + icon = { + Icon( + TablerIcons.Plus, + contentDescription = stringResource(R.string.add_link), + ) + }, + text = { + Text(stringResource(R.string.add_link)) + }, + expanded = isExpanded, + onClick = { + selectedLink = createDeeprObject() + }, + ) + }, ) { contentPadding -> Box( modifier = @@ -383,6 +407,7 @@ fun HomeScreen( .fillMaxSize(), ) { Content( + listState = listState, viewModel = viewModel, hazeState = hazeState, contentPaddingValues = contentPadding, @@ -416,14 +441,15 @@ fun HomeScreen( @OptIn(ExperimentalMaterial3ExpressiveApi::class, ExperimentalMaterial3Api::class) @Composable fun Content( + listState: ScrollableState, hazeState: HazeState, selectedTag: List, contentPaddingValues: PaddingValues, currentViewType: @ViewType Int, searchQuery: String, favouriteFilter: Int, - modifier: Modifier = Modifier, viewModel: AccountViewModel, + modifier: Modifier = Modifier, editDeepr: (GetLinksAndTags) -> Unit = {}, ) { val accounts by viewModel.accounts.collectAsStateWithLifecycle() @@ -532,6 +558,7 @@ fun Content( Column(modifier.fillMaxSize()) { DeeprList( + listState = listState, modifier = Modifier .weight(1f) @@ -730,7 +757,7 @@ fun Content( // Show "Load More" or "Show Less" button if there are more than 9 tags if ((selectedTags?.size ?: 0) > 9) { - androidx.compose.material3.TextButton( + TextButton( onClick = { tagsExpanded = !tagsExpanded }, modifier = Modifier.padding(start = 4.dp), contentPadding = PaddingValues(horizontal = 8.dp, vertical = 0.dp), @@ -798,6 +825,7 @@ fun MenuListItem( @Composable fun DeeprList( + listState: ScrollableState, accounts: List, selectedTag: List, contentPaddingValues: PaddingValues, @@ -899,6 +927,7 @@ fun DeeprList( when (viewType) { ViewType.LIST -> { LazyColumn( + state = listState as? LazyListState ?: rememberLazyListState(), modifier = modifier, contentPadding = contentPaddingValues, verticalArrangement = Arrangement.spacedBy(4.dp), @@ -923,6 +952,7 @@ fun DeeprList( ViewType.GRID -> { LazyVerticalStaggeredGrid( + state = listState as? LazyStaggeredGridState ?: rememberLazyStaggeredGridState(), columns = StaggeredGridCells.Adaptive(minSize = 160.dp), modifier = modifier, contentPadding = contentPaddingValues, @@ -946,6 +976,7 @@ fun DeeprList( ViewType.COMPACT -> { LazyColumn( + state = listState as? LazyListState ?: rememberLazyListState(), modifier = modifier, contentPadding = contentPaddingValues, verticalArrangement = Arrangement.spacedBy(4.dp), diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionBottomSheet.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt similarity index 97% rename from app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionBottomSheet.kt rename to app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt index 9f601f6a..bfc0b6f4 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionBottomSheet.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt @@ -63,7 +63,6 @@ import kotlinx.coroutines.runBlocking import org.koin.compose.koinInject import org.koin.compose.viewmodel.koinActivityViewModel - object TagSelectionScreen : TopLevelRoute { override val icon: ImageVector get() = TablerIcons.Tag @@ -289,14 +288,15 @@ object TagSelectionScreen : TopLevelRoute { }, confirmButton = { Button(onClick = { - val result = runBlocking { - try { - viewModel.updateTag(Tags(tag.id, tag.name)) - Result.success(true) - } catch (e: Exception) { - return@runBlocking Result.failure(e) + val result = + runBlocking { + try { + viewModel.updateTag(Tags(tag.id, tag.name)) + Result.success(true) + } catch (e: Exception) { + return@runBlocking Result.failure(e) + } } - } if (result.isFailure) { val exception = result.exceptionOrNull() tagEditError = From 893775b3c2a9af445eb2f9af5640c7babe357afe Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Wed, 12 Nov 2025 23:03:26 +0530 Subject: [PATCH 5/6] feat: Add selected tags filter chips to Home screen --- .../deepr/ui/screens/home/Home.kt | 47 ++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) 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 247c9bb4..c638b70d 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 @@ -133,6 +133,7 @@ import compose.icons.tablericons.Refresh import compose.icons.tablericons.Search import compose.icons.tablericons.Tag import compose.icons.tablericons.Trash +import compose.icons.tablericons.X import dev.chrisbanes.haze.HazeState import dev.chrisbanes.haze.hazeEffect import dev.chrisbanes.haze.hazeSource @@ -381,6 +382,43 @@ fun HomeScreen( label = { Text(stringResource(R.string.favourites) + " (${favouriteLinks ?: 0})") }, ) } + + // Selected tags filter chips + AnimatedVisibility( + visible = selectedTag.isNotEmpty(), + enter = expandVertically(expandFrom = Alignment.Top), + exit = shrinkVertically(shrinkTowards = Alignment.Top), + ) { + FlowRow( + modifier = + Modifier + .fillMaxWidth() + .padding(horizontal = 8.dp, vertical = 4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + selectedTag.forEach { tag -> + FilterChip( + selected = true, + onClick = { viewModel.setTagFilter(tag) }, + label = { Text(tag.name) }, + leadingIcon = { + Icon( + TablerIcons.Tag, + contentDescription = null, + modifier = Modifier.size(16.dp), + ) + }, + trailingIcon = { + Icon( + TablerIcons.X, + contentDescription = stringResource(R.string.remove_tag), + modifier = Modifier.size(18.dp), + ) + }, + ) + } + } + } } }, floatingActionButton = { @@ -406,11 +444,18 @@ fun HomeScreen( Modifier .fillMaxSize(), ) { + val layoutDirection = LocalLayoutDirection.current Content( listState = listState, viewModel = viewModel, hazeState = hazeState, - contentPaddingValues = contentPadding, + contentPaddingValues = + PaddingValues( + start = contentPadding.calculateLeftPadding(layoutDirection), + end = contentPadding.calculateRightPadding(layoutDirection), + top = contentPadding.calculateTopPadding() + 8.dp, + bottom = contentPadding.calculateBottomPadding() + 8.dp, + ), selectedTag = selectedTag, currentViewType = currentViewType, searchQuery = textFieldState.text.toString(), From add8d9491d4623b4c3f64c9d7f3c78efd6a79f59 Mon Sep 17 00:00:00 2001 From: Yogesh Choudhary Paliyal Date: Thu, 13 Nov 2025 00:10:19 +0530 Subject: [PATCH 6/6] feat: Enhance Home screen with LocalSharedText and update navigation handling --- .editorconfig | 2 +- .../com/yogeshpaliyal/deepr/MainActivity.kt | 131 +-- .../com/yogeshpaliyal/deepr/ui/Navigation.kt | 12 +- .../deepr/ui/screens/ScanQRVirtualScreen.kt | 2 +- .../deepr/ui/screens/Settings.kt | 14 +- .../yogeshpaliyal/deepr/ui/screens/Splash.kt | 2 +- .../deepr/ui/screens/home/Home.kt | 42 +- .../ui/screens/home/TagSelectionScreen.kt | 763 +++++++++++------- 8 files changed, 613 insertions(+), 355 deletions(-) diff --git a/.editorconfig b/.editorconfig index 63ea01f7..96ee3b98 100644 --- a/.editorconfig +++ b/.editorconfig @@ -1,3 +1,3 @@ [*.{kt,kts}] ktlint_function_naming_ignore_when_annotated_with = Composable -compose_allowed_composition_locals = LocalNavigator +compose_allowed_composition_locals = LocalNavigator,LocalSharedText diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt b/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt index d38d833e..51797eea 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/MainActivity.kt @@ -9,7 +9,7 @@ import androidx.activity.enableEdgeToEdge import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.material3.BottomAppBar import androidx.compose.material3.BottomAppBarDefaults import androidx.compose.material3.ExperimentalMaterial3Api @@ -20,11 +20,13 @@ import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.platform.LocalHapticFeedback +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.navigation3.rememberViewModelStoreNavEntryDecorator @@ -33,6 +35,7 @@ import androidx.navigation3.runtime.rememberSavedStateNavEntryDecorator import androidx.navigation3.ui.NavDisplay import androidx.navigation3.ui.rememberSceneSetupNavEntryDecorator import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore +import com.yogeshpaliyal.deepr.ui.BaseScreen import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.ui.Screen import com.yogeshpaliyal.deepr.ui.TopLevelBackStack @@ -130,7 +133,10 @@ class MainActivity : ComponentActivity() { } private val TOP_LEVEL_ROUTES: List = - listOf(Dashboard2 {}, TagSelectionScreen, Settings) + listOf(Dashboard2(), TagSelectionScreen, Settings) + +val LocalSharedText = + compositionLocalOf Unit>?> { null } @OptIn(ExperimentalMaterial3Api::class) @Composable @@ -141,70 +147,81 @@ fun Dashboard( ) { val backStack = remember { - TopLevelBackStack( - Dashboard2( - sharedText = sharedText, - resetSharedText = resetSharedText, - ), + TopLevelBackStack( + Dashboard2(), ) } val current = backStack.getLast() val scrollBehavior = BottomAppBarDefaults.exitAlwaysScrollBehavior() val hapticFeedback = LocalHapticFeedback.current + val layoutDirection = LocalLayoutDirection.current - CompositionLocalProvider(LocalNavigator provides backStack) { - Scaffold( - modifier = modifier, - bottomBar = { - AnimatedVisibility( - (TOP_LEVEL_ROUTES.any { it::class == current::class }), - enter = slideInVertically(initialOffsetY = { it }), - exit = slideOutVertically(targetOffsetY = { it }), - ) { - BottomAppBar(scrollBehavior = scrollBehavior) { - TOP_LEVEL_ROUTES.forEach { topLevelRoute -> - val isSelected = topLevelRoute::class == backStack.topLevelKey::class - NavigationBarItem( - selected = isSelected, - onClick = { - hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick) - backStack.addTopLevel(topLevelRoute) - }, - label = { - Text(stringResource(topLevelRoute.label)) - }, - icon = { - Icon( - imageVector = topLevelRoute.icon, - contentDescription = null, - ) - }, - ) + CompositionLocalProvider(LocalSharedText provides Pair(sharedText, resetSharedText)) { + CompositionLocalProvider(LocalNavigator provides backStack) { + Scaffold( + modifier = modifier, + bottomBar = { + AnimatedVisibility( + (TOP_LEVEL_ROUTES.any { it::class == current::class }), + enter = slideInVertically(initialOffsetY = { it }), + exit = slideOutVertically(targetOffsetY = { it }), + ) { + BottomAppBar(scrollBehavior = scrollBehavior) { + TOP_LEVEL_ROUTES.forEach { topLevelRoute -> + val isSelected = + topLevelRoute::class == backStack.topLevelKey::class + NavigationBarItem( + selected = isSelected, + onClick = { + hapticFeedback.performHapticFeedback(HapticFeedbackType.ContextClick) + backStack.addTopLevel(topLevelRoute) + }, + label = { + Text(stringResource(topLevelRoute.label)) + }, + icon = { + Icon( + imageVector = topLevelRoute.icon, + contentDescription = null, + ) + }, + ) + } } } - } - }, - ) { contentPadding -> - NavDisplay( - modifier = Modifier.padding(contentPadding), - backStack = backStack.backStack, - entryDecorators = - listOf( - // Add the default decorators for managing scenes and saving state - rememberSceneSetupNavEntryDecorator(), - rememberSavedStateNavEntryDecorator(), - // Then add the view model store decorator - rememberViewModelStoreNavEntryDecorator(), - ), - onBack = { - backStack.removeLast() - }, - entryProvider = { - NavEntry(it) { - it.Content() - } }, - ) + ) { contentPadding -> + NavDisplay( + backStack = backStack.backStack, + entryDecorators = + listOf( + // Add the default decorators for managing scenes and saving state + rememberSceneSetupNavEntryDecorator(), + rememberSavedStateNavEntryDecorator(), + // Then add the view model store decorator + rememberViewModelStoreNavEntryDecorator(), + ), + onBack = { + backStack.removeLast() + }, + entryProvider = { + NavEntry(it) { entryItem -> + if (entryItem is TopLevelRoute) { + entryItem.Content( + WindowInsets( + left = contentPadding.calculateLeftPadding(layoutDirection), + right = contentPadding.calculateRightPadding(layoutDirection), + top = contentPadding.calculateTopPadding(), + bottom = contentPadding.calculateBottomPadding(), + ), + ) + } else if (entryItem is Screen) { + entryItem.Content() + } + } + }, + ) + } } } } diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt index f1f1dc12..1e810937 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/Navigation.kt @@ -1,5 +1,6 @@ package com.yogeshpaliyal.deepr.ui +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.runtime.Composable import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.getValue @@ -13,17 +14,22 @@ import com.yogeshpaliyal.deepr.ui.screens.home.Dashboard2 import kotlin.collections.remove val LocalNavigator = - compositionLocalOf> { TopLevelBackStack(Dashboard2 {}) } + compositionLocalOf> { TopLevelBackStack(Dashboard2()) } -interface Screen : NavKey { +sealed interface BaseScreen : NavKey + +interface Screen : BaseScreen { @Composable fun Content() } -interface TopLevelRoute : Screen { +interface TopLevelRoute : BaseScreen { val icon: ImageVector val label: Int + + @Composable + fun Content(windowInsets: WindowInsets) } class TopLevelBackStack( diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt index 0e5f0898..75bafae9 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/ScanQRVirtualScreen.kt @@ -37,7 +37,7 @@ fun ScanQRCode() { createDeeprObject( link = normalizedLink, ), - ) {}, + ), ) } else { Toast.makeText(context, "Invalid deeplink", Toast.LENGTH_SHORT).show() diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt index c4719a6b..0b9fa03c 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Settings.kt @@ -74,20 +74,21 @@ import compose.icons.tablericons.Upload import org.koin.androidx.compose.koinViewModel object Settings : TopLevelRoute { - @Composable - override fun Content() { - SettingsScreen() - } - override val icon: ImageVector get() = TablerIcons.Settings override val label: Int get() = R.string.settings + + @Composable + override fun Content(windowInsets: WindowInsets) { + SettingsScreen(windowInsets) + } } @OptIn(ExperimentalMaterial3Api::class, ExperimentalPermissionsApi::class) @Composable fun SettingsScreen( + windowInsets: WindowInsets, modifier: Modifier = Modifier, viewModel: AccountViewModel = koinViewModel(), ) { @@ -110,12 +111,11 @@ fun SettingsScreen( val isThumbnailEnable by viewModel.isThumbnailEnable.collectAsStateWithLifecycle() Scaffold( - contentWindowInsets = WindowInsets(), + contentWindowInsets = windowInsets, modifier = modifier.fillMaxSize(), topBar = { Column { TopAppBar( - windowInsets = WindowInsets(), title = { Text(stringResource(R.string.settings)) }, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt index 809efdbe..f127f793 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/Splash.kt @@ -31,7 +31,7 @@ object Splash : Screen { // if (isFirstTime) { // navigator.add(IntroScreen) // } else { - navigator.clearStackAndAdd(Dashboard2 {}) + navigator.clearStackAndAdd(Dashboard2()) // } } 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 c638b70d..9e2cc3e8 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 @@ -4,6 +4,7 @@ import android.content.ClipData import android.content.ClipboardManager import android.content.Context import android.widget.Toast +import androidx.activity.compose.BackHandler import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.expandVertically import androidx.compose.animation.scaleIn @@ -33,6 +34,7 @@ import androidx.compose.foundation.lazy.staggeredgrid.LazyStaggeredGridState import androidx.compose.foundation.lazy.staggeredgrid.LazyVerticalStaggeredGrid import androidx.compose.foundation.lazy.staggeredgrid.StaggeredGridCells import androidx.compose.foundation.lazy.staggeredgrid.rememberLazyStaggeredGridState +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.input.clearText import androidx.compose.foundation.text.input.rememberTextFieldState import androidx.compose.foundation.text.selection.SelectionContainer @@ -60,6 +62,7 @@ import androidx.compose.material3.SearchBarValue import androidx.compose.material3.SegmentedButton import androidx.compose.material3.SegmentedButtonDefaults import androidx.compose.material3.SingleChoiceSegmentedButtonRow +import androidx.compose.material3.Surface import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.material3.TooltipAnchorPosition @@ -81,8 +84,10 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.graphicsLayer import androidx.compose.ui.graphics.vector.ImageVector +import androidx.compose.ui.hapticfeedback.HapticFeedbackType import androidx.compose.ui.layout.ContentScale import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalHapticFeedback import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.TextStyle @@ -94,6 +99,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle import coil3.compose.AsyncImage import com.yogeshpaliyal.deepr.DeeprQueries import com.yogeshpaliyal.deepr.GetLinksAndTags +import com.yogeshpaliyal.deepr.LocalSharedText import com.yogeshpaliyal.deepr.R import com.yogeshpaliyal.deepr.SharedLink import com.yogeshpaliyal.deepr.Tags @@ -153,8 +159,6 @@ data object Home ) class Dashboard2( val mSelectedLink: GetLinksAndTags? = null, - val sharedText: SharedLink? = null, - val resetSharedText: () -> Unit, ) : TopLevelRoute { override val icon: ImageVector get() = TablerIcons.Home @@ -162,12 +166,18 @@ class Dashboard2( get() = R.string.home @Composable - override fun Content() { - HomeScreen( - mSelectedLink = mSelectedLink, - sharedText = sharedText, - resetSharedText = resetSharedText, - ) + override fun Content(windowInsets: WindowInsets) { + val localSharedText = LocalSharedText.current + Surface(shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp)) { + HomeScreen( + windowInsets, + mSelectedLink = mSelectedLink, + sharedText = localSharedText?.first, + resetSharedText = { + localSharedText?.second?.invoke() + }, + ) + } } } @@ -178,6 +188,7 @@ class Dashboard2( ) @Composable fun HomeScreen( + windowInsets: WindowInsets, modifier: Modifier = Modifier, deeprQueries: DeeprQueries = koinInject(), analyticsManager: AnalyticsManager = koinInject(), @@ -188,6 +199,7 @@ fun HomeScreen( val viewModel: AccountViewModel = koinActivityViewModel() val currentViewType by viewModel.viewType.collectAsStateWithLifecycle() val localNavigator = LocalNavigator.current + val hapticFeedback = LocalHapticFeedback.current var selectedLink by remember { mutableStateOf(mSelectedLink) } val selectedTag by viewModel.selectedTagFilter.collectAsStateWithLifecycle() @@ -214,6 +226,17 @@ fun HomeScreen( } } + BackHandler(enabled = selectedTag.isNotEmpty() || searchBarState.currentValue == SearchBarValue.Expanded) { + hapticFeedback.performHapticFeedback(HapticFeedbackType.VirtualKey) + if (searchBarState.currentValue == SearchBarValue.Expanded) { + scope.launch { + searchBarState.animateToCollapsed() + } + } else if (selectedTag.isNotEmpty()) { + viewModel.setTagFilter(null) + } + } + // Handle shared text from other apps LaunchedEffect(sharedText) { if (!sharedText?.url.isNullOrBlank()) { @@ -323,7 +346,7 @@ fun HomeScreen( } Scaffold( - contentWindowInsets = WindowInsets(), + contentWindowInsets = windowInsets, modifier = modifier.fillMaxSize(), topBar = { Column( @@ -335,7 +358,6 @@ fun HomeScreen( ).fillMaxWidth(), ) { AppBarWithSearch( - windowInsets = WindowInsets(), scrollBehavior = scrollBehavior, state = searchBarState, inputField = inputField, diff --git a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt index bfc0b6f4..e9fad689 100644 --- a/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt +++ b/app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/home/TagSelectionScreen.kt @@ -1,35 +1,46 @@ package com.yogeshpaliyal.deepr.ui.screens.home import android.database.sqlite.SQLiteConstraintException +import android.view.Surface import android.widget.Toast +import androidx.compose.animation.AnimatedVisibility +import androidx.compose.animation.scaleIn +import androidx.compose.animation.scaleOut +import androidx.compose.foundation.BorderStroke import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.WindowInsets +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.layout.width import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.material3.AlertDialog import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.Card +import androidx.compose.material3.CardDefaults import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.ExtendedFloatingActionButton import androidx.compose.material3.FilledIconButton -import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.Icon import androidx.compose.material3.IconButton -import androidx.compose.material3.ListItem -import androidx.compose.material3.ListItemDefaults +import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedButton import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Surface import androidx.compose.material3.Text -import androidx.compose.material3.TextField -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults +import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -37,13 +48,13 @@ import androidx.compose.runtime.remember import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -51,11 +62,13 @@ import com.yogeshpaliyal.deepr.DeeprQueries import com.yogeshpaliyal.deepr.GetAllTagsWithCount import com.yogeshpaliyal.deepr.R import com.yogeshpaliyal.deepr.Tags +import com.yogeshpaliyal.deepr.ui.LocalNavigator import com.yogeshpaliyal.deepr.ui.TopLevelRoute import com.yogeshpaliyal.deepr.ui.components.ClearInputIconButton import com.yogeshpaliyal.deepr.viewmodel.AccountViewModel import compose.icons.TablerIcons import compose.icons.tablericons.Edit +import compose.icons.tablericons.Eye import compose.icons.tablericons.Plus import compose.icons.tablericons.Tag import compose.icons.tablericons.Trash @@ -71,318 +84,518 @@ object TagSelectionScreen : TopLevelRoute { @OptIn(ExperimentalMaterial3Api::class) @Composable - override fun Content() { + override fun Content(windowInsets: WindowInsets) { val viewModel: AccountViewModel = koinActivityViewModel() val selectedTag by viewModel.selectedTagFilter.collectAsStateWithLifecycle() var newTagName by remember { mutableStateOf("") } val tagsWithCount by viewModel.allTagsWithCount.collectAsStateWithLifecycle() val context = LocalContext.current + val navigator = LocalNavigator.current val deeprQueries: DeeprQueries = koinInject() var isTagEditEnable by remember { mutableStateOf(null) } var isTagDeleteEnable by remember { mutableStateOf(null) } var tagEditError by remember { mutableStateOf(null) } - Column { - TopAppBar( - title = { - Row( - modifier = - Modifier - .fillMaxWidth() - .padding(end = 24.dp), - verticalAlignment = Alignment.CenterVertically, + Scaffold( + contentWindowInsets = windowInsets, + floatingActionButton = { + AnimatedVisibility( + selectedTag.isNotEmpty(), + enter = scaleIn(), + exit = scaleOut(), + ) { + ExtendedFloatingActionButton( + onClick = { + navigator.clearStackAndAdd(Dashboard2()) + }, + icon = { + Icon( + imageVector = TablerIcons.Eye, + contentDescription = "View Filtered Links", + ) + }, + text = { Text("View Filtered Links") }, + ) + } + }, + ) { paddingValues -> + Column( + modifier = + Modifier + .fillMaxSize() + .padding(paddingValues), + ) { + // Top Section - Create New Tag + Surface( + modifier = Modifier.fillMaxWidth(), + tonalElevation = 2.dp, + shape = RoundedCornerShape(topStart = 16.dp, topEnd = 16.dp), + ) { + Column( + modifier = Modifier.padding(16.dp), ) { - Text(stringResource(R.string.tags)) - - Spacer(modifier = Modifier.width(24.dp)) - - OutlinedTextField( - value = newTagName, - onValueChange = { newTagName = it }, - modifier = Modifier.weight(1f), - label = { Text(stringResource(R.string.new_tag)) }, - singleLine = true, - textStyle = MaterialTheme.typography.bodyLarge, - suffix = - if (newTagName.isNotBlank()) { - { - ClearInputIconButton( - onClick = { - newTagName = "" - }, - ) - } - } else { - null - }, + Text( + text = stringResource(R.string.tags), + style = MaterialTheme.typography.headlineMedium, + fontWeight = FontWeight.Bold, ) - Spacer(modifier = Modifier.width(8.dp)) + Spacer(modifier = Modifier.height(16.dp)) - FilledIconButton( - onClick = { - val trimmedTagName = newTagName.trim() - if (trimmedTagName.isNotBlank()) { - val existingTag = - tagsWithCount.find { - it.name.equals( - trimmedTagName, - ignoreCase = true, + Row( + modifier = Modifier.fillMaxWidth(), + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + OutlinedTextField( + value = newTagName, + onValueChange = { newTagName = it }, + modifier = Modifier.weight(1f), + label = { Text(stringResource(R.string.new_tag)) }, + placeholder = { Text("Enter tag name") }, + singleLine = true, + shape = RoundedCornerShape(12.dp), + trailingIcon = + if (newTagName.isNotBlank()) { + { + ClearInputIconButton( + onClick = { + newTagName = "" + }, ) } - - if (existingTag != null) { - Toast - .makeText( - context, - context.getString(R.string.tag_name_exists), - Toast.LENGTH_SHORT, - ).show() } else { - deeprQueries.insertTag(trimmedTagName) - newTagName = "" + null + }, + ) + + FilledIconButton( + onClick = { + val trimmedTagName = newTagName.trim() + if (trimmedTagName.isNotBlank()) { + val existingTag = + tagsWithCount.find { + it.name.equals( + trimmedTagName, + ignoreCase = true, + ) + } + + if (existingTag != null) { + Toast + .makeText( + context, + context.getString(R.string.tag_name_exists), + Toast.LENGTH_SHORT, + ).show() + } else { + deeprQueries.insertTag(trimmedTagName) + newTagName = "" + Toast + .makeText( + context, + "Tag created successfully", + Toast.LENGTH_SHORT, + ).show() + } + } + }, + enabled = newTagName.isNotBlank(), + modifier = Modifier.size(56.dp), + ) { + Icon( + imageVector = TablerIcons.Plus, + contentDescription = stringResource(R.string.create_tag), + ) + } + } + + // Show selected tags info + AnimatedVisibility(selectedTag.isNotEmpty()) { + Column { + Spacer(modifier = Modifier.height(12.dp)) + Card( + modifier = Modifier.fillMaxWidth(), + colors = + CardDefaults.cardColors( + containerColor = MaterialTheme.colorScheme.primaryContainer, + ), + shape = RoundedCornerShape(8.dp), + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(12.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically, + ) { + Text( + text = + stringResource( + R.string.selected_tags_count, + selectedTag.size, + ), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onPrimaryContainer, + ) + TextButton( + onClick = { viewModel.setTagFilter(null) }, + ) { + Text(stringResource(R.string.clear_all_filters)) + } } } - }, - enabled = newTagName.isNotBlank(), - shape = CircleShape, - ) { - Icon( - imageVector = TablerIcons.Plus, - contentDescription = stringResource(R.string.create_tag), - ) + } } } - }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - ) - - Spacer(modifier = Modifier.height(8.dp)) + } - HorizontalDivider() - LazyColumn { - // Show "Clear All Filters" option if any tags are selected - if (selectedTag.isNotEmpty()) { - item { - ListItem( + Surface { + // Tags List + if (tagsWithCount.isEmpty()) { + // Empty State + Box( modifier = - Modifier.clickable { - viewModel.setTagFilter(null) - }, - headlineContent = { + Modifier + .fillMaxSize() + .padding(32.dp), + contentAlignment = Alignment.Center, + ) { + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center, + ) { + Icon( + imageVector = TablerIcons.Tag, + contentDescription = null, + modifier = Modifier.size(64.dp), + tint = MaterialTheme.colorScheme.outline, + ) + Spacer(modifier = Modifier.height(16.dp)) Text( - stringResource(R.string.clear_all_filters), - color = MaterialTheme.colorScheme.error, + text = "No tags yet", + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.onSurfaceVariant, ) - }, - ) + Spacer(modifier = Modifier.height(8.dp)) + Text( + text = "Create your first tag to organize your links", + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.outline, + textAlign = TextAlign.Center, + ) + } + } + } else { + LazyColumn( + modifier = Modifier.fillMaxSize(), + contentPadding = + androidx.compose.foundation.layout + .PaddingValues(16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(tagsWithCount.sortedBy { it.name }) { tag -> + val isSelected = selectedTag.any { it.id == tag.id } + Card( + modifier = + Modifier + .fillMaxWidth() + .clickable { + viewModel.setTagFilter(Tags(tag.id, tag.name)) + }, + shape = RoundedCornerShape(12.dp), + colors = + CardDefaults.cardColors( + containerColor = + if (isSelected) { + MaterialTheme.colorScheme.surfaceContainerHighest + } else { + MaterialTheme.colorScheme.surfaceVariant + }, + ), + border = + if (isSelected) { + BorderStroke(1.dp, MaterialTheme.colorScheme.primary) + } else { + null + }, + ) { + Row( + modifier = + Modifier + .fillMaxWidth() + .padding(12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + androidx.compose.material3.Checkbox( + checked = isSelected, + onCheckedChange = { + viewModel.setTagFilter(Tags(tag.id, tag.name)) + }, + ) + + Spacer(modifier = Modifier.width(12.dp)) + + Column(modifier = Modifier.weight(1f)) { + Text( + text = tag.name, + style = MaterialTheme.typography.titleMedium, + fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal, + color = + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurface + }, + ) + Spacer(modifier = Modifier.height(4.dp)) + Text( + text = "${tag.linkCount} ${if (tag.linkCount == 1L) "link" else "links"}", + style = MaterialTheme.typography.bodySmall, + color = + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer.copy( + alpha = 0.7f, + ) + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ) + } + + Row( + horizontalArrangement = Arrangement.spacedBy(4.dp), + ) { + IconButton( + onClick = { isTagEditEnable = tag }, + colors = + IconButtonDefaults.iconButtonColors( + contentColor = + if (isSelected) { + MaterialTheme.colorScheme.onPrimaryContainer + } else { + MaterialTheme.colorScheme.onSurfaceVariant + }, + ), + ) { + Icon( + imageVector = TablerIcons.Edit, + contentDescription = stringResource(R.string.edit_tag_description), + modifier = Modifier.size(20.dp), + ) + } + + IconButton( + onClick = { isTagDeleteEnable = tag }, + colors = + IconButtonDefaults.iconButtonColors( + contentColor = MaterialTheme.colorScheme.error, + ), + ) { + Icon( + imageVector = TablerIcons.Trash, + contentDescription = stringResource(R.string.delete_tag_description), + modifier = Modifier.size(20.dp), + ) + } + } + } + } + } + } } } - item { - ListItem( - modifier = - Modifier.clickable { - // Don't dismiss, allow multi-selection - }, - headlineContent = { + isTagEditEnable?.let { tag -> + AlertDialog( + onDismissRequest = { + isTagEditEnable = null + tagEditError = null + }, + title = { Text( - if (selectedTag.isEmpty()) { - stringResource(R.string.all) - } else { - stringResource(R.string.selected_tags_count, selectedTag.size) - }, + text = stringResource(R.string.edit_tag), + style = MaterialTheme.typography.headlineSmall, ) }, - colors = ListItemDefaults.colors(containerColor = Color.Transparent), - ) - } + text = { + Column { + OutlinedTextField( + value = tag.name, + onValueChange = { + isTagEditEnable = tag.copy(name = it) + tagEditError = null + }, + modifier = Modifier.fillMaxWidth(), + label = { Text("Tag name") }, + singleLine = true, + isError = tagEditError != null, + supportingText = { + tagEditError?.let { + Text( + text = it, + color = MaterialTheme.colorScheme.error, + ) + } + }, + shape = RoundedCornerShape(12.dp), + trailingIcon = + if (isTagEditEnable?.name?.isNotBlank() == true) { + { + ClearInputIconButton( + onClick = { + isTagEditEnable = tag.copy(name = "") + }, + ) + } + } else { + null + }, + ) + } + }, + confirmButton = { + Button( + onClick = { + val trimmedName = isTagEditEnable?.name?.trim() ?: "" + if (trimmedName.isBlank()) { + tagEditError = "Tag name cannot be empty" + return@Button + } - items(tagsWithCount.sortedBy { it.name }) { tag -> - val isSelected = selectedTag.any { it.id == tag.id } - ListItem( - modifier = - Modifier.clickable { - viewModel.setTagFilter(Tags(tag.id, tag.name)) - // Don't dismiss to allow multi-selection - }, - leadingContent = { - androidx.compose.material3.Checkbox( - checked = isSelected, - onCheckedChange = { - viewModel.setTagFilter(Tags(tag.id, tag.name)) + val result = + runBlocking { + try { + viewModel.updateTag(Tags(tag.id, trimmedName)) + Result.success(true) + } catch (e: Exception) { + return@runBlocking Result.failure(e) + } + } + if (result.isFailure) { + val exception = result.exceptionOrNull() + tagEditError = + when (exception) { + is SQLiteConstraintException -> { + context.getString(R.string.tag_name_exists) + } + + else -> { + context.getString(R.string.failed_to_edit_tag) + } + } + } else { + isTagEditEnable = null + tagEditError = null + Toast + .makeText( + context, + context.getString(R.string.tag_edited_successfully), + Toast.LENGTH_SHORT, + ).show() + } }, - ) + enabled = isTagEditEnable?.name?.isNotBlank() == true, + ) { + Text(stringResource(R.string.edit)) + } }, - headlineContent = { Text("${tag.name} (${tag.linkCount})") }, - trailingContent = { - Row { - IconButton(onClick = { - isTagEditEnable = tag - }) { - Icon( - imageVector = TablerIcons.Edit, - contentDescription = stringResource(R.string.edit_tag_description), - ) - } - - IconButton(onClick = { - isTagDeleteEnable = tag - }) { - Icon( - imageVector = TablerIcons.Trash, - contentDescription = stringResource(R.string.delete_tag_description), - ) - } + dismissButton = { + TextButton(onClick = { + isTagEditEnable = null + tagEditError = null + }) { + Text(stringResource(R.string.cancel)) } }, - colors = - if (isSelected) { - ListItemDefaults.colors( - headlineColor = MaterialTheme.colorScheme.primary, - ) - } else { - ListItemDefaults.colors(containerColor = Color.Transparent) - }, ) } - } - } - isTagEditEnable?.let { tag -> - AlertDialog( - onDismissRequest = { - isTagEditEnable = null - tagEditError = null - }, - title = { - Text(text = stringResource(R.string.edit_tag)) - }, - text = { - Column { - TextField( - value = tag.name, - onValueChange = { - isTagEditEnable = tag.copy(name = it) - }, - isError = tagEditError != null, - supportingText = { - tagEditError?.let { - Text(text = it) - } - }, - suffix = - if (isTagEditEnable?.name.isNullOrEmpty()) { - null - } else { - { - ClearInputIconButton( - onClick = { - isTagEditEnable = tag.copy(name = "") - }, - ) - } - }, - ) - } - }, - confirmButton = { - Button(onClick = { - val result = - runBlocking { - try { - viewModel.updateTag(Tags(tag.id, tag.name)) - Result.success(true) - } catch (e: Exception) { - return@runBlocking Result.failure(e) - } - } - if (result.isFailure) { - val exception = result.exceptionOrNull() - tagEditError = - when (exception) { - is SQLiteConstraintException -> { - context.getString(R.string.tag_name_exists) + isTagDeleteEnable?.let { tag -> + AlertDialog( + onDismissRequest = { + isTagDeleteEnable = null + }, + icon = { + Icon( + imageVector = TablerIcons.Trash, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + modifier = Modifier.size(32.dp), + ) + }, + title = { + Text( + text = stringResource(R.string.delete_tag), + style = MaterialTheme.typography.headlineSmall, + ) + }, + text = { + Column { + val message = + buildAnnotatedString { + append("Are you sure you want to delete ") + withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { + append("'${tag.name}'") + } + append(" tag?") } + Text(text = message) - else -> { - context.getString(R.string.failed_to_edit_tag) + if (tag.linkCount > 0) { + Spacer(modifier = Modifier.height(8.dp)) + Card( + colors = + CardDefaults.cardColors( + containerColor = + MaterialTheme.colorScheme.errorContainer.copy( + alpha = 0.3f, + ), + ), + ) { + Text( + text = "This tag is used by ${tag.linkCount} ${if (tag.linkCount == 1L) "link" else "links"}", + modifier = Modifier.padding(12.dp), + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onErrorContainer, + ) } } - } else { - isTagEditEnable = null - tagEditError = null - Toast - .makeText( - context, - context.getString(R.string.tag_edited_successfully), - Toast.LENGTH_SHORT, - ).show() - } - }) { - Text(stringResource(R.string.edit)) - } - }, - dismissButton = { - Button(onClick = { - isTagEditEnable = null - tagEditError = null - }) { - Text(stringResource(R.string.cancel)) - } - }, - ) - } - - isTagDeleteEnable?.let { tag -> - AlertDialog( - onDismissRequest = { - isTagDeleteEnable = null - }, - title = { - Text(text = stringResource(R.string.delete_tag)) - }, - text = { - val message = - buildAnnotatedString { - append("Are you sure you want to delete ") - withStyle(style = SpanStyle(fontWeight = FontWeight.Bold)) { - append("'${tag.name}'") } - append(" tag?") - } - Text(text = message) - }, - confirmButton = { - Button( - onClick = { - viewModel.deleteTag(tag.id) + }, + confirmButton = { + Button( + onClick = { + viewModel.deleteTag(tag.id) - isTagDeleteEnable = null - Toast - .makeText( - context, - context.getString(R.string.tag_deleted_successfully), - Toast.LENGTH_SHORT, - ).show() + isTagDeleteEnable = null + Toast + .makeText( + context, + context.getString(R.string.tag_deleted_successfully), + Toast.LENGTH_SHORT, + ).show() + }, + colors = + ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.error, + contentColor = MaterialTheme.colorScheme.onError, + ), + ) { + Text(stringResource(R.string.delete)) + } }, - colors = - ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.errorContainer, - contentColor = MaterialTheme.colorScheme.onErrorContainer, - ), - ) { - Text(stringResource(R.string.delete)) - } - }, - dismissButton = { - OutlinedButton(onClick = { - isTagDeleteEnable = null - }) { - Text(stringResource(R.string.cancel)) - } - }, - ) + dismissButton = { + TextButton(onClick = { + isTagDeleteEnable = null + }) { + Text(stringResource(R.string.cancel)) + } + }, + ) + } + } } } }