@@ -55,13 +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
5860import androidx.compose.ui.window.Dialog
5961import androidx.core.graphics.drawable.toBitmap
6062import androidx.lifecycle.viewmodel.compose.viewModel
6163import com.masterdns.vpn.R
6264import com.masterdns.vpn.util.GlobalSettings
6365import kotlinx.coroutines.launch
64-
6566@OptIn(ExperimentalMaterial3Api ::class )
6667@Composable
6768fun GlobalSettingsScreen (vm : GlobalSettingsViewModel = viewModel()) {
@@ -77,7 +78,6 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
7778 val snackbarHostState = remember { SnackbarHostState () }
7879 val scope = rememberCoroutineScope()
7980 val uriHandler = LocalUriHandler .current
80-
8181 Scaffold (
8282 topBar = { TopAppBar (title = { Text (" Settings" ) }) },
8383 snackbarHost = { SnackbarHost (hostState = snackbarHostState) }
@@ -118,17 +118,14 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
118118 draft = draft.copy(connectionMode = mode)
119119 modeExpanded = false
120120 }
121- )
122121 }
123122 }
124123 }
125-
126124 RowSwitch (
127125 title = " Split Tunneling" ,
128126 checked = draft.splitTunnelingEnabled,
129127 onChecked = { draft = draft.copy(splitTunnelingEnabled = it) }
130128 )
131-
132129 if (draft.splitTunnelingEnabled) {
133130 Card (
134131 onClick = {
@@ -137,33 +134,23 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
137134 selectedQuery = " "
138135 activeTab = " AVAILABLE"
139136 showAppPicker = true
140- },
141137 modifier = Modifier .fillMaxWidth()
142138 ) {
143139 Column (modifier = Modifier .padding(12 .dp)) {
144140 Text (" Split Tunnel Apps" )
145141 Text (
146142 " ${parseCsv(draft.splitPackagesCsv).size} selected apps - tap to choose" ,
147143 style = MaterialTheme .typography.bodySmall
148- )
149- }
150- }
151- }
152-
153144 Button (
154145 onClick = {
155146 vm.save(normalize(draft))
156147 scope.launch { snackbarHostState.showSnackbar(" Global settings saved and applied" ) }
157148 },
158149 modifier = Modifier .fillMaxWidth()
159- ) {
160150 Text (" Save Global Settings" )
161- }
162151 }
163152 }
164153 }
165- item {
166- Card (colors = CardDefaults .cardColors()) {
167154 Column (
168155 modifier = Modifier .padding(12 .dp),
169156 verticalArrangement = Arrangement .spacedBy(8 .dp)
@@ -176,41 +163,38 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
176163 title = " Main GitHub:" ,
177164 link = mainGithubLink,
178165 onOpen = { uriHandler.openUri(" https://$mainGithubLink " ) }
179- )
180- LinkRow (
181166 title = " Main Telegram:" ,
182167 link = mainTelegramLink,
183168 onOpen = { uriHandler.openUri(" https://$mainTelegramLink " ) }
184- )
185- LinkRow (
186169 title = " MDV-HN Android Client:" ,
187170 link = androidClientGithubLink,
188171 onOpen = { uriHandler.openUri(" https://$androidClientGithubLink " ) }
189- )
190- }
191- }
192- }
172+ val engineVersion = stringResource(R .string.engine_version)
173+ modifier = Modifier
174+ .fillMaxWidth()
175+ .padding(12 .dp),
176+ verticalArrangement = Arrangement .spacedBy(4 .dp)
177+ Text (" Version Info" , style = MaterialTheme .typography.titleMedium)
178+ Row (
179+ modifier = Modifier .fillMaxWidth(),
180+ 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 )
193185 }
194186 }
195-
196187 if (showAppPicker) {
197188 val selectedApps = installedApps.filter { draftAppSelection.contains(it.packageName) }
198189 val availableApps = installedApps.filterNot { draftAppSelection.contains(it.packageName) }
199-
200190 val selectedFiltered = selectedApps.filter {
201191 val q = selectedQuery.trim().lowercase()
202192 q.isEmpty() ||
203193 it.label.lowercase().contains(q) ||
204194 it.packageName.lowercase().contains(q)
205195 }.sortedWith(compareBy({ it.label.lowercase() }, { it.packageName }))
206-
207196 val availableFiltered = availableApps.filter {
208197 val q = availableQuery.trim().lowercase()
209- q.isEmpty() ||
210- it.label.lowercase().contains(q) ||
211- it.packageName.lowercase().contains(q)
212- }.sortedWith(compareBy({ it.label.lowercase() }, { it.packageName }))
213-
214198 Dialog (onDismissRequest = { showAppPicker = false }) {
215199 Surface (
216200 modifier = Modifier
@@ -231,86 +215,54 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
231215 style = MaterialTheme .typography.bodySmall,
232216 color = MaterialTheme .colorScheme.onSurface.copy(alpha = 0.7f )
233217 )
234-
235218 Row (horizontalArrangement = Arrangement .spacedBy(8 .dp)) {
236219 FilterChip (
237220 selected = activeTab == " SELECTED" ,
238221 onClick = { activeTab = " SELECTED" },
239222 label = { Text (" Selected ${selectedApps.size} " ) }
240- )
241- FilterChip (
242223 selected = activeTab == " AVAILABLE" ,
243224 onClick = { activeTab = " AVAILABLE" },
244225 label = { Text (" Available ${availableApps.size} " ) }
245- )
246- }
247-
248226 if (activeTab == " SELECTED" ) {
249227 OutlinedTextField (
250228 value = selectedQuery,
251229 onValueChange = { selectedQuery = it },
252230 label = { Text (" Search selected apps" ) },
253- modifier = Modifier .fillMaxWidth()
254- )
255231 } else {
256- OutlinedTextField (
257232 value = availableQuery,
258233 onValueChange = { availableQuery = it },
259234 label = { Text (" Search available apps" ) },
260- modifier = Modifier .fillMaxWidth()
261- )
262- }
263-
264- Row (horizontalArrangement = Arrangement .spacedBy(8 .dp)) {
265235 OutlinedButton (
266- onClick = {
267236 draftAppSelection = draftAppSelection.toMutableSet().apply {
268237 addAll(availableFiltered.map { it.packageName })
269- }
270- },
271238 modifier = Modifier .weight(1f )
272- ) {
273239 Text (" Select Visible" )
274- }
275- OutlinedButton (
276240 onClick = { draftAppSelection = mutableSetOf () },
277- modifier = Modifier .weight(1f )
278- ) {
279241 Text (" Select None" )
280- }
281- }
282-
283242 Surface (
284243 modifier = Modifier .fillMaxWidth(),
285244 shape = RoundedCornerShape (12 .dp),
286245 color = MaterialTheme .colorScheme.surfaceVariant.copy(alpha = 0.5f )
287- ) {
288246 Column (
289247 modifier = Modifier
290248 .fillMaxWidth()
291249 .padding(10 .dp)
292- ) {
293250 val appsToShow = if (activeTab == " SELECTED" ) selectedFiltered else availableFiltered
294251 val emptyText = if (activeTab == " SELECTED" ) {
295252 " No selected app matches your search"
296253 } else {
297254 " No available app matches your search"
298- }
299-
300255 Text (
301256 if (activeTab == " SELECTED" ) " Selected Apps" else " Available Apps" ,
302257 style = MaterialTheme .typography.labelLarge,
303258 color = MaterialTheme .colorScheme.primary
304- )
305-
306259 if (appsToShow.isEmpty()) {
307260 Text (
308261 emptyText,
309262 style = MaterialTheme .typography.bodySmall,
310263 color = MaterialTheme .colorScheme.onSurface.copy(alpha = 0.7f ),
311264 modifier = Modifier .padding(top = 8 .dp)
312265 )
313- } else {
314266 LazyColumn (
315267 modifier = Modifier
316268 .fillMaxWidth()
@@ -327,34 +279,14 @@ fun GlobalSettingsScreen(vm: GlobalSettingsViewModel = viewModel()) {
327279 }
328280 )
329281 }
330- }
331- }
332- }
333- }
334-
335282 Row (
336- modifier = Modifier .fillMaxWidth(),
337283 horizontalArrangement = Arrangement .End
338- ) {
339284 TextButton (onClick = { showAppPicker = false }) {
340285 Text (" Cancel" )
341- }
342- Button (
343- onClick = {
344286 draft = draft.copy(splitPackagesCsv = draftAppSelection.sorted().joinToString(" ," ))
345287 showAppPicker = false
346- }
347- ) {
348288 Text (" Apply" )
349- }
350- }
351- }
352- }
353- }
354- }
355289}
356-
357- @Composable
358290private fun LinkRow (title : String , link : String , onOpen : () -> Unit ) {
359291 Column (
360292 modifier = Modifier
@@ -367,7 +299,6 @@ private fun LinkRow(title: String, link: String, onOpen: () -> Unit) {
367299 style = MaterialTheme .typography.bodyMedium,
368300 color = MaterialTheme .colorScheme.onSurfaceVariant
369301 )
370- Text (
371302 text = link,
372303 style = MaterialTheme .typography.bodyMedium.copy(
373304 textDecoration = TextDecoration .Underline ,
@@ -376,11 +307,6 @@ private fun LinkRow(title: String, link: String, onOpen: () -> Unit) {
376307 color = MaterialTheme .colorScheme.primary,
377308 maxLines = 3 ,
378309 overflow = TextOverflow .Ellipsis
379- )
380- }
381- }
382-
383- @Composable
384310private fun AppRow (
385311 app : GlobalSettingsViewModel .AppEntry ,
386312 checked : Boolean ,
@@ -391,19 +317,14 @@ private fun AppRow(
391317 runCatching {
392318 context.packageManager.getApplicationIcon(app.packageName).toBitmap(48 , 48 )
393319 }.getOrNull()
394- }
395320 Row (
396- modifier = Modifier
397- .fillMaxWidth()
398321 .clickable { onToggle() }
399322 .padding(vertical = 4 .dp),
400323 horizontalArrangement = Arrangement .SpaceBetween ,
401324 verticalAlignment = Alignment .CenterVertically
402- ) {
403325 Row (
404326 modifier = Modifier .weight(1f ),
405327 verticalAlignment = Alignment .CenterVertically
406- ) {
407328 if (appIconBitmap != null ) {
408329 Image (
409330 bitmap = appIconBitmap.asImageBitmap(),
@@ -413,41 +334,23 @@ private fun AppRow(
413334 } else {
414335 Icon (
415336 imageVector = Icons .Filled .ArrowDropDown ,
416- contentDescription = null ,
417- modifier = Modifier .size(24 .dp)
418- )
419- }
420337 Spacer (modifier = Modifier .size(8 .dp))
421338 Column {
422339 Text (text = app.label)
423340 Text (text = app.packageName)
424- }
425- }
426341 Checkbox (
427342 checked = checked,
428343 onCheckedChange = { onToggle() }
429- )
430- }
431- }
432-
433- @Composable
434344private fun RowSwitch (title : String , checked : Boolean , onChecked : (Boolean ) -> Unit ) {
435- Row (
436345 modifier = Modifier .fillMaxWidth(),
437346 horizontalArrangement = Arrangement .SpaceBetween
438- ) {
439347 Text (title)
440348 Switch (checked = checked, onCheckedChange = onChecked)
441- }
442- }
443-
444349private fun parseCsv (value : String ): Set <String > {
445350 return value.split(" ," )
446351 .map { it.trim() }
447352 .filter { it.isNotBlank() }
448353 .toSet()
449- }
450-
451354private fun normalize (settings : GlobalSettings ): GlobalSettings {
452355 return settings.copy(
453356 connectionMode = settings.connectionMode.uppercase(),
@@ -458,4 +361,3 @@ private fun normalize(settings: GlobalSettings): GlobalSettings {
458361 .distinct()
459362 .joinToString(" ," )
460363 )
461- }
0 commit comments