@@ -68,6 +68,7 @@ import compose.icons.TablerIcons
6868import compose.icons.tablericons.ArrowLeft
6969import compose.icons.tablericons.Copy
7070import compose.icons.tablericons.DeviceMobile
71+ import compose.icons.tablericons.Edit
7172import compose.icons.tablericons.InfoCircle
7273import compose.icons.tablericons.Qrcode
7374import compose.icons.tablericons.Server
@@ -87,11 +88,17 @@ fun LocalNetworkServerScreen(
8788 val hapticFeedback = LocalHapticFeedback .current
8889 val isRunning by viewModel.isRunning.collectAsStateWithLifecycle()
8990 val serverUrl by viewModel.serverUrl.collectAsStateWithLifecycle()
91+ val serverPort by viewModel.serverPort.collectAsStateWithLifecycle()
9092 val isRtl = LocalLayoutDirection .current == LayoutDirection .Rtl
9193
9294 // Track if user wants to start the server (used for permission flow)
9395 var pendingStart by remember { mutableStateOf(false ) }
9496
97+ // Port configuration dialog
98+ var showPortDialog by remember { mutableStateOf(false ) }
99+ var portInput by remember { mutableStateOf(" " ) }
100+ var portError by remember { mutableStateOf(false ) }
101+
95102 // Request notification permission for Android 13+
96103 val notificationPermissionState =
97104 if (Build .VERSION .SDK_INT >= Build .VERSION_CODES .TIRAMISU ) {
@@ -142,9 +149,19 @@ fun LocalNetworkServerScreen(
142149 verticalArrangement = Arrangement .spacedBy(12 .dp),
143150 ) {
144151 // Server Status Card
145-
146152 ServerSwitch (isRunning, { pendingStart = it }, notificationPermissionState)
147153
154+ // Port Configuration Card
155+ PortConfigurationCard (
156+ currentPort = serverPort,
157+ isServerRunning = isRunning,
158+ onChangePort = {
159+ portInput = serverPort.toString()
160+ portError = false
161+ showPortDialog = true
162+ },
163+ )
164+
148165 // Server Details Section
149166 AnimatedVisibility (
150167 visible = isRunning && serverUrl != null ,
@@ -328,14 +345,86 @@ fun LocalNetworkServerScreen(
328345 }
329346 }
330347 }
348+
349+ // Port Configuration Dialog
350+ if (showPortDialog) {
351+ androidx.compose.material3.AlertDialog (
352+ onDismissRequest = { showPortDialog = false },
353+ title = { Text (stringResource(R .string.change_port)) },
354+ text = {
355+ Column (verticalArrangement = Arrangement .spacedBy(8 .dp)) {
356+ Text (
357+ text = stringResource(R .string.port_range_hint),
358+ style = MaterialTheme .typography.bodyMedium,
359+ color = MaterialTheme .colorScheme.onSurfaceVariant,
360+ )
361+ androidx.compose.material3.OutlinedTextField (
362+ value = portInput,
363+ onValueChange = {
364+ portInput = it
365+ portError = false
366+ },
367+ label = { Text (stringResource(R .string.port_number)) },
368+ placeholder = { Text (stringResource(R .string.default_port)) },
369+ isError = portError,
370+ supportingText =
371+ if (portError) {
372+ { Text (stringResource(R .string.invalid_port)) }
373+ } else {
374+ null
375+ },
376+ keyboardOptions =
377+ androidx.compose.foundation.text.KeyboardOptions (
378+ keyboardType = androidx.compose.ui.text.input.KeyboardType .Number ,
379+ ),
380+ singleLine = true ,
381+ modifier = Modifier .fillMaxWidth(),
382+ )
383+ if (isRunning) {
384+ Text (
385+ text = stringResource(R .string.port_changed_restart),
386+ style = MaterialTheme .typography.bodySmall,
387+ color = MaterialTheme .colorScheme.error,
388+ )
389+ }
390+ }
391+ },
392+ confirmButton = {
393+ androidx.compose.material3.TextButton (
394+ onClick = {
395+ val port = portInput.toIntOrNull()
396+ if (port != null && port in 1024 .. 65535 ) {
397+ viewModel.setServerPort(port)
398+ showPortDialog = false
399+ Toast
400+ .makeText(
401+ context,
402+ if (isRunning) context.getString(R .string.port_changed_restart) else context.getString(R .string.saved),
403+ Toast .LENGTH_SHORT ,
404+ ).show()
405+ } else {
406+ portError = true
407+ }
408+ },
409+ ) {
410+ Text (stringResource(R .string.save))
411+ }
412+ },
413+ dismissButton = {
414+ androidx.compose.material3.TextButton (onClick = { showPortDialog = false }) {
415+ Text (stringResource(R .string.cancel))
416+ }
417+ },
418+ )
419+ }
331420}
332421
333422@Preview()
334423@OptIn(ExperimentalPermissionsApi ::class )
335424@Composable
336425private fun ServerSwitch (
337- isRunning : Boolean ,
338- setPendingStart : (Boolean ) -> Unit ,
426+ isRunning : Boolean = false ,
427+ setPendingStart : (Boolean ) -> Unit = {} ,
339428 notificationPermissionState : PermissionState ? = null,
340429) {
341430 val hapticFeedback = LocalHapticFeedback .current
@@ -568,3 +657,58 @@ private fun ApiEndpointItem(
568657 }
569658 }
570659}
660+
661+ @Composable
662+ private fun PortConfigurationCard (
663+ currentPort : Int ,
664+ isServerRunning : Boolean ,
665+ onChangePort : () -> Unit ,
666+ ) {
667+ val hapticFeedback = LocalHapticFeedback .current
668+ Card (
669+ modifier = Modifier .fillMaxWidth(),
670+ colors =
671+ CardDefaults .cardColors(
672+ containerColor = MaterialTheme .colorScheme.surfaceContainer,
673+ ),
674+ ) {
675+ Row (
676+ verticalAlignment = Alignment .CenterVertically ,
677+ horizontalArrangement = Arrangement .spacedBy(12 .dp),
678+ modifier = Modifier .fillMaxWidth().padding(8 .dp),
679+ ) {
680+ Icon (
681+ TablerIcons .Server ,
682+ contentDescription = null ,
683+ tint = MaterialTheme .colorScheme.primary,
684+ modifier = Modifier .size(20 .dp),
685+ )
686+ Column (modifier = Modifier .weight(1f )) {
687+ Text (
688+ text = stringResource(R .string.server_port),
689+ style = MaterialTheme .typography.titleLarge,
690+ fontWeight = FontWeight .SemiBold ,
691+ )
692+ Text (
693+ text = " $currentPort " ,
694+ style = MaterialTheme .typography.bodyMedium,
695+ color = MaterialTheme .colorScheme.onSurfaceVariant,
696+ fontWeight = FontWeight .Medium ,
697+ )
698+ }
699+ IconButton (
700+ onClick = {
701+ hapticFeedback.performHapticFeedback(HapticFeedbackType .LongPress )
702+ onChangePort()
703+ },
704+ modifier = Modifier .size(40 .dp),
705+ ) {
706+ Icon (
707+ TablerIcons .Edit ,
708+ contentDescription = stringResource(R .string.change_port),
709+ tint = MaterialTheme .colorScheme.primary,
710+ )
711+ }
712+ }
713+ }
714+ }
0 commit comments