Skip to content

Commit 1a5d9ed

Browse files
committed
feat(ui): implement update badges and enhance profile ˜navigation
This commit introduces visual indicators for available updates in the bottom navigation and adds support for navigating to developer profiles from the account section. It also includes several UI refinements and fixes for background rendering. - **feat(navigation)**: Added an update notification badge to the "Apps" tab in the `BottomNavigation` when updates are available. - **feat(profile)**: Added `OnRepositoriesClick` action to `ProfileViewModel` and implemented navigation to developer profiles when clicking on the repository count stat. - **feat(profile)**: Updated proxy type selection in the network settings to use a scrollable `LazyRow`. - **refactor(apps)**: Moved app filtering logic from the UI layer to `AppsViewModel` and added automatic sorting to prioritize apps with available updates. - **refactor(core)**: Relocated `isLiquidFrostAvailable` utility to the core presentation module for broader accessibility across features. - **fix(ui)**: Enhanced conditional rendering for "liquid" glass effects; components now fall back to a standard background when the effect is unavailable on the current platform or API level. - **fix(details)**: Updated `SmartInstallButton` to use the `onClick` parameter of the `Card` component for better touch handling. - **chore**: Cleaned up unused imports and comments across multiple presentation modules.
1 parent 9d0e8ed commit 1a5d9ed

17 files changed

Lines changed: 162 additions & 101 deletions

File tree

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/AppNavigation.kt

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import androidx.compose.ui.Modifier
1717
import androidx.compose.ui.layout.onGloballyPositioned
1818
import androidx.compose.ui.platform.LocalDensity
1919
import androidx.compose.ui.unit.dp
20+
import androidx.lifecycle.compose.collectAsStateWithLifecycle
21+
import androidx.lifecycle.viewmodel.compose.viewModel
2022
import androidx.navigation.NavHostController
2123
import androidx.navigation.compose.NavHost
2224
import androidx.navigation.compose.composable
@@ -26,6 +28,7 @@ import io.github.fletchmckee.liquid.rememberLiquidState
2628
import org.koin.compose.viewmodel.koinViewModel
2729
import org.koin.core.parameter.parametersOf
2830
import zed.rainxch.apps.presentation.AppsRoot
31+
import zed.rainxch.apps.presentation.AppsViewModel
2932
import zed.rainxch.auth.presentation.AuthenticationRoot
3033
import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight
3134
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
@@ -45,6 +48,9 @@ fun AppNavigation(
4548
var bottomNavigationHeight by remember { mutableStateOf(0.dp) }
4649
val density = LocalDensity.current
4750

51+
val appsViewModel = koinViewModel<AppsViewModel>()
52+
val appsState by appsViewModel.state.collectAsStateWithLifecycle()
53+
4854
CompositionLocalProvider(
4955
LocalBottomNavigationLiquid provides liquidState,
5056
LocalBottomNavigationHeight provides bottomNavigationHeight
@@ -222,6 +228,9 @@ fun AppNavigation(
222228
},
223229
onNavigateToFavouriteRepos = {
224230
navController.navigate(GithubStoreGraph.FavouritesScreen)
231+
},
232+
onNavigateToDevProfile = { username ->
233+
navController.navigate(GithubStoreGraph.DeveloperProfileScreen(username))
225234
}
226235
)
227236
}
@@ -237,7 +246,9 @@ fun AppNavigation(
237246
repositoryId = repoId
238247
)
239248
)
240-
}
249+
},
250+
viewModel = appsViewModel,
251+
state = appsState
241252
)
242253
}
243254
}
@@ -257,6 +268,7 @@ fun AppNavigation(
257268
restoreState = true
258269
}
259270
},
271+
isUpdateAvailable = appsState.apps.any { it.installedApp.isUpdateAvailable },
260272
modifier = Modifier
261273
.align(Alignment.BottomCenter)
262274
.navigationBarsPadding()

composeApp/src/commonMain/kotlin/zed/rainxch/githubstore/app/navigation/BottomNavigation.kt

Lines changed: 59 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -58,12 +58,13 @@ import zed.rainxch.core.domain.getPlatform
5858
import zed.rainxch.core.domain.model.Platform
5959
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
6060
import zed.rainxch.core.presentation.theme.GithubStoreTheme
61-
import zed.rainxch.details.presentation.utils.isLiquidFrostAvailable
61+
import zed.rainxch.core.presentation.utils.isLiquidFrostAvailable
6262

6363
@Composable
6464
fun BottomNavigation(
6565
currentScreen: GithubStoreGraph,
6666
onNavigate: (GithubStoreGraph) -> Unit,
67+
isUpdateAvailable: Boolean,
6768
modifier: Modifier = Modifier
6869
) {
6970
val liquidState = LocalBottomNavigationLiquid.current
@@ -138,9 +139,7 @@ fun BottomNavigation(
138139
if (isLiquidFrostAvailable()) {
139140
Modifier.liquid(liquidState) {
140141
this.shape = CircleShape
141-
if (isLiquidFrostAvailable()) {
142-
this.frost = if (isDarkTheme) 12.dp else 10.dp
143-
}
142+
this.frost = if (isDarkTheme) 12.dp else 10.dp
144143
this.curve = if (isDarkTheme) .35f else .45f
145144
this.refraction = if (isDarkTheme) .08f else .12f
146145
this.dispersion = if (isDarkTheme) .18f else .25f
@@ -237,6 +236,7 @@ fun BottomNavigation(
237236
visibleItems.forEachIndexed { index, item ->
238237
LiquidGlassTabItem(
239238
item = item,
239+
hasBadge = item.screen == GithubStoreGraph.AppsScreen && isUpdateAvailable,
240240
isSelected = item.screen == currentScreen,
241241
onSelect = { onNavigate(item.screen) },
242242
onPositioned = { x, width ->
@@ -264,6 +264,7 @@ private fun LiquidGlassTabItem(
264264
item: BottomNavigationItem,
265265
isSelected: Boolean,
266266
onSelect: () -> Unit,
267+
hasBadge: Boolean = false,
267268
onPositioned: suspend (x: Float, width: Float) -> Unit
268269
) {
269270
val scope = rememberCoroutineScope()
@@ -331,7 +332,7 @@ private fun LiquidGlassTabItem(
331332
label = "hPadding"
332333
)
333334

334-
Column(
335+
Box(
335336
modifier = Modifier
336337
.clip(CircleShape)
337338
.clickable(
@@ -347,46 +348,59 @@ private fun LiquidGlassTabItem(
347348
scaleX = pressScale
348349
scaleY = pressScale
349350
}
350-
.padding(horizontal = horizontalPadding, vertical = 6.dp),
351-
horizontalAlignment = Alignment.CenterHorizontally,
352-
verticalArrangement = Arrangement.spacedBy(1.dp)
351+
.padding(horizontal = horizontalPadding, vertical = 6.dp)
353352
) {
354-
Icon(
355-
imageVector = if (isSelected) item.iconFilled else item.iconOutlined,
356-
contentDescription = stringResource(item.titleRes),
357-
modifier = Modifier
358-
.size(22.dp)
359-
.graphicsLayer {
360-
scaleX = iconScale
361-
scaleY = iconScale
362-
translationY = with(density) { iconOffsetY.toPx() }
363-
},
364-
tint = iconTint
365-
)
366-
367-
Box(
368-
modifier = Modifier
369-
.height(if (isSelected) 16.dp else 0.dp)
370-
.graphicsLayer {
371-
alpha = labelAlpha
372-
scaleX = labelScale
373-
scaleY = labelScale
374-
},
375-
contentAlignment = Alignment.Center
353+
Column(
354+
horizontalAlignment = Alignment.CenterHorizontally,
355+
verticalArrangement = Arrangement.spacedBy(1.dp)
376356
) {
377-
Text(
378-
text = stringResource(item.titleRes),
379-
style = MaterialTheme.typography.labelSmall.copy(
380-
fontSize = 10.sp,
381-
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
382-
lineHeight = 12.sp
383-
),
384-
color = if (isSelected) {
385-
MaterialTheme.colorScheme.onPrimaryContainer
386-
} else {
387-
MaterialTheme.colorScheme.onSurface
388-
},
389-
maxLines = 1
357+
Icon(
358+
imageVector = if (isSelected) item.iconFilled else item.iconOutlined,
359+
contentDescription = stringResource(item.titleRes),
360+
modifier = Modifier
361+
.size(22.dp)
362+
.graphicsLayer {
363+
scaleX = iconScale
364+
scaleY = iconScale
365+
translationY = with(density) { iconOffsetY.toPx() }
366+
},
367+
tint = iconTint
368+
)
369+
370+
Box(
371+
modifier = Modifier
372+
.height(if (isSelected) 16.dp else 0.dp)
373+
.graphicsLayer {
374+
alpha = labelAlpha
375+
scaleX = labelScale
376+
scaleY = labelScale
377+
},
378+
contentAlignment = Alignment.Center
379+
) {
380+
Text(
381+
text = stringResource(item.titleRes),
382+
style = MaterialTheme.typography.labelSmall.copy(
383+
fontSize = 10.sp,
384+
fontWeight = if (isSelected) FontWeight.SemiBold else FontWeight.Normal,
385+
lineHeight = 12.sp
386+
),
387+
color = if (isSelected) {
388+
MaterialTheme.colorScheme.onPrimaryContainer
389+
} else {
390+
MaterialTheme.colorScheme.onSurface
391+
},
392+
maxLines = 1
393+
)
394+
}
395+
}
396+
397+
if (hasBadge) {
398+
Box(
399+
Modifier
400+
.size(12.dp)
401+
.clip(CircleShape)
402+
.background(MaterialTheme.colorScheme.error)
403+
.align(Alignment.TopEnd)
390404
)
391405
}
392406
}
@@ -403,7 +417,8 @@ fun BottomNavigationPreview() {
403417
currentScreen = GithubStoreGraph.HomeScreen,
404418
onNavigate = {
405419

406-
}
420+
},
421+
isUpdateAvailable = true
407422
)
408423
}
409424
}

feature/details/presentation/src/androidMain/kotlin/zed/rainxch/details/presentation/utils/isLiquidFrostAvailable.android.kt renamed to core/presentation/src/androidMain/kotlin/zed/rainxch/core/presentation/utils/isLiquidFrostAvailable.android.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package zed.rainxch.details.presentation.utils
1+
package zed.rainxch.core.presentation.utils
22

33
import android.os.Build
44
import androidx.annotation.ChecksSdkIntAtLeast
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
package zed.rainxch.core.presentation.utils
2+
3+
expect fun isLiquidFrostAvailable() : Boolean

feature/details/presentation/src/jvmMain/kotlin/zed/rainxch/details/presentation/utils/isLiquidFrostAvailable.jvm.kt renamed to core/presentation/src/jvmMain/kotlin/zed/rainxch/core/presentation/utils/isLiquidFrostAvailable.jvm.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
package zed.rainxch.details.presentation.utils
1+
package zed.rainxch.core.presentation.utils
22

33
actual fun isLiquidFrostAvailable(): Boolean {
44
return true

feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsRoot.kt

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -77,9 +77,9 @@ import zed.rainxch.core.presentation.utils.ObserveAsEvents
7777
fun AppsRoot(
7878
onNavigateBack: () -> Unit,
7979
onNavigateToRepo: (repoId: Long) -> Unit,
80-
viewModel: AppsViewModel = koinViewModel()
80+
viewModel: AppsViewModel = koinViewModel(),
81+
state: AppsState,
8182
) {
82-
val state by viewModel.state.collectAsStateWithLifecycle()
8383
val snackbarHostState = remember { SnackbarHostState() }
8484
val coroutineScope = rememberCoroutineScope()
8585

@@ -251,23 +251,6 @@ fun AppsScreen(
251251
)
252252
}
253253

254-
val filteredApps = remember(state.apps, state.searchQuery) {
255-
if (state.searchQuery.isBlank()) {
256-
state.apps
257-
} else {
258-
state.apps.filter { appItem ->
259-
appItem.installedApp.appName.contains(
260-
state.searchQuery,
261-
ignoreCase = true
262-
) ||
263-
appItem.installedApp.repoOwner.contains(
264-
state.searchQuery,
265-
ignoreCase = true
266-
)
267-
}
268-
}
269-
}
270-
271254
when {
272255
state.isLoading -> {
273256
Box(
@@ -278,7 +261,7 @@ fun AppsScreen(
278261
}
279262
}
280263

281-
filteredApps.isEmpty() -> {
264+
state.filteredApps.isEmpty() -> {
282265
Box(
283266
modifier = Modifier.fillMaxSize(),
284267
contentAlignment = Alignment.Center
@@ -294,7 +277,7 @@ fun AppsScreen(
294277
verticalArrangement = Arrangement.spacedBy(12.dp)
295278
) {
296279
items(
297-
items = filteredApps,
280+
items = state.filteredApps,
298281
key = { it.installedApp.packageName }
299282
) { appItem ->
300283
AppItemCard(

feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsState.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import zed.rainxch.apps.presentation.model.UpdateAllProgress
55

66
data class AppsState(
77
val apps: List<AppItem> = emptyList(),
8+
val filteredApps: List<AppItem> = emptyList(),
89
val searchQuery: String = "",
910
val isLoading: Boolean = false,
1011
val isUpdatingAll: Boolean = false,

feature/apps/presentation/src/commonMain/kotlin/zed/rainxch/apps/presentation/AppsViewModel.kt

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package zed.rainxch.apps.presentation
22

3+
import androidx.compose.runtime.remember
34
import androidx.lifecycle.ViewModel
45
import androidx.lifecycle.viewModelScope
56
import zed.rainxch.githubstore.core.presentation.res.*
@@ -96,6 +97,8 @@ class AppsViewModel(
9697
}
9798
)
9899
}
100+
101+
filterApps()
99102
}
100103
} catch (e: Exception) {
101104
logger.error("Failed to load apps: ${e.message}")
@@ -157,7 +160,11 @@ class AppsViewModel(
157160
}
158161

159162
is AppsAction.OnSearchChange -> {
160-
_state.update { it.copy(searchQuery = action.query) }
163+
_state.update {
164+
it.copy(searchQuery = action.query)
165+
}
166+
167+
filterApps()
161168
}
162169

163170
is AppsAction.OnOpenApp -> {
@@ -200,6 +207,28 @@ class AppsViewModel(
200207
}
201208
}
202209

210+
private fun filterApps() {
211+
_state.update {
212+
it.copy(
213+
filteredApps = if (_state.value.searchQuery.isBlank()) {
214+
_state.value.apps.sortedBy { it.installedApp.isUpdateAvailable }
215+
} else {
216+
_state.value.apps.filter { appItem ->
217+
appItem.installedApp.appName.contains(
218+
_state.value.searchQuery,
219+
ignoreCase = true
220+
) ||
221+
appItem.installedApp.repoOwner.contains(
222+
_state.value.searchQuery,
223+
ignoreCase = true
224+
)
225+
}.sortedBy { it.installedApp.isUpdateAvailable }
226+
}
227+
)
228+
}
229+
230+
}
231+
203232
private fun uninstallApp(app: InstalledApp) {
204233
viewModelScope.launch {
205234
try {

feature/details/presentation/src/commonMain/kotlin/zed/rainxch/details/presentation/DetailsRoot.kt

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,7 @@ import org.jetbrains.compose.ui.tooling.preview.Preview
6161
import org.koin.compose.viewmodel.koinViewModel
6262
import zed.rainxch.core.presentation.theme.GithubStoreTheme
6363
import zed.rainxch.core.presentation.utils.ObserveAsEvents
64+
import zed.rainxch.core.presentation.utils.isLiquidFrostAvailable
6465
import zed.rainxch.details.presentation.components.sections.about
6566
import zed.rainxch.details.presentation.components.sections.author
6667
import zed.rainxch.details.presentation.components.sections.header
@@ -69,7 +70,6 @@ import zed.rainxch.details.presentation.components.sections.stats
6970
import zed.rainxch.details.presentation.components.sections.whatsNew
7071
import zed.rainxch.details.presentation.components.states.ErrorState
7172
import zed.rainxch.details.presentation.utils.LocalTopbarLiquidState
72-
import zed.rainxch.details.presentation.utils.isLiquidFrostAvailable
7373

7474
@Composable
7575
fun DetailsRoot(
@@ -385,15 +385,18 @@ private fun DetailsTopbar(
385385
1f to MaterialTheme.colorScheme.surface.copy(alpha = 0.85f)
386386
)
387387
)
388-
.liquid(liquidTopbarState) {
389-
this.shape = CutCornerShape(0.dp)
388+
.then(
390389
if (isLiquidFrostAvailable()) {
391-
this.frost = 5.dp
390+
Modifier.liquid(liquidTopbarState) {
391+
this.shape = CutCornerShape(0.dp)
392+
if (isLiquidFrostAvailable()) {
393+
this.frost = 5.dp
394+
}
395+
this.curve = .25f
396+
this.refraction = .05f
397+
this.dispersion = .1f
392398
}
393-
this.curve = .25f
394-
this.refraction = .05f
395-
this.dispersion = .1f
396-
}
399+
} else Modifier.background(MaterialTheme.colorScheme.surfaceContainerHighest))
397400
)
398401
}
399402

0 commit comments

Comments
 (0)