From 1fe3cc98c63e4762f9851ca0d5050a5f60866a3b Mon Sep 17 00:00:00 2001 From: Arena Agent Date: Sun, 18 Oct 2026 06:05:14 +0000 Subject: [PATCH] fix(ui): stop bottom bar covering the screen, show tabs on Search/Profile, sky-blue #0494F4 theme Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- .../store/data/settings/SettingsRepository.kt | 28 ++ .../com/gothwad/store/ui/StoreViewModel.kt | 2 + .../com/gothwad/store/ui/components/Chrome.kt | 8 +- .../com/gothwad/store/ui/components/Common.kt | 14 +- .../com/gothwad/store/ui/components/Rows.kt | 13 +- .../gothwad/store/ui/navigation/AppNavHost.kt | 24 +- .../store/ui/screens/AccountScreens.kt | 462 +++++++++--------- .../gothwad/store/ui/screens/SearchScreen.kt | 228 ++++++--- .../java/com/gothwad/store/ui/theme/Color.kt | 40 +- .../java/com/gothwad/store/ui/theme/Theme.kt | 22 +- app/src/main/res/values-night/themes.xml | 2 +- app/src/main/res/values/colors.xml | 2 +- app/src/main/res/values/themes.xml | 2 +- task.md | 1 + 14 files changed, 491 insertions(+), 357 deletions(-) diff --git a/app/src/main/java/com/gothwad/store/data/settings/SettingsRepository.kt b/app/src/main/java/com/gothwad/store/data/settings/SettingsRepository.kt index 5b2f82a..42634fc 100644 --- a/app/src/main/java/com/gothwad/store/data/settings/SettingsRepository.kt +++ b/app/src/main/java/com/gothwad/store/data/settings/SettingsRepository.kt @@ -76,6 +76,8 @@ data class Settings( val lastUpdateCheck: Long = 0, val onboardingDone: Boolean = false, val searchHistory: List = emptyList(), + /** App ids the user opened, newest first. Shown at the bottom of Search. */ + val recentlyViewed: List = emptyList(), ) private val Context.settingStore: DataStore by preferencesDataStore("gothwad_store_settings") @@ -116,6 +118,7 @@ class SettingsRepository(private val context: Context) { // set cannot do that, so the terms live in one newline separated value. val searchHistory = stringPreferencesKey("search_history_ordered") val searchHistoryLegacy = stringSetPreferencesKey("search_history") + val recentlyViewed = stringPreferencesKey("recently_viewed_apps") } private val scope = CoroutineScope(SupervisorJob() + Dispatchers.IO) @@ -152,6 +155,7 @@ class SettingsRepository(private val context: Context) { lastUpdateCheck = prefs[Keys.lastUpdateCheck] ?: 0L, onboardingDone = prefs[Keys.onboarding] ?: false, searchHistory = readSearchHistory(prefs), + recentlyViewed = readRecentlyViewed(prefs), ) }.stateIn(scope, SharingStarted.Eagerly, null) @@ -205,6 +209,30 @@ class SettingsRepository(private val context: Context) { prefs.remove(Keys.searchHistoryLegacy) } + /** Records an opened app id so Search can offer "Recently viewed". */ + suspend fun addRecentlyViewed(appId: String) = edit { prefs -> + val clean = appId.trim() + if (clean.isEmpty()) return@edit + val current = readRecentlyViewed(prefs).toMutableList() + current.remove(clean) + current.add(0, clean) + prefs[Keys.recentlyViewed] = current.take(10).joinToString("\n") + } + + suspend fun clearRecentlyViewed() = edit { prefs -> + prefs.remove(Keys.recentlyViewed) + } + + /** Newest first, capped to the ten ids the UI may ever need. */ + private fun readRecentlyViewed(prefs: androidx.datastore.preferences.core.Preferences): List = + prefs[Keys.recentlyViewed] + ?.split('\n') + ?.map { it.trim() } + ?.filter { it.isNotEmpty() } + ?.distinct() + ?.take(10) + .orEmpty() + /** Newest first; falls back to the old unordered set for existing installs. */ private fun readSearchHistory(prefs: androidx.datastore.preferences.core.Preferences): List { val ordered = prefs[Keys.searchHistory] diff --git a/app/src/main/java/com/gothwad/store/ui/StoreViewModel.kt b/app/src/main/java/com/gothwad/store/ui/StoreViewModel.kt index 506b4a1..28e68a2 100644 --- a/app/src/main/java/com/gothwad/store/ui/StoreViewModel.kt +++ b/app/src/main/java/com/gothwad/store/ui/StoreViewModel.kt @@ -177,6 +177,8 @@ class StoreViewModel : ViewModel() { fun openApp(id: String) { detailJob?.cancel() _detail.value = UiState.Loading + // Powers the "Recently viewed" shelf at the bottom of Search. + viewModelScope.launch { settings.addRecentlyViewed(id) } detailJob = viewModelScope.launch { _detail.value = runCatching { repository.detail(id) }.fold( onSuccess = { detail -> diff --git a/app/src/main/java/com/gothwad/store/ui/components/Chrome.kt b/app/src/main/java/com/gothwad/store/ui/components/Chrome.kt index 2bbe159..70f964a 100644 --- a/app/src/main/java/com/gothwad/store/ui/components/Chrome.kt +++ b/app/src/main/java/com/gothwad/store/ui/components/Chrome.kt @@ -14,7 +14,6 @@ import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height -import androidx.compose.foundation.layout.heightIn import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size @@ -300,8 +299,11 @@ fun GothwadBottomBar( Row( modifier = Modifier .fillMaxWidth() + // Fixed height on purpose: the tab items use fillMaxHeight(), so + // a heightIn() row would size them to the Scaffold's maximum + // (the whole screen) and the bar would cover the header/content. .windowInsetsPadding(WindowInsets.navigationBars) - .heightIn(min = 80.dp) + .height(72.dp) .testTag("bottom_bar"), verticalAlignment = Alignment.CenterVertically, ) { @@ -340,7 +342,7 @@ private fun BottomTabItem( .padding(vertical = 6.dp) .testTag("tab_${spec.route}"), horizontalAlignment = Alignment.CenterHorizontally, - verticalArrangement = Arrangement.spacedBy(3.dp), + verticalArrangement = Arrangement.spacedBy(3.dp, Alignment.CenterVertically), ) { Box( modifier = Modifier diff --git a/app/src/main/java/com/gothwad/store/ui/components/Common.kt b/app/src/main/java/com/gothwad/store/ui/components/Common.kt index 7e91269..9cda313 100644 --- a/app/src/main/java/com/gothwad/store/ui/components/Common.kt +++ b/app/src/main/java/com/gothwad/store/ui/components/Common.kt @@ -22,6 +22,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CloudOff import androidx.compose.material.icons.filled.ErrorOutline +import androidx.compose.material.icons.filled.Info import androidx.compose.material.icons.filled.InstallMobile import androidx.compose.material.icons.filled.Star import androidx.compose.material3.Button @@ -139,8 +140,8 @@ fun VerifiedBadge() = PillLabel( @Composable fun UpdateBadge() = PillLabel( text = "Update", - background = PlayColors.BlueLight, - contentColor = PlayColors.Blue, + background = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, icon = Icons.Filled.InstallMobile, ) @@ -242,7 +243,7 @@ fun PlayChip( label = "chipBorder", ) val background = animateColorAsState( - targetValue = if (selected) GothwadTheme.extra.successContainer else Color.Transparent, + targetValue = if (selected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent, label = "chipBackground", ) Row( @@ -287,7 +288,7 @@ fun SegmentedRow( modifier = Modifier .weight(1f) .clip(RoundedCornerShape(50)) - .background(if (selected) GothwadTheme.extra.successContainer else Color.Transparent) + .background(if (selected) MaterialTheme.colorScheme.primaryContainer else Color.Transparent) .clickable { onSelect(index) } .padding(vertical = 9.dp) .then(Modifier), @@ -378,15 +379,16 @@ fun InfoBanner( action: (@Composable () -> Unit)? = null, ) { val (container, accent) = when (tone) { - BannerTone.Info -> GothwadTheme.extra.successContainer to GothwadTheme.extra.success + BannerTone.Info -> MaterialTheme.colorScheme.primaryContainer to MaterialTheme.colorScheme.primary BannerTone.Warning -> GothwadTheme.extra.warningContainer to GothwadTheme.extra.warning BannerTone.Error -> PlayColors.ErrorContainer to MaterialTheme.colorScheme.error BannerTone.Success -> GothwadTheme.extra.successContainer to GothwadTheme.extra.success } val icon = when (tone) { + BannerTone.Info -> Icons.Filled.Info BannerTone.Error -> Icons.Filled.ErrorOutline BannerTone.Warning -> Icons.Filled.CloudOff - else -> Icons.Filled.CheckCircle + BannerTone.Success -> Icons.Filled.CheckCircle } Row( modifier = modifier diff --git a/app/src/main/java/com/gothwad/store/ui/components/Rows.kt b/app/src/main/java/com/gothwad/store/ui/components/Rows.kt index d35d05b..b6dc24b 100644 --- a/app/src/main/java/com/gothwad/store/ui/components/Rows.kt +++ b/app/src/main/java/com/gothwad/store/ui/components/Rows.kt @@ -45,10 +45,9 @@ import com.gothwad.store.data.model.StoreApp import com.gothwad.store.data.model.StoreRelease import com.gothwad.store.ui.InstallAction import com.gothwad.store.ui.theme.GothwadTheme -import com.gothwad.store.ui.theme.PlayColors /** - * The Play-store style install control: a green rounded button that turns into a + * The Play-store style install control: a brand-blue rounded button that turns into a * progress ring while downloading, then into "Open" once installed. */ @Composable @@ -72,7 +71,7 @@ fun InstallButton( Modifier .size(38.dp) .clip(CircleShape) - .background(GothwadTheme.extra.successContainer) + .background(MaterialTheme.colorScheme.primaryContainer) .clickable(onClick = onClick), contentAlignment = Alignment.Center, ) { @@ -92,7 +91,7 @@ fun InstallButton( Modifier .size(38.dp) .clip(CircleShape) - .background(GothwadTheme.extra.successContainer), + .background(MaterialTheme.colorScheme.primaryContainer), contentAlignment = Alignment.Center, ) { ProgressRing(progress = action.progress.coerceAtLeast(0.02f)) @@ -104,7 +103,7 @@ fun InstallButton( Modifier .size(38.dp) .clip(CircleShape) - .background(GothwadTheme.extra.successContainer), + .background(MaterialTheme.colorScheme.primaryContainer), contentAlignment = Alignment.Center, ) { ProgressRing(progress = 0.02f) @@ -203,7 +202,7 @@ fun ProgressRing(progress: Float, modifier: Modifier = Modifier) { progress = { progress.coerceIn(0f, 1f) }, modifier = modifier.size(38.dp), color = MaterialTheme.colorScheme.primary, - trackColor = GothwadTheme.extra.successContainer, + trackColor = MaterialTheme.colorScheme.primaryContainer, strokeWidth = 2.5.dp, ) } @@ -432,7 +431,7 @@ fun BannerCard( .clip(RoundedCornerShape(18.dp)) .background( Brush.linearGradient( - listOf(PlayColors.BannerStart, PlayColors.BannerEnd), + listOf(GothwadTheme.extra.bannerStart, GothwadTheme.extra.bannerEnd), ), ) .padding(18.dp), diff --git a/app/src/main/java/com/gothwad/store/ui/navigation/AppNavHost.kt b/app/src/main/java/com/gothwad/store/ui/navigation/AppNavHost.kt index 49cad43..8d54fb5 100644 --- a/app/src/main/java/com/gothwad/store/ui/navigation/AppNavHost.kt +++ b/app/src/main/java/com/gothwad/store/ui/navigation/AppNavHost.kt @@ -73,8 +73,9 @@ import com.gothwad.store.ui.screens.WishlistScreen import com.gothwad.store.ui.screens.WriteReviewScreen /** - * One NavHost for the whole app. The four tab destinations render the shared - * Play-style bottom bar; every other destination is pushed with a back arrow. + * One NavHost for the whole app. The five tab destinations (Home, Apps, + * Search, Games, Profile) render the shared bottom bar; every other + * destination is pushed as a sub-screen with a back arrow. */ @Composable fun AppNavHost( @@ -87,14 +88,13 @@ fun AppNavHost( val currentRoute = backStackEntry?.destination?.route.orEmpty() val onTab: (String) -> Unit = { route -> - if (route == Routes.SEARCH) { - navController.navigate(Routes.SEARCH) { launchSingleTop = true } - } else { - navController.navigate(route) { - popUpTo(Routes.HOME) { saveState = true } - launchSingleTop = true - restoreState = true - } + // Every tab (Search included) is a top-level destination, so they all + // use the same save/restore rules. Search used to be special-cased and + // was not wrapped in TabShell, which is why it had no bottom bar. + navController.navigate(route) { + popUpTo(Routes.HOME) { saveState = true } + launchSingleTop = true + restoreState = true } } @@ -124,7 +124,9 @@ fun AppNavHost( composable(Routes.ACCOUNT) { TabShell(currentRoute, onTab, snackbarHostState) { AccountScreen(vm, navController) } } - composable(Routes.SEARCH) { SearchScreen(vm, navController) } + composable(Routes.SEARCH) { + TabShell(currentRoute, onTab, snackbarHostState) { SearchScreen(vm, navController) } + } // ── Catalogue ─────────────────────────────────────────────────── composable( diff --git a/app/src/main/java/com/gothwad/store/ui/screens/AccountScreens.kt b/app/src/main/java/com/gothwad/store/ui/screens/AccountScreens.kt index 9c54e3e..00cafd6 100644 --- a/app/src/main/java/com/gothwad/store/ui/screens/AccountScreens.kt +++ b/app/src/main/java/com/gothwad/store/ui/screens/AccountScreens.kt @@ -37,6 +37,7 @@ import androidx.compose.material.icons.filled.SystemUpdate import androidx.compose.material.icons.filled.Tune import androidx.compose.material.icons.filled.Wifi import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable @@ -66,6 +67,9 @@ import com.gothwad.store.ui.StoreEvent import com.gothwad.store.ui.StoreViewModel import com.gothwad.store.ui.components.BannerTone import com.gothwad.store.ui.components.BrandAvatar +import com.gothwad.store.ui.components.DownloadsAction +import com.gothwad.store.ui.components.GothwadScaffold +import com.gothwad.store.ui.components.GothwadTopBar import com.gothwad.store.ui.components.InfoBanner import com.gothwad.store.ui.components.KeyValueRow import com.gothwad.store.ui.components.OutlineButton @@ -94,242 +98,264 @@ fun AccountScreen(vm: StoreViewModel, nav: NavController) { val user = (authState as? AuthState.SignedIn)?.user - LazyColumn( - modifier = Modifier.fillMaxSize(), - contentPadding = PaddingValues(bottom = Spacing.bottomContentPadding), - ) { - item { - ProfileCard( - name = user?.displayName ?: settings?.localDisplayName?.takeIf { it.isNotBlank() } ?: "Not signed in", - handle = user?.let { if (it.username.isNotBlank()) "@${it.username}" else it.email } - ?: "Sign in with Gothwad for cloud library and reviews", - picture = user?.picture, - signedIn = user != null, - onSignIn = { nav.navigate(Routes.SIGN_IN) }, - onManage = { nav.navigate(Routes.ACCOUNT_DETAILS) }, + // Profile is a bottom-tab destination, so it needs the shared Scaffold: + // without it the content started under the status bar and the bottom bar + // was missing entirely. + GothwadScaffold( + topBar = { + GothwadTopBar( + title = "Profile", + showBrandMark = true, + actions = { + DownloadsAction { nav.navigate(Routes.DOWNLOADS) } + IconButton(onClick = { nav.navigate(Routes.SETTINGS) }) { + Icon( + Icons.Filled.Settings, + contentDescription = "Settings", + tint = GothwadTheme.extra.textSecondary, + ) + } + }, ) - } - - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = Spacing.screenH, vertical = 6.dp) - .clip(RoundedCornerShape(18.dp)) - .background(MaterialTheme.colorScheme.surface) - .clickable { nav.navigate(Routes.MY_APPS) } - .padding(16.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - ) { - StatTile( - label = "Installed", - value = libraryState.installed.size.toString(), - modifier = Modifier.weight(1f), - ) - StatTile( - label = "Wishlist", - value = libraryState.wishlist.size.toString(), - modifier = Modifier.weight(1f), - ) - StatTile( - label = "Downloads", - value = tasks.count { it.isActiveRunning() }.toString(), - modifier = Modifier.weight(1f), - ) - } - } - - item { SectionHeader(title = "Your content") } - item { - SettingsGroup { - SettingRow( - title = "My apps & device", - subtitle = "Updates, library and everything installed", - icon = Icons.Filled.SystemUpdate, - onClick = { nav.navigate(Routes.MY_APPS) }, - ) - SettingRow( - title = "Wishlist", - subtitle = "${libraryState.wishlist.size} saved app(s)", - icon = Icons.Filled.BookmarkBorder, - onClick = { nav.navigate(Routes.WISHLIST) }, - ) - SettingRow( - title = "Manage downloads", - subtitle = "${tasks.size} job(s) · ${Format.bytes(AppGraph.downloader.usedBytes())} on disk", - icon = Icons.Filled.Download, - onClick = { nav.navigate(Routes.DOWNLOADS) }, - ) - SettingRow( - title = "Download history", - subtitle = "Finished, cancelled and failed APKs", - icon = Icons.Filled.Storage, - onClick = { nav.navigate(Routes.DOWNLOAD_HISTORY) }, - ) - SettingRow( - title = "On this device", - subtitle = "Every package Android reports", - icon = Icons.Filled.PhoneAndroid, - onClick = { nav.navigate(Routes.ON_DEVICE) }, + }, + ) { padding -> + LazyColumn( + modifier = Modifier.fillMaxSize().padding(padding), + contentPadding = PaddingValues(bottom = Spacing.bottomContentPadding), + ) { + item { + ProfileCard( + name = user?.displayName ?: settings?.localDisplayName?.takeIf { it.isNotBlank() } ?: "Not signed in", + handle = user?.let { if (it.username.isNotBlank()) "@${it.username}" else it.email } + ?: "Sign in with Gothwad for cloud library and reviews", + picture = user?.picture, + signedIn = user != null, + onSignIn = { nav.navigate(Routes.SIGN_IN) }, + onManage = { nav.navigate(Routes.ACCOUNT_DETAILS) }, ) } - } - item { SectionHeader(title = "Gothwad") } - item { - SettingsGroup { - SettingRow( - title = "Gothwad Accounts", - subtitle = "Profile, password and connected devices", - icon = Icons.Filled.ManageAccounts, - onClick = { nav.navigate(Routes.ACCOUNT_DETAILS) }, - ) - SettingRow( - title = "Developer Console", - subtitle = "Publish and manage your own apps at console.gothwadtech.com", - icon = Icons.Filled.Dashboard, - onClick = { nav.navigate(Routes.DEVELOPER_CONSOLE) }, - trailingText = "Open", - ) - SettingRow( - title = "Sign-in security", - subtitle = "Sessions, tokens and revoking access", - icon = Icons.Filled.Lock, - onClick = { nav.navigate(Routes.ACCOUNT_SECURITY) }, - ) + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Spacing.screenH, vertical = 6.dp) + .clip(RoundedCornerShape(18.dp)) + .background(MaterialTheme.colorScheme.surface) + .clickable { nav.navigate(Routes.MY_APPS) } + .padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + ) { + StatTile( + label = "Installed", + value = libraryState.installed.size.toString(), + modifier = Modifier.weight(1f), + ) + StatTile( + label = "Wishlist", + value = libraryState.wishlist.size.toString(), + modifier = Modifier.weight(1f), + ) + StatTile( + label = "Downloads", + value = tasks.count { it.isActiveRunning() }.toString(), + modifier = Modifier.weight(1f), + ) + } } - } - item { SectionHeader(title = "Settings") } - item { - SettingsGroup { - SettingRow( - title = "General", - subtitle = "Theme, layout and charts", - icon = Icons.Filled.Settings, - onClick = { nav.navigate(Routes.SETTINGS) }, - ) - SettingRow( - title = "Appearance", - subtitle = "Dark mode, theme mode and list density", - icon = Icons.Filled.Palette, - onClick = { nav.navigate(Routes.APPEARANCE) }, - ) - SettingRow( - title = "Notifications", - subtitle = "Download, install and update alerts", - icon = Icons.Filled.Notifications, - onClick = { nav.navigate(Routes.NOTIFICATIONS) }, - ) - SettingRow( - title = "Network & data", - subtitle = "Wi-Fi only, data saver and retries", - icon = Icons.Filled.Wifi, - onClick = { nav.navigate(Routes.NETWORK) }, - ) - SettingRow( - title = "Download preferences", - subtitle = "Checksums, cleanup and parallel downloads", - icon = Icons.Filled.Tune, - onClick = { nav.navigate(Routes.DOWNLOAD_SETTINGS) }, - ) - SettingRow( - title = "Installer", - subtitle = "Install permission, Shizuku, root and ADB", - icon = Icons.Filled.Security, - onClick = { nav.navigate(Routes.INSTALL_SETTINGS) }, - ) - SettingRow( - title = "Storage", - subtitle = "APKs kept and cleanup tools", - icon = Icons.Filled.Storage, - onClick = { nav.navigate(Routes.STORAGE) }, - ) - SettingRow( - title = "Privacy", - subtitle = "What Kiwi Store collects and stores", - icon = Icons.Filled.PrivacyTip, - onClick = { nav.navigate(Routes.PRIVACY) }, - ) - SettingRow( - title = "Advanced", - subtitle = "Cache, API host and diagnostics", - icon = Icons.Filled.Tune, - onClick = { nav.navigate(Routes.ADVANCED) }, - ) - SettingRow( - title = "About this device", - subtitle = "Android version, ABI and app id", - icon = Icons.Filled.PhoneAndroid, - onClick = { nav.navigate(Routes.DEVICE_INFO) }, - ) + item { SectionHeader(title = "Your content") } + item { + SettingsGroup { + SettingRow( + title = "My apps & device", + subtitle = "Updates, library and everything installed", + icon = Icons.Filled.SystemUpdate, + onClick = { nav.navigate(Routes.MY_APPS) }, + ) + SettingRow( + title = "Wishlist", + subtitle = "${libraryState.wishlist.size} saved app(s)", + icon = Icons.Filled.BookmarkBorder, + onClick = { nav.navigate(Routes.WISHLIST) }, + ) + SettingRow( + title = "Manage downloads", + subtitle = "${tasks.size} job(s) · ${Format.bytes(AppGraph.downloader.usedBytes())} on disk", + icon = Icons.Filled.Download, + onClick = { nav.navigate(Routes.DOWNLOADS) }, + ) + SettingRow( + title = "Download history", + subtitle = "Finished, cancelled and failed APKs", + icon = Icons.Filled.Storage, + onClick = { nav.navigate(Routes.DOWNLOAD_HISTORY) }, + ) + SettingRow( + title = "On this device", + subtitle = "Every package Android reports", + icon = Icons.Filled.PhoneAndroid, + onClick = { nav.navigate(Routes.ON_DEVICE) }, + ) + } } - } - item { SectionHeader(title = "Support") } - item { - SettingsGroup { - SettingRow( - title = "Help & troubleshoot", - subtitle = "Installs, downloads and account problems", - icon = Icons.Filled.HelpOutline, - onClick = { nav.navigate(Routes.HELP) }, - ) - SettingRow( - title = "Send feedback", - subtitle = "Tell the Gothwad team what to fix", - icon = Icons.Filled.Feedback, - onClick = { nav.navigate(Routes.FEEDBACK) }, - ) - SettingRow( - title = "About Kiwi Store", - subtitle = "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", - icon = Icons.Filled.Info, - onClick = { nav.navigate(Routes.ABOUT) }, - ) - SettingRow( - title = "Terms & licences", - subtitle = "Legal notices and open-source licences", - icon = Icons.Filled.AccountCircle, - onClick = { nav.navigate(Routes.LEGAL) }, - ) + item { SectionHeader(title = "Gothwad") } + item { + SettingsGroup { + SettingRow( + title = "Gothwad Accounts", + subtitle = "Profile, password and connected devices", + icon = Icons.Filled.ManageAccounts, + onClick = { nav.navigate(Routes.ACCOUNT_DETAILS) }, + ) + SettingRow( + title = "Developer Console", + subtitle = "Publish and manage your own apps at console.gothwadtech.com", + icon = Icons.Filled.Dashboard, + onClick = { nav.navigate(Routes.DEVELOPER_CONSOLE) }, + trailingText = "Open", + ) + SettingRow( + title = "Sign-in security", + subtitle = "Sessions, tokens and revoking access", + icon = Icons.Filled.Lock, + onClick = { nav.navigate(Routes.ACCOUNT_SECURITY) }, + ) + } } - } - if (user != null) { + item { SectionHeader(title = "Settings") } item { - Column(Modifier.padding(Spacing.screenH)) { - Spacer(Modifier.height(8.dp)) - OutlineButton( - label = "Sign out of this device", - icon = Icons.Filled.Logout, - onClick = { - scope.launch { - AppGraph.auth.signOut() - vm.emit(StoreEvent.Message("Signed out. Your local library stays on this device.")) - } - }, - modifier = Modifier.fillMaxWidth(), + SettingsGroup { + SettingRow( + title = "General", + subtitle = "Theme, layout and charts", + icon = Icons.Filled.Settings, + onClick = { nav.navigate(Routes.SETTINGS) }, ) - Spacer(Modifier.height(14.dp)) - Text( - "Kiwi Store ${BuildConfig.VERSION_NAME} · signed in as ${user.username.ifBlank { user.id }}", - fontSize = 11.sp, - color = GothwadTheme.extra.textTertiary, + SettingRow( + title = "Appearance", + subtitle = "Dark mode, theme mode and list density", + icon = Icons.Filled.Palette, + onClick = { nav.navigate(Routes.APPEARANCE) }, + ) + SettingRow( + title = "Notifications", + subtitle = "Download, install and update alerts", + icon = Icons.Filled.Notifications, + onClick = { nav.navigate(Routes.NOTIFICATIONS) }, + ) + SettingRow( + title = "Network & data", + subtitle = "Wi-Fi only, data saver and retries", + icon = Icons.Filled.Wifi, + onClick = { nav.navigate(Routes.NETWORK) }, + ) + SettingRow( + title = "Download preferences", + subtitle = "Checksums, cleanup and parallel downloads", + icon = Icons.Filled.Tune, + onClick = { nav.navigate(Routes.DOWNLOAD_SETTINGS) }, + ) + SettingRow( + title = "Installer", + subtitle = "Install permission, Shizuku, root and ADB", + icon = Icons.Filled.Security, + onClick = { nav.navigate(Routes.INSTALL_SETTINGS) }, + ) + SettingRow( + title = "Storage", + subtitle = "APKs kept and cleanup tools", + icon = Icons.Filled.Storage, + onClick = { nav.navigate(Routes.STORAGE) }, + ) + SettingRow( + title = "Privacy", + subtitle = "What Kiwi Store collects and stores", + icon = Icons.Filled.PrivacyTip, + onClick = { nav.navigate(Routes.PRIVACY) }, + ) + SettingRow( + title = "Advanced", + subtitle = "Cache, API host and diagnostics", + icon = Icons.Filled.Tune, + onClick = { nav.navigate(Routes.ADVANCED) }, + ) + SettingRow( + title = "About this device", + subtitle = "Android version, ABI and app id", + icon = Icons.Filled.PhoneAndroid, + onClick = { nav.navigate(Routes.DEVICE_INFO) }, ) } } - } else { + + item { SectionHeader(title = "Support") } item { - Column(Modifier.padding(Spacing.screenH)) { - Spacer(Modifier.height(8.dp)) - OutlineButton( - label = "Not signed in — tap to sign in", - onClick = { nav.navigate(Routes.SIGN_IN) }, - modifier = Modifier.fillMaxWidth(), + SettingsGroup { + SettingRow( + title = "Help & troubleshoot", + subtitle = "Installs, downloads and account problems", + icon = Icons.Filled.HelpOutline, + onClick = { nav.navigate(Routes.HELP) }, + ) + SettingRow( + title = "Send feedback", + subtitle = "Tell the Gothwad team what to fix", + icon = Icons.Filled.Feedback, + onClick = { nav.navigate(Routes.FEEDBACK) }, + ) + SettingRow( + title = "About Kiwi Store", + subtitle = "Version ${BuildConfig.VERSION_NAME} (${BuildConfig.VERSION_CODE})", + icon = Icons.Filled.Info, + onClick = { nav.navigate(Routes.ABOUT) }, + ) + SettingRow( + title = "Terms & licences", + subtitle = "Legal notices and open-source licences", + icon = Icons.Filled.AccountCircle, + onClick = { nav.navigate(Routes.LEGAL) }, ) } } + + if (user != null) { + item { + Column(Modifier.padding(Spacing.screenH)) { + Spacer(Modifier.height(8.dp)) + OutlineButton( + label = "Sign out of this device", + icon = Icons.Filled.Logout, + onClick = { + scope.launch { + AppGraph.auth.signOut() + vm.emit(StoreEvent.Message("Signed out. Your local library stays on this device.")) + } + }, + modifier = Modifier.fillMaxWidth(), + ) + Spacer(Modifier.height(14.dp)) + Text( + "Kiwi Store ${BuildConfig.VERSION_NAME} · signed in as ${user.username.ifBlank { user.id }}", + fontSize = 11.sp, + color = GothwadTheme.extra.textTertiary, + ) + } + } + } else { + item { + Column(Modifier.padding(Spacing.screenH)) { + Spacer(Modifier.height(8.dp)) + OutlineButton( + label = "Not signed in — tap to sign in", + onClick = { nav.navigate(Routes.SIGN_IN) }, + modifier = Modifier.fillMaxWidth(), + ) + } + } + } } } } @@ -358,7 +384,7 @@ private fun ProfileCard( modifier = Modifier .size(62.dp) .clip(RoundedCornerShape(20.dp)) - .background(GothwadTheme.extra.successContainer), + .background(MaterialTheme.colorScheme.primaryContainer), contentAlignment = Alignment.Center, ) { if (picture != null) { diff --git a/app/src/main/java/com/gothwad/store/ui/screens/SearchScreen.kt b/app/src/main/java/com/gothwad/store/ui/screens/SearchScreen.kt index d6040d7..6f280fa 100644 --- a/app/src/main/java/com/gothwad/store/ui/screens/SearchScreen.kt +++ b/app/src/main/java/com/gothwad/store/ui/screens/SearchScreen.kt @@ -1,13 +1,12 @@ package com.gothwad.store.ui.screens +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.PaddingValues import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyRow @@ -20,7 +19,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text import androidx.compose.runtime.Composable -import androidx.compose.foundation.clickable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -31,15 +29,15 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp import androidx.lifecycle.compose.collectAsStateWithLifecycle -import kotlinx.coroutines.launch import androidx.navigation.NavController +import kotlinx.coroutines.launch import com.gothwad.store.core.AppGraph -import com.gothwad.store.core.Format +import com.gothwad.store.data.model.StoreApp +import com.gothwad.store.data.model.StoreRelease import com.gothwad.store.ui.Routes import com.gothwad.store.ui.StoreViewModel import com.gothwad.store.ui.UiState import com.gothwad.store.ui.components.AppRowItem -import com.gothwad.store.ui.components.AppIcon import com.gothwad.store.ui.components.DividerLine import com.gothwad.store.ui.components.EmptyState import com.gothwad.store.ui.components.ErrorState @@ -51,12 +49,21 @@ import com.gothwad.store.ui.components.SkeletonList import com.gothwad.store.ui.theme.GothwadTheme import com.gothwad.store.ui.theme.Spacing +/** + * Search is a bottom-tab destination, so it uses the shared [GothwadScaffold]: + * the tab bar stays visible while the search field and results scroll. + * + * Idle screen order (top → bottom): recent searches, five suggested apps, then + * "Recently viewed" at the very bottom — exactly the Play-style stacking. + */ @Composable fun SearchScreen(vm: StoreViewModel, nav: NavController) { var query by rememberSaveable { mutableStateOf("") } val results by vm.searchResults.collectAsStateWithLifecycle() val releases by vm.releases.collectAsStateWithLifecycle() val home by vm.home.collectAsStateWithLifecycle() + val apps by vm.apps.collectAsStateWithLifecycle() + val knownApps by vm.knownApps.collectAsStateWithLifecycle() val settings by AppGraph.settings.settings.collectAsStateWithLifecycle(initialValue = null) val scope = rememberCoroutineScope() @@ -68,14 +75,17 @@ fun SearchScreen(vm: StoreViewModel, nav: NavController) { query = query, onQueryChange = { query = it }, onBack = { - // Go back to wherever the user came from instead of - // stacking a second Home entry on the back stack. - if (!nav.popBackStack()) nav.navigate(Routes.HOME) + // Back returns to the previous tab; with an empty back stack + // fall back to Home instead of stacking a second Home entry. + if (!nav.popBackStack()) { + nav.navigate(Routes.HOME) { launchSingleTop = true } + } }, onSubmit = { term -> - if (term.isNotBlank()) { - query = term - scope.launch { AppGraph.settings.addSearchTerm(term) } + val clean = term.trim() + if (clean.isNotBlank()) { + query = clean + scope.launch { AppGraph.settings.addSearchTerm(clean) } } }, ) @@ -83,79 +93,29 @@ fun SearchScreen(vm: StoreViewModel, nav: NavController) { ) { padding -> Column(Modifier.fillMaxSize().padding(padding)) { if (query.isBlank()) { - val history = settings?.searchHistory.orEmpty() - LazyColumn(contentPadding = PaddingValues(bottom = Spacing.bottomContentPadding)) { - if (history.isNotEmpty()) { - item { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(horizontal = Spacing.screenH, vertical = 12.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - Icon( - Icons.Filled.History, - contentDescription = null, - tint = GothwadTheme.extra.textSecondary, - modifier = Modifier.padding(end = 8.dp), - ) - Text( - "Recent searches", - style = MaterialTheme.typography.titleSmall, - modifier = Modifier.weight(1f), - ) - Text( - "Clear", - color = MaterialTheme.colorScheme.primary, - style = MaterialTheme.typography.labelLarge, - modifier = Modifier - .clickable { scope.launch { AppGraph.settings.clearSearchHistory() } } - .padding(horizontal = 6.dp, vertical = 2.dp), - ) - } - } - item { - LazyRow( - contentPadding = PaddingValues(horizontal = Spacing.screenH), - horizontalArrangement = Arrangement.spacedBy(8.dp), - ) { - items(history) { term -> - PlayChip(term, false, onClick = { query = term }) - } - } - } - } - val suggestions = (home as? UiState.Ready)?.value?.apps.orEmpty().take(8) - if (suggestions.isNotEmpty()) { - item { SectionHeader(title = "Discover", subtitle = "Popular right now") } - itemsIndexed(suggestions, key = { _, app -> "discover-${app.id}" }) { index, app -> - Column { - if (index > 0) DividerLine(startIndent = 64.dp) - AppRowItem( - app = app, - release = releases[app.id], - onOpen = { nav.navigate(Routes.app(app.id)) }, - onAction = { vm.onAppAction(app) }, - ) - } - } - } else { - item { - EmptyState( - title = "Search Kiwi Store", - message = "Find apps and games by name, developer or package name.", - icon = Icons.Filled.Search, - ) - } - } + val catalogue = when (val state = home) { + is UiState.Ready -> state.value.apps + else -> (apps as? UiState.Ready)?.value?.apps.orEmpty() } + SearchIdleContent( + history = settings?.searchHistory.orEmpty(), + suggestions = catalogue.take(5), + recentlyViewed = settings?.recentlyViewed.orEmpty(), + knownApps = knownApps, + releases = releases, + vm = vm, + nav = nav, + onTermClick = { query = it }, + onClearHistory = { scope.launch { AppGraph.settings.clearSearchHistory() } }, + onClearViewed = { scope.launch { AppGraph.settings.clearRecentlyViewed() } }, + ) } else { when (val current = results) { UiState.Loading -> SkeletonList(rows = 6) is UiState.Failed -> ErrorState(current.message, onRetry = { vm.search(query, 0) }) is UiState.Ready -> { - val apps = current.value.apps - if (apps.isEmpty()) { + val found = current.value.apps + if (found.isEmpty()) { EmptyState( title = "No results for “$query”", message = "Check the spelling, or try a different app, game or developer name.", @@ -166,10 +126,10 @@ fun SearchScreen(vm: StoreViewModel, nav: NavController) { item { SectionHeader( title = "Results", - subtitle = "${apps.size} match${if (apps.size == 1) "" else "es"}", + subtitle = "${found.size} match${if (found.size == 1) "" else "es"}", ) } - itemsIndexed(apps, key = { _, app -> app.id }) { index, app -> + itemsIndexed(found, key = { _, app -> app.id }) { index, app -> Column { if (index > 0) DividerLine(startIndent = 64.dp) AppRowItem( @@ -188,3 +148,109 @@ fun SearchScreen(vm: StoreViewModel, nav: NavController) { } } } + +@Composable +private fun SearchIdleContent( + history: List, + suggestions: List, + recentlyViewed: List, + knownApps: Map, + releases: Map, + vm: StoreViewModel, + nav: NavController, + onTermClick: (String) -> Unit, + onClearHistory: () -> Unit, + onClearViewed: () -> Unit, +) { + // The ids are persisted, the listings may not be in this session's cache — + // fall back to the known-app map and silently drop anything unknown. + val visited = recentlyViewed.mapNotNull { knownApps[it] }.distinctBy { it.id }.take(8) + + if (history.isEmpty() && suggestions.isEmpty() && visited.isEmpty()) { + EmptyState( + title = "Search Kiwi Store", + message = "Find apps and games by name, developer or package name.", + icon = Icons.Filled.Search, + ) + return + } + + LazyColumn(contentPadding = PaddingValues(bottom = Spacing.bottomContentPadding)) { + if (history.isNotEmpty()) { + item { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = Spacing.screenH, vertical = 12.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + Icon( + Icons.Filled.History, + contentDescription = null, + tint = GothwadTheme.extra.textSecondary, + modifier = Modifier.padding(end = 8.dp), + ) + Text( + "Recent searches", + style = MaterialTheme.typography.titleSmall, + modifier = Modifier.weight(1f), + ) + Text( + "Clear", + color = MaterialTheme.colorScheme.primary, + style = MaterialTheme.typography.labelLarge, + modifier = Modifier + .clickable(onClick = onClearHistory) + .padding(horizontal = 6.dp, vertical = 2.dp), + ) + } + } + item { + LazyRow( + contentPadding = PaddingValues(horizontal = Spacing.screenH), + horizontalArrangement = Arrangement.spacedBy(8.dp), + ) { + items(history.take(5)) { term -> + PlayChip(term, false, onClick = { onTermClick(term) }) + } + } + } + } + + if (suggestions.isNotEmpty()) { + item { SectionHeader(title = "Suggested apps", subtitle = "Popular right now") } + itemsIndexed(suggestions, key = { _, app -> "discover-${app.id}" }) { index, app -> + Column { + if (index > 0) DividerLine(startIndent = 64.dp) + AppRowItem( + app = app, + release = releases[app.id], + onOpen = { nav.navigate(Routes.app(app.id)) }, + onAction = { vm.onAppAction(app) }, + ) + } + } + } + + if (visited.isNotEmpty()) { + item { + SectionHeader( + title = "Recently viewed", + actionLabel = "Clear", + onAction = onClearViewed, + ) + } + itemsIndexed(visited, key = { _, app -> "visited-${app.id}" }) { index, app -> + Column { + if (index > 0) DividerLine(startIndent = 64.dp) + AppRowItem( + app = app, + release = releases[app.id], + onOpen = { nav.navigate(Routes.app(app.id)) }, + onAction = { vm.onAppAction(app) }, + ) + } + } + } + } +} diff --git a/app/src/main/java/com/gothwad/store/ui/theme/Color.kt b/app/src/main/java/com/gothwad/store/ui/theme/Color.kt index f0f37a3..095a229 100644 --- a/app/src/main/java/com/gothwad/store/ui/theme/Color.kt +++ b/app/src/main/java/com/gothwad/store/ui/theme/Color.kt @@ -2,14 +2,19 @@ package com.gothwad.store.ui.theme import androidx.compose.ui.graphics.Color -// ── Google Play 2024 palette ──────────────────────────────────────────────── +// ── Kiwi Store palette ─────────────────────────────────────────────────────── +// The brand colour is sky blue #0494F4 (the same blue as the launcher icon). +// The property names used to say "Green" when the store was Google-Play green; +// they are now Brand* so nobody re-introduces the old colour by accident. object PlayColors { - // Brand - val Green = Color(0xFF01875F) - val GreenDark = Color(0xFF005B41) - val GreenLight = Color(0xFFE6F4EA) - val GreenContainer = Color(0xFFC4EED0) - val GreenInk = Color(0xFF056B4E) + // Brand — sky blue #0494F4 + val Brand = Color(0xFF0494F4) + val BrandDark = Color(0xFF036FB8) + val BrandLight = Color(0xFFE4F3FE) + val BrandContainer = Color(0xFFC2E4FD) + val BrandInk = Color(0xFF035A94) + val OnBrand = Color(0xFFFFFFFF) + val Blue = Color(0xFF1A73E8) val BlueLight = Color(0xFFE8F0FE) @@ -26,7 +31,6 @@ object PlayColors { val TextPrimary = Color(0xFF202124) val TextSecondary = Color(0xFF5F6368) val TextTertiary = Color(0xFF80868B) - val OnGreen = Color(0xFFFFFFFF) // Accents val Star = Color(0xFFF4B400) @@ -43,12 +47,13 @@ object PlayColors { val Yellow = Color(0xFFFBBC04) val Red = Color(0xFFEA4335) - val BannerStart = Color(0xFF01875F) - val BannerEnd = Color(0xFF00BCD4) + // Hero banner gradient — sky blue to cyan. + val BannerStart = Color(0xFF0494F4) + val BannerEnd = Color(0xFF00C6FF) // Per-app generated icon tint (mirrors the Worker's iconColor). val iconPalette = listOf( - Color(0xFF01875F), Color(0xFF1A73E8), Color(0xFFA142F4), Color(0xFF24C1E0), + Color(0xFF0494F4), Color(0xFF1A73E8), Color(0xFFA142F4), Color(0xFF24C1E0), Color(0xFFFA903E), Color(0xFFF538A0), Color(0xFF34A853), Color(0xFFEA4335), Color(0xFFFBBC04), Color(0xFF5F6368), Color(0xFF00838F), Color(0xFF6D4C41), ) @@ -56,11 +61,11 @@ object PlayColors { // ── Dark palette (follows the system setting) ──────────────────────────────── object PlayColorsDark { - val Green = Color(0xFF4ADE9B) - val GreenDark = Color(0xFF00543C) - val GreenLight = Color(0xFF10352A) - val GreenContainer = Color(0xFF1E4C3B) - val GreenInk = Color(0xFFB7F5D4) + val Brand = Color(0xFF6FC9FF) + val BrandDark = Color(0xFF024E7A) + val BrandLight = Color(0xFF0D2A3C) + val BrandContainer = Color(0xFF123F5C) + val BrandInk = Color(0xFFCDE9FF) val Surface = Color(0xFF1F1F1F) val SurfaceMuted = Color(0xFF171717) @@ -80,6 +85,7 @@ object PlayColorsDark { val Success = Color(0xFF81C995) val SuccessContainer = Color(0xFF1B3324) - val BannerStart = Color(0xFF00543C) + // Keep the banner in the same sky-blue family in dark mode. + val BannerStart = Color(0xFF025B94) val BannerEnd = Color(0xFF00566B) } diff --git a/app/src/main/java/com/gothwad/store/ui/theme/Theme.kt b/app/src/main/java/com/gothwad/store/ui/theme/Theme.kt index e8f43d0..21417f0 100644 --- a/app/src/main/java/com/gothwad/store/ui/theme/Theme.kt +++ b/app/src/main/java/com/gothwad/store/ui/theme/Theme.kt @@ -13,10 +13,10 @@ import androidx.compose.ui.graphics.Color import com.gothwad.store.data.settings.ThemeMode private val LightScheme = lightColorScheme( - primary = PlayColors.Green, - onPrimary = PlayColors.OnGreen, - primaryContainer = PlayColors.GreenLight, - onPrimaryContainer = PlayColors.GreenInk, + primary = PlayColors.Brand, + onPrimary = PlayColors.OnBrand, + primaryContainer = PlayColors.BrandLight, + onPrimaryContainer = PlayColors.BrandInk, secondary = PlayColors.Blue, onSecondary = Color.White, secondaryContainer = PlayColors.BlueLight, @@ -40,10 +40,10 @@ private val LightScheme = lightColorScheme( ) private val DarkScheme = darkColorScheme( - primary = PlayColorsDark.Green, - onPrimary = Color(0xFF00331F), - primaryContainer = PlayColorsDark.GreenContainer, - onPrimaryContainer = PlayColorsDark.GreenInk, + primary = PlayColorsDark.Brand, + onPrimary = Color(0xFF00344F), + primaryContainer = PlayColorsDark.BrandContainer, + onPrimaryContainer = PlayColorsDark.BrandInk, secondary = Color(0xFF8AB4F8), onSecondary = Color(0xFF0B2B52), secondaryContainer = Color(0xFF1D3A5F), @@ -85,7 +85,7 @@ data class GothwadExtraColors( val chipBorder: Color, val bannerStart: Color, val bannerEnd: Color, - val greenInk: Color, + val brandInk: Color, ) val LocalExtraColors: ProvidableCompositionLocal = @@ -106,7 +106,7 @@ private val LightExtraColors = GothwadExtraColors( chipBorder = PlayColors.ChipBorder, bannerStart = PlayColors.BannerStart, bannerEnd = PlayColors.BannerEnd, - greenInk = PlayColors.GreenInk, + brandInk = PlayColors.BrandInk, ) private val DarkExtraColors = GothwadExtraColors( @@ -124,7 +124,7 @@ private val DarkExtraColors = GothwadExtraColors( chipBorder = PlayColorsDark.ChipBorder, bannerStart = PlayColorsDark.BannerStart, bannerEnd = PlayColorsDark.BannerEnd, - greenInk = PlayColorsDark.GreenInk, + brandInk = PlayColorsDark.BrandInk, ) object GothwadTheme { diff --git a/app/src/main/res/values-night/themes.xml b/app/src/main/res/values-night/themes.xml index 22528ff..90fbbc3 100644 --- a/app/src/main/res/values-night/themes.xml +++ b/app/src/main/res/values-night/themes.xml @@ -2,7 +2,7 @@