Skip to content

Commit 8a7e5ca

Browse files
committed
feat(profile): Enhance proxy settings validation and improve UI
This commit improves the robustness of proxy configuration in the profile section by adding validation and error handling, while also refining the UI and navigation state management. - **feat(profile)**: Added localized error messages for invalid proxy ports, missing hosts, and save failures in `ProfileViewModel`. - **feat(profile)**: Implemented `runCatching` for proxy configuration updates to provide explicit success/failure events. - **feat(ui)**: Introduced `LocalBottomNavigationHeight` to track the bottom bar's height globally via `onGloballyPositioned`. - **refactor(profile)**: Replaced custom `ProxyTypeChip` with Material3 `FilterChip` for a more standard selection UI in the Network section. - **refactor(profile)**: Updated `ProfileRoot` to adjust `SnackbarHost` padding dynamically based on the bottom navigation height, preventing overlap. - **fix(profile)**: Added accessibility content descriptions to the proxy password visibility toggle.
1 parent a4965c5 commit 8a7e5ca

5 files changed

Lines changed: 86 additions & 52 deletions

File tree

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

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,14 @@ import androidx.compose.foundation.layout.padding
88
import androidx.compose.material3.MaterialTheme
99
import androidx.compose.runtime.Composable
1010
import androidx.compose.runtime.CompositionLocalProvider
11+
import androidx.compose.runtime.getValue
12+
import androidx.compose.runtime.mutableStateOf
13+
import androidx.compose.runtime.remember
14+
import androidx.compose.runtime.setValue
1115
import androidx.compose.ui.Alignment
1216
import androidx.compose.ui.Modifier
17+
import androidx.compose.ui.layout.onGloballyPositioned
18+
import androidx.compose.ui.platform.LocalDensity
1319
import androidx.compose.ui.unit.dp
1420
import androidx.navigation.NavHostController
1521
import androidx.navigation.compose.NavHost
@@ -21,6 +27,7 @@ import org.koin.compose.viewmodel.koinViewModel
2127
import org.koin.core.parameter.parametersOf
2228
import zed.rainxch.apps.presentation.AppsRoot
2329
import zed.rainxch.auth.presentation.AuthenticationRoot
30+
import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight
2431
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
2532
import zed.rainxch.details.presentation.DetailsRoot
2633
import zed.rainxch.devprofile.presentation.DeveloperProfileRoot
@@ -35,9 +42,12 @@ fun AppNavigation(
3542
navController: NavHostController
3643
) {
3744
val liquidState = rememberLiquidState()
45+
var bottomNavigationHeight by remember { mutableStateOf(0.dp) }
46+
val density = LocalDensity.current
3847

3948
CompositionLocalProvider(
40-
value = LocalBottomNavigationLiquid provides liquidState
49+
LocalBottomNavigationLiquid provides liquidState,
50+
LocalBottomNavigationHeight provides bottomNavigationHeight
4151
) {
4252
Box(
4353
modifier = Modifier.fillMaxSize()
@@ -235,6 +245,9 @@ fun AppNavigation(
235245
.align(Alignment.BottomCenter)
236246
.navigationBarsPadding()
237247
.padding(bottom = 24.dp)
248+
.onGloballyPositioned { coordinates ->
249+
bottomNavigationHeight = with(density) { coordinates.size.height.toDp() }
250+
}
238251
)
239252
}
240253
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
package zed.rainxch.core.presentation.locals
2+
3+
import androidx.compose.runtime.compositionLocalOf
4+
import androidx.compose.ui.unit.Dp
5+
6+
val LocalBottomNavigationHeight = compositionLocalOf<Dp> {
7+
error("Not initialized yet")
8+
}

feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileRoot.kt

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package zed.rainxch.profile.presentation
22

33
import androidx.compose.foundation.layout.Spacer
44
import androidx.compose.foundation.layout.fillMaxSize
5+
import androidx.compose.foundation.layout.fillMaxWidth
56
import androidx.compose.foundation.layout.height
67
import androidx.compose.foundation.layout.padding
78
import androidx.compose.foundation.layout.size
@@ -34,6 +35,7 @@ import org.jetbrains.compose.resources.getString
3435
import org.jetbrains.compose.resources.stringResource
3536
import org.jetbrains.compose.ui.tooling.preview.Preview
3637
import org.koin.compose.viewmodel.koinViewModel
38+
import zed.rainxch.core.presentation.locals.LocalBottomNavigationHeight
3739
import zed.rainxch.core.presentation.locals.LocalBottomNavigationLiquid
3840
import zed.rainxch.core.presentation.theme.GithubStoreTheme
3941
import zed.rainxch.core.presentation.utils.ObserveAsEvents
@@ -119,9 +121,13 @@ fun ProfileScreen(
119121
snackbarState: SnackbarHostState
120122
) {
121123
val liquidState = LocalBottomNavigationLiquid.current
124+
val bottomNavHeight = LocalBottomNavigationHeight.current
122125
Scaffold(
123126
snackbarHost = {
124-
SnackbarHost(hostState = snackbarState)
127+
SnackbarHost(
128+
hostState = snackbarState,
129+
modifier = Modifier.padding(bottom = bottomNavHeight)
130+
)
125131
},
126132
topBar = {
127133
TopAppBar(onAction)

feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/ProfileViewModel.kt

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,16 @@ import kotlinx.coroutines.flow.receiveAsFlow
1010
import kotlinx.coroutines.flow.stateIn
1111
import kotlinx.coroutines.flow.update
1212
import kotlinx.coroutines.launch
13+
import org.jetbrains.compose.resources.getString
14+
import org.jetbrains.compose.resources.stringResource
1315
import zed.rainxch.core.domain.model.ProxyConfig
1416
import zed.rainxch.core.domain.repository.ProxyRepository
1517
import zed.rainxch.core.domain.repository.ThemesRepository
1618
import zed.rainxch.core.domain.utils.BrowserHelper
19+
import zed.rainxch.githubstore.core.presentation.res.Res
20+
import zed.rainxch.githubstore.core.presentation.res.failed_to_save_proxy_settings
21+
import zed.rainxch.githubstore.core.presentation.res.invalid_proxy_port
22+
import zed.rainxch.githubstore.core.presentation.res.proxy_host_required
1723
import zed.rainxch.profile.domain.repository.ProfileRepository
1824

1925
class ProfileViewModel(
@@ -208,7 +214,18 @@ class ProfileViewModel(
208214
else -> return
209215
}
210216
viewModelScope.launch {
211-
proxyRepository.setProxyConfig(config)
217+
runCatching {
218+
proxyRepository.setProxyConfig(config)
219+
}.onSuccess {
220+
_events.send(ProfileEvent.OnProxySaved)
221+
}.onFailure { error ->
222+
_events.send(
223+
ProfileEvent.OnProxySaveError(
224+
error.message ?: getString(Res.string.failed_to_save_proxy_settings)
225+
)
226+
)
227+
}
228+
212229
}
213230
}
214231
}
@@ -237,8 +254,19 @@ class ProfileViewModel(
237254
val currentState = _state.value
238255
val port = currentState.proxyPort.toIntOrNull()
239256
?.takeIf { it in 1..65535 }
240-
?: return
241-
val host = currentState.proxyHost.trim().takeIf { it.isNotBlank() } ?: return
257+
?: run {
258+
viewModelScope.launch {
259+
_events.send(ProfileEvent.OnProxySaveError(getString(Res.string.invalid_proxy_port)))
260+
}
261+
return
262+
}
263+
val host = currentState.proxyHost.trim().takeIf { it.isNotBlank() } ?: run {
264+
viewModelScope.launch {
265+
_events.send(ProfileEvent.OnProxySaveError(getString(Res.string.proxy_host_required)))
266+
}
267+
return
268+
}
269+
242270
val username = currentState.proxyUsername.takeIf { it.isNotBlank() }
243271
val password = currentState.proxyPassword.takeIf { it.isNotBlank() }
244272

@@ -257,7 +285,7 @@ class ProfileViewModel(
257285
}.onFailure { error ->
258286
_events.send(
259287
ProfileEvent.OnProxySaveError(
260-
error.message ?: "Failed to save proxy settings"
288+
error.message ?: getString(Res.string.failed_to_save_proxy_settings)
261289
)
262290
)
263291
}

feature/profile/presentation/src/commonMain/kotlin/zed/rainxch/profile/presentation/components/sections/Network.kt

Lines changed: 25 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@ import androidx.compose.animation.expandVertically
55
import androidx.compose.animation.fadeIn
66
import androidx.compose.animation.fadeOut
77
import androidx.compose.animation.shrinkVertically
8-
import androidx.compose.foundation.background
9-
import androidx.compose.foundation.clickable
108
import androidx.compose.foundation.layout.Arrangement
119
import androidx.compose.foundation.layout.Column
1210
import androidx.compose.foundation.layout.Row
@@ -26,6 +24,7 @@ import androidx.compose.material3.CardDefaults
2624
import androidx.compose.material3.ElevatedCard
2725
import androidx.compose.material3.ExperimentalMaterial3ExpressiveApi
2826
import androidx.compose.material3.FilledTonalButton
27+
import androidx.compose.material3.FilterChip
2928
import androidx.compose.material3.Icon
3029
import androidx.compose.material3.IconButton
3130
import androidx.compose.material3.MaterialTheme
@@ -34,11 +33,11 @@ import androidx.compose.material3.Text
3433
import androidx.compose.runtime.Composable
3534
import androidx.compose.ui.Alignment
3635
import androidx.compose.ui.Modifier
37-
import androidx.compose.ui.draw.clip
3836
import androidx.compose.ui.text.font.FontWeight
3937
import androidx.compose.ui.text.input.KeyboardType
4038
import androidx.compose.ui.text.input.PasswordVisualTransformation
4139
import androidx.compose.ui.text.input.VisualTransformation
40+
import androidx.compose.ui.text.style.TextOverflow
4241
import androidx.compose.ui.unit.dp
4342
import org.jetbrains.compose.resources.stringResource
4443
import zed.rainxch.githubstore.core.presentation.res.*
@@ -131,15 +130,24 @@ private fun ProxyTypeCard(
131130
horizontalArrangement = Arrangement.spacedBy(8.dp)
132131
) {
133132
ProxyType.entries.forEach { type ->
134-
ProxyTypeChip(
135-
label = when (type) {
136-
ProxyType.NONE -> stringResource(Res.string.proxy_none)
137-
ProxyType.SYSTEM -> stringResource(Res.string.proxy_system)
138-
ProxyType.HTTP -> stringResource(Res.string.proxy_http)
139-
ProxyType.SOCKS -> stringResource(Res.string.proxy_socks)
140-
},
141-
isSelected = selectedType == type,
133+
FilterChip(
134+
selected = selectedType == type,
142135
onClick = { onTypeSelected(type) },
136+
label = {
137+
Text(
138+
text = when (type) {
139+
ProxyType.NONE -> stringResource(Res.string.proxy_none)
140+
ProxyType.SYSTEM -> stringResource(Res.string.proxy_system)
141+
ProxyType.HTTP -> stringResource(Res.string.proxy_http)
142+
ProxyType.SOCKS -> stringResource(Res.string.proxy_socks)
143+
},
144+
fontWeight = if (selectedType == type) FontWeight.Bold else FontWeight.Normal,
145+
style = MaterialTheme.typography.bodyMedium,
146+
color = MaterialTheme.colorScheme.onSurface,
147+
maxLines = 1,
148+
overflow = TextOverflow.Ellipsis
149+
)
150+
},
143151
modifier = Modifier.weight(1f)
144152
)
145153
}
@@ -148,40 +156,6 @@ private fun ProxyTypeCard(
148156
}
149157
}
150158

151-
@Composable
152-
private fun ProxyTypeChip(
153-
label: String,
154-
isSelected: Boolean,
155-
onClick: () -> Unit,
156-
modifier: Modifier = Modifier
157-
) {
158-
Column(
159-
modifier = modifier
160-
.clip(RoundedCornerShape(12.dp))
161-
.background(
162-
if (isSelected) {
163-
MaterialTheme.colorScheme.primaryContainer
164-
} else {
165-
MaterialTheme.colorScheme.surface
166-
}
167-
)
168-
.clickable(onClick = onClick)
169-
.padding(vertical = 10.dp),
170-
horizontalAlignment = Alignment.CenterHorizontally
171-
) {
172-
Text(
173-
text = label,
174-
style = MaterialTheme.typography.labelLarge,
175-
color = if (isSelected) {
176-
MaterialTheme.colorScheme.onPrimaryContainer
177-
} else {
178-
MaterialTheme.colorScheme.onSurfaceVariant
179-
},
180-
fontWeight = if (isSelected) FontWeight.Bold else FontWeight.Normal
181-
)
182-
}
183-
}
184-
185159
@OptIn(ExperimentalMaterial3ExpressiveApi::class)
186160
@Composable
187161
private fun ProxyDetailsCard(
@@ -268,7 +242,12 @@ private fun ProxyDetailsCard(
268242
} else {
269243
Icons.Default.Visibility
270244
},
271-
contentDescription = null,
245+
contentDescription = if (state.isProxyPasswordVisible) {
246+
stringResource(Res.string.proxy_hide_password)
247+
} else {
248+
stringResource(Res.string.proxy_show_password)
249+
},
250+
272251
modifier = Modifier.size(20.dp)
273252
)
274253
}

0 commit comments

Comments
 (0)