Skip to content

Commit e71e2e1

Browse files
Add server port configuration and management functionality (#238)
1 parent a42c8b9 commit e71e2e1

File tree

7 files changed

+211
-6
lines changed

7 files changed

+211
-6
lines changed

app/src/main/java/com/yogeshpaliyal/deepr/DeeprApplication.kt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class DeeprApplication : Application() {
8484
}
8585

8686
single<LocalServerRepository> {
87-
LocalServerRepositoryImpl(androidContext(), get(), get(), get())
87+
LocalServerRepositoryImpl(androidContext(), get(), get(), get(), get())
8888
}
8989

9090
viewModel {

app/src/main/java/com/yogeshpaliyal/deepr/preference/AppPreferenceDataStore.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ class AppPreferenceDataStore(
3030
private val LAST_BACKUP_TIME = longPreferencesKey("last_backup_time")
3131
private val DEFAULT_PAGE_FAVOURITES = booleanPreferencesKey("default_page_favourites")
3232
private val IS_THUMBNAIL_ENABLE = booleanPreferencesKey("is_thumbnail_enable")
33+
private val SERVER_PORT = stringPreferencesKey("server_port")
3334
}
3435

3536
val getSortingOrder: Flow<@SortType String> =
@@ -87,6 +88,11 @@ class AppPreferenceDataStore(
8788
preferences[IS_THUMBNAIL_ENABLE] ?: false
8889
}
8990

91+
val getServerPort: Flow<String> =
92+
context.appDataStore.data.map { preferences ->
93+
preferences[SERVER_PORT] ?: "" // Default to empty string
94+
}
95+
9096
suspend fun setSortingOrder(order: @SortType String) {
9197
context.appDataStore.edit { prefs ->
9298
prefs[SORTING_ORDER] = order
@@ -158,4 +164,10 @@ class AppPreferenceDataStore(
158164
prefs[IS_THUMBNAIL_ENABLE] = thumbnail
159165
}
160166
}
167+
168+
suspend fun setServerPort(port: String) {
169+
context.appDataStore.edit { prefs ->
170+
prefs[SERVER_PORT] = port
171+
}
172+
}
161173
}

app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepository.kt

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,11 @@ import kotlinx.coroutines.flow.StateFlow
55
interface LocalServerRepository {
66
val isRunning: StateFlow<Boolean>
77
val serverUrl: StateFlow<String?>
8+
val serverPort: StateFlow<Int>
89

910
suspend fun startServer()
1011

1112
suspend fun stopServer()
13+
14+
suspend fun setServerPort(port: Int)
1215
}

app/src/main/java/com/yogeshpaliyal/deepr/server/LocalServerRepositoryImpl.kt

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import android.net.wifi.WifiManager
55
import android.util.Log
66
import com.yogeshpaliyal.deepr.DeeprQueries
77
import com.yogeshpaliyal.deepr.data.NetworkRepository
8+
import com.yogeshpaliyal.deepr.preference.AppPreferenceDataStore
89
import com.yogeshpaliyal.deepr.viewmodel.AccountViewModel
910
import io.ktor.http.ContentType
1011
import io.ktor.http.HttpStatusCode
@@ -21,9 +22,12 @@ import io.ktor.server.response.respondText
2122
import io.ktor.server.routing.get
2223
import io.ktor.server.routing.post
2324
import io.ktor.server.routing.routing
25+
import kotlinx.coroutines.CoroutineScope
26+
import kotlinx.coroutines.Dispatchers
2427
import kotlinx.coroutines.flow.MutableStateFlow
2528
import kotlinx.coroutines.flow.StateFlow
2629
import kotlinx.coroutines.flow.asStateFlow
30+
import kotlinx.coroutines.launch
2731
import kotlinx.serialization.Serializable
2832
import kotlinx.serialization.json.Json
2933
import java.net.NetworkInterface
@@ -34,6 +38,7 @@ class LocalServerRepositoryImpl(
3438
private val deeprQueries: DeeprQueries,
3539
private val accountViewModel: AccountViewModel,
3640
private val networkRepository: NetworkRepository,
41+
private val preferenceDataStore: AppPreferenceDataStore,
3742
) : LocalServerRepository {
3843
private var server: EmbeddedServer<CIOApplicationEngine, CIOApplicationEngine.Configuration>? = null
3944
private val _isRunning = MutableStateFlow(false)
@@ -42,7 +47,29 @@ class LocalServerRepositoryImpl(
4247
private val _serverUrl = MutableStateFlow<String?>(null)
4348
override val serverUrl: StateFlow<String?> = _serverUrl.asStateFlow()
4449

45-
private val port = 8080
50+
private val _serverPort = MutableStateFlow(8080)
51+
override val serverPort: StateFlow<Int> = _serverPort.asStateFlow()
52+
53+
init {
54+
// Load saved port on initialization
55+
CoroutineScope(Dispatchers.IO).launch {
56+
preferenceDataStore.getServerPort.collect { portString ->
57+
val port = portString.toIntOrNull()
58+
if (port != null && port in 1024..65535) {
59+
_serverPort.value = port
60+
} else {
61+
_serverPort.value = 8080
62+
}
63+
}
64+
}
65+
}
66+
67+
override suspend fun setServerPort(port: Int) {
68+
if (port in 1024..65535) {
69+
_serverPort.value = port
70+
preferenceDataStore.setServerPort(port.toString())
71+
}
72+
}
4673

4774
override suspend fun startServer() {
4875
if (_isRunning.value) {
@@ -57,6 +84,8 @@ class LocalServerRepositoryImpl(
5784
return
5885
}
5986

87+
val port = _serverPort.value
88+
6089
server =
6190
embeddedServer(CIO, host = "0.0.0.0", port = port) {
6291
install(ContentNegotiation) {

app/src/main/java/com/yogeshpaliyal/deepr/ui/screens/LocalNetworkServer.kt

Lines changed: 147 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ import compose.icons.TablerIcons
6868
import compose.icons.tablericons.ArrowLeft
6969
import compose.icons.tablericons.Copy
7070
import compose.icons.tablericons.DeviceMobile
71+
import compose.icons.tablericons.Edit
7172
import compose.icons.tablericons.InfoCircle
7273
import compose.icons.tablericons.Qrcode
7374
import 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
336425
private 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+
}
Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,20 @@
11
package com.yogeshpaliyal.deepr.viewmodel
22

33
import androidx.lifecycle.ViewModel
4+
import androidx.lifecycle.viewModelScope
45
import com.yogeshpaliyal.deepr.server.LocalServerRepository
6+
import kotlinx.coroutines.launch
57

68
class LocalServerViewModel(
7-
localServerRepository: LocalServerRepository,
9+
private val localServerRepository: LocalServerRepository,
810
) : ViewModel() {
911
val isRunning = localServerRepository.isRunning
1012
val serverUrl = localServerRepository.serverUrl
13+
val serverPort = localServerRepository.serverPort
14+
15+
fun setServerPort(port: Int) {
16+
viewModelScope.launch {
17+
localServerRepository.setServerPort(port)
18+
}
19+
}
1120
}

app/src/main/res/values/strings.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,4 +205,12 @@
205205
<string name="local_server_notification_text">Server URL: %s</string>
206206
<string name="local_server_starting">Starting server…</string>
207207
<string name="stop">Stop</string>
208+
<string name="server_port">Server Port</string>
209+
<string name="custom_port">Custom Port</string>
210+
<string name="port_number">Port Number</string>
211+
<string name="default_port">Default: 8080</string>
212+
<string name="port_range_hint">Enter port (1024-65535)</string>
213+
<string name="invalid_port">Invalid port number. Must be between 1024 and 65535.</string>
214+
<string name="port_changed_restart">Port changed. Please restart the server to apply changes.</string>
215+
<string name="change_port">Change Port</string>
208216
</resources>

0 commit comments

Comments
 (0)