@@ -55,14 +55,14 @@ import androidx.compose.ui.text.font.FontWeight
5555import androidx.compose.ui.text.style.TextDecoration
5656import androidx.compose.ui.text.style.TextOverflow
5757import androidx.compose.ui.unit.dp
58- import androidx.compose.ui.text.style.TextAlign
59- import com.masterdns.vpn.BuildConfig
6058import androidx.compose.ui.window.Dialog
6159import androidx.core.graphics.drawable.toBitmap
6260import androidx.lifecycle.viewmodel.compose.viewModel
6361import com.masterdns.vpn.R
6462import com.masterdns.vpn.util.GlobalSettings
6563import kotlinx.coroutines.launch
64+ import com.masterdns.vpn.BuildConfig
65+
6666@OptIn(ExperimentalMaterial3Api ::class )
6767@Composable
6868fun GlobalSettingsScreen (vm : GlobalSettingsViewModel = viewModel()) {
@@ -78,6 +78,7 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
7878 val snackbarHostState = remember { SnackbarHostState () }
7979 val scope = rememberCoroutineScope()
8080 val uriHandler = LocalUriHandler .current
81+
8182 Scaffold (
8283 topBar = { TopAppBar (title = { Text (" Settings" ) }) },
8384 snackbarHost = { SnackbarHost (hostState = snackbarHostState) }
@@ -118,14 +119,17 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
118119 draft = draft.copy(connectionMode = mode)
119120 modeExpanded = false
120121 }
122+ )
121123 }
122124 }
123125 }
126+
124127 RowSwitch (
125128 title = " Split Tunneling" ,
126129 checked = draft.splitTunnelingEnabled,
127130 onChecked = { draft = draft.copy(splitTunnelingEnabled = it) }
128131 )
132+
129133 if (draft.splitTunnelingEnabled) {
130134 Card (
131135 onClick = {
@@ -134,23 +138,33 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
134138 selectedQuery = " "
135139 activeTab = " AVAILABLE"
136140 showAppPicker = true
141+ },
137142 modifier = Modifier .fillMaxWidth()
138143 ) {
139144 Column (modifier = Modifier .padding(12 .dp)) {
140145 Text (" Split Tunnel Apps" )
141146 Text (
142147 " ${parseCsv(draft.splitPackagesCsv).size} selected apps - tap to choose" ,
143148 style = MaterialTheme .typography.bodySmall
149+ )
150+ }
151+ }
152+ }
153+
144154 Button (
145155 onClick = {
146156 vm.save(normalize(draft))
147157 scope.launch { snackbarHostState.showSnackbar(" Global settings saved and applied" ) }
148158 },
149159 modifier = Modifier .fillMaxWidth()
160+ ) {
150161 Text (" Save Global Settings" )
162+ }
151163 }
152164 }
153165 }
166+ item {
167+ Card (colors = CardDefaults .cardColors()) {
154168 Column (
155169 modifier = Modifier .padding(12 .dp),
156170 verticalArrangement = Arrangement .spacedBy(8 .dp)
@@ -163,38 +177,84 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
163177 title = " Main GitHub:" ,
164178 link = mainGithubLink,
165179 onOpen = { uriHandler.openUri(" https://$mainGithubLink " ) }
180+ )
181+ LinkRow (
166182 title = " Main Telegram:" ,
167183 link = mainTelegramLink,
168184 onOpen = { uriHandler.openUri(" https://$mainTelegramLink " ) }
185+ )
186+ LinkRow (
169187 title = " MDV-HN Android Client:" ,
170188 link = androidClientGithubLink,
171189 onOpen = { uriHandler.openUri(" https://$androidClientGithubLink " ) }
190+ )
191+ }
192+ }
193+ }
194+ item {
172195 val engineVersion = stringResource(R .string.engine_version)
196+ Card (colors = CardDefaults .cardColors()) {
197+ Column (
173198 modifier = Modifier
174199 .fillMaxWidth()
175200 .padding(12 .dp),
176- verticalArrangement = Arrangement .spacedBy(4 .dp)
201+ verticalArrangement = Arrangement .spacedBy(6 .dp)
202+ ) {
177203 Text (" Version Info" , style = MaterialTheme .typography.titleMedium)
178204 Row (
179205 modifier = Modifier .fillMaxWidth(),
180206 horizontalArrangement = Arrangement .SpaceBetween
181- Text (" App Version" , style = MaterialTheme .typography.bodyMedium, color = MaterialTheme .colorScheme.onSurfaceVariant)
182- Text (BuildConfig .VERSION_NAME , style = MaterialTheme .typography.bodyMedium, fontWeight = FontWeight .Medium )
183- Text (" Upstream Engine" , style = MaterialTheme .typography.bodyMedium, color = MaterialTheme .colorScheme.onSurfaceVariant)
184- Text (engineVersion, style = MaterialTheme .typography.bodyMedium, fontWeight = FontWeight .Medium )
207+ ) {
208+ Text (
209+ " App Version" ,
210+ style = MaterialTheme .typography.bodyMedium,
211+ color = MaterialTheme .colorScheme.onSurfaceVariant
212+ )
213+ Text (
214+ BuildConfig .VERSION_NAME ,
215+ style = MaterialTheme .typography.bodyMedium,
216+ fontWeight = FontWeight .Medium
217+ )
218+ }
219+ Row (
220+ modifier = Modifier .fillMaxWidth(),
221+ horizontalArrangement = Arrangement .SpaceBetween
222+ ) {
223+ Text (
224+ " Upstream Engine" ,
225+ style = MaterialTheme .typography.bodyMedium,
226+ color = MaterialTheme .colorScheme.onSurfaceVariant
227+ )
228+ Text (
229+ engineVersion,
230+ style = MaterialTheme .typography.bodyMedium,
231+ fontWeight = FontWeight .Medium
232+ )
233+ }
234+ }
235+ }
236+ }
185237 }
186238 }
239+
187240 if (showAppPicker) {
188241 val selectedApps = installedApps.filter { draftAppSelection.contains(it.packageName) }
189242 val availableApps = installedApps.filterNot { draftAppSelection.contains(it.packageName) }
243+
190244 val selectedFiltered = selectedApps.filter {
191245 val q = selectedQuery.trim().lowercase()
192246 q.isEmpty() ||
193247 it.label.lowercase().contains(q) ||
194248 it.packageName.lowercase().contains(q)
195249 }.sortedWith(compareBy({ it.label.lowercase() }, { it.packageName }))
250+
196251 val availableFiltered = availableApps.filter {
197252 val q = availableQuery.trim().lowercase()
253+ q.isEmpty() ||
254+ it.label.lowercase().contains(q) ||
255+ it.packageName.lowercase().contains(q)
256+ }.sortedWith(compareBy({ it.label.lowercase() }, { it.packageName }))
257+
198258 Dialog (onDismissRequest = { showAppPicker = false }) {
199259 Surface (
200260 modifier = Modifier
@@ -215,54 +275,86 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
215275 style = MaterialTheme .typography.bodySmall,
216276 color = MaterialTheme .colorScheme.onSurface.copy(alpha = 0.7f )
217277 )
278+
218279 Row (horizontalArrangement = Arrangement .spacedBy(8 .dp)) {
219280 FilterChip (
220281 selected = activeTab == " SELECTED" ,
221282 onClick = { activeTab = " SELECTED" },
222283 label = { Text (" Selected ${selectedApps.size} " ) }
284+ )
285+ FilterChip (
223286 selected = activeTab == " AVAILABLE" ,
224287 onClick = { activeTab = " AVAILABLE" },
225288 label = { Text (" Available ${availableApps.size} " ) }
289+ )
290+ }
291+
226292 if (activeTab == " SELECTED" ) {
227293 OutlinedTextField (
228294 value = selectedQuery,
229295 onValueChange = { selectedQuery = it },
230296 label = { Text (" Search selected apps" ) },
297+ modifier = Modifier .fillMaxWidth()
298+ )
231299 } else {
300+ OutlinedTextField (
232301 value = availableQuery,
233302 onValueChange = { availableQuery = it },
234303 label = { Text (" Search available apps" ) },
304+ modifier = Modifier .fillMaxWidth()
305+ )
306+ }
307+
308+ Row (horizontalArrangement = Arrangement .spacedBy(8 .dp)) {
235309 OutlinedButton (
310+ onClick = {
236311 draftAppSelection = draftAppSelection.toMutableSet().apply {
237312 addAll(availableFiltered.map { it.packageName })
313+ }
314+ },
238315 modifier = Modifier .weight(1f )
316+ ) {
239317 Text (" Select Visible" )
318+ }
319+ OutlinedButton (
240320 onClick = { draftAppSelection = mutableSetOf () },
321+ modifier = Modifier .weight(1f )
322+ ) {
241323 Text (" Select None" )
324+ }
325+ }
326+
242327 Surface (
243328 modifier = Modifier .fillMaxWidth(),
244329 shape = RoundedCornerShape (12 .dp),
245330 color = MaterialTheme .colorScheme.surfaceVariant.copy(alpha = 0.5f )
331+ ) {
246332 Column (
247333 modifier = Modifier
248334 .fillMaxWidth()
249335 .padding(10 .dp)
336+ ) {
250337 val appsToShow = if (activeTab == " SELECTED" ) selectedFiltered else availableFiltered
251338 val emptyText = if (activeTab == " SELECTED" ) {
252339 " No selected app matches your search"
253340 } else {
254341 " No available app matches your search"
342+ }
343+
255344 Text (
256345 if (activeTab == " SELECTED" ) " Selected Apps" else " Available Apps" ,
257346 style = MaterialTheme .typography.labelLarge,
258347 color = MaterialTheme .colorScheme.primary
348+ )
349+
259350 if (appsToShow.isEmpty()) {
260351 Text (
261352 emptyText,
262353 style = MaterialTheme .typography.bodySmall,
263354 color = MaterialTheme .colorScheme.onSurface.copy(alpha = 0.7f ),
264355 modifier = Modifier .padding(top = 8 .dp)
265356 )
357+ } else {
266358 LazyColumn (
267359 modifier = Modifier
268360 .fillMaxWidth()
@@ -279,14 +371,34 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
279371 }
280372 )
281373 }
374+ }
375+ }
376+ }
377+ }
378+
282379 Row (
380+ modifier = Modifier .fillMaxWidth(),
283381 horizontalArrangement = Arrangement .End
382+ ) {
284383 TextButton (onClick = { showAppPicker = false }) {
285384 Text (" Cancel" )
385+ }
386+ Button (
387+ onClick = {
286388 draft = draft.copy(splitPackagesCsv = draftAppSelection.sorted().joinToString(" ," ))
287389 showAppPicker = false
390+ }
391+ ) {
288392 Text (" Apply" )
393+ }
394+ }
395+ }
396+ }
397+ }
398+ }
289399}
400+
401+ @Composable
290402private fun LinkRow (title : String , link : String , onOpen : () -> Unit ) {
291403 Column (
292404 modifier = Modifier
@@ -299,6 +411,7 @@ private fun LinkRow(title: String, link: String, onOpen: () -> Unit) {
299411 style = MaterialTheme .typography.bodyMedium,
300412 color = MaterialTheme .colorScheme.onSurfaceVariant
301413 )
414+ Text (
302415 text = link,
303416 style = MaterialTheme .typography.bodyMedium.copy(
304417 textDecoration = TextDecoration .Underline ,
@@ -307,6 +420,11 @@ private fun LinkRow(title: String, link: String, onOpen: () -> Unit) {
307420 color = MaterialTheme .colorScheme.primary,
308421 maxLines = 3 ,
309422 overflow = TextOverflow .Ellipsis
423+ )
424+ }
425+ }
426+
427+ @Composable
310428private fun AppRow (
311429 app : GlobalSettingsViewModel .AppEntry ,
312430 checked : Boolean ,
@@ -317,14 +435,19 @@ private fun AppRow(
317435 runCatching {
318436 context.packageManager.getApplicationIcon(app.packageName).toBitmap(48 , 48 )
319437 }.getOrNull()
438+ }
320439 Row (
440+ modifier = Modifier
441+ .fillMaxWidth()
321442 .clickable { onToggle() }
322443 .padding(vertical = 4 .dp),
323444 horizontalArrangement = Arrangement .SpaceBetween ,
324445 verticalAlignment = Alignment .CenterVertically
446+ ) {
325447 Row (
326448 modifier = Modifier .weight(1f ),
327449 verticalAlignment = Alignment .CenterVertically
450+ ) {
328451 if (appIconBitmap != null ) {
329452 Image (
330453 bitmap = appIconBitmap.asImageBitmap(),
@@ -334,23 +457,41 @@ private fun AppRow(
334457 } else {
335458 Icon (
336459 imageVector = Icons .Filled .ArrowDropDown ,
460+ contentDescription = null ,
461+ modifier = Modifier .size(24 .dp)
462+ )
463+ }
337464 Spacer (modifier = Modifier .size(8 .dp))
338465 Column {
339466 Text (text = app.label)
340467 Text (text = app.packageName)
468+ }
469+ }
341470 Checkbox (
342471 checked = checked,
343472 onCheckedChange = { onToggle() }
473+ )
474+ }
475+ }
476+
477+ @Composable
344478private fun RowSwitch (title : String , checked : Boolean , onChecked : (Boolean ) -> Unit ) {
479+ Row (
345480 modifier = Modifier .fillMaxWidth(),
346481 horizontalArrangement = Arrangement .SpaceBetween
482+ ) {
347483 Text (title)
348484 Switch (checked = checked, onCheckedChange = onChecked)
485+ }
486+ }
487+
349488private fun parseCsv (value : String ): Set <String > {
350489 return value.split(" ," )
351490 .map { it.trim() }
352491 .filter { it.isNotBlank() }
353492 .toSet()
493+ }
494+
354495private fun normalize (settings : GlobalSettings ): GlobalSettings {
355496 return settings.copy(
356497 connectionMode = settings.connectionMode.uppercase(),
@@ -361,3 +502,4 @@ private fun normalize(settings: GlobalSettings): GlobalSettings {
361502 .distinct()
362503 .joinToString(" ," )
363504 )
505+ }
0 commit comments