@ -19,10 +19,14 @@ import com.utun.chat.ChatApplication
import com.utun.chat.data.NativeLib
import com.utun.chat.data.NativeLib
import com.utun.chat.data.ServerEntry
import com.utun.chat.data.ServerEntry
import com.utun.chat.data.LocalSocketState
import com.utun.chat.data.LocalSocketState
import com.utun.chat.headless.HeadlessService
import com.utun.chat.viewmodel.ChatViewModel
import com.utun.chat.viewmodel.ChatViewModel
import android.content.Intent
import androidx.core.content.ContextCompat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.delay
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking
import kotlinx.coroutines.withContext
import kotlinx.coroutines.withContext
import java.util.UUID
import java.util.UUID
@ -47,7 +51,7 @@ fun SettingsScreen(vm: ChatViewModel, onBack: () -> Unit, onExit: () -> Unit = {
val tabs = listOf ( " Settings " , " Audio " , " Storage " )
val tabs = listOf ( " Settings " , " Audio " , " Storage " )
val handleBack = {
val handleBack = {
if ( dirty ) {
if ( dirty && NativeLib . isReady ( ) ) {
scope . launch ( Dispatchers . IO ) {
scope . launch ( Dispatchers . IO ) {
val path = provider . getString ( " db.path " ) ?: " utun_chat.db "
val path = provider . getString ( " db.path " ) ?: " utun_chat.db "
val port = provider . getInt ( " control.port " )
val port = provider . getInt ( " control.port " )
@ -115,6 +119,7 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
var connectionMode by remember { mutableStateOf ( " manual " ) }
var connectionMode by remember { mutableStateOf ( " manual " ) }
val localSocketStates by vm . localSockets . collectAsState ( )
val localSocketStates by vm . localSockets . collectAsState ( )
val serviceEnabled by provider . serviceEnabledFlow . collectAsState ( initial = true )
LaunchedEffect ( Unit ) {
LaunchedEffect ( Unit ) {
nodeName = provider . getString ( " node.name " ) ?: provider . getDeviceName ( )
nodeName = provider . getString ( " node.name " ) ?: provider . getDeviceName ( )
@ -144,8 +149,9 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
connectionMode = mode ; onDirty ( true )
connectionMode = mode ; onDirty ( true )
scope . launch ( Dispatchers . IO ) {
scope . launch ( Dispatchers . IO ) {
provider . setConnectionMode ( mode )
provider . setConnectionMode ( mode )
NativeLib . restart ( provider . getString ( " db.path " ) ?: " " ,
if ( NativeLib . isReady ( ) )
provider . getInt ( " control.port " ) , provider . buildConfigText ( ) )
NativeLib . restart ( provider . getString ( " db.path " ) ?: " " ,
provider . getInt ( " control.port " ) , provider . buildConfigText ( ) )
}
}
}
}
val setDebug = { level : String ->
val setDebug = { level : String ->
@ -360,16 +366,43 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
}
}
Spacer ( Modifier . height ( 16. dp ) )
Spacer ( Modifier . height ( 16. dp ) )
/* ── Background service ── */
HorizontalDivider ( Modifier . padding ( vertical = 8. dp ) )
Text ( " Service " , style = MaterialTheme . typography . titleMedium )
Row ( verticalAlignment = Alignment . CenterVertically , horizontalArrangement = Arrangement . spacedBy ( 8. dp ) ) {
Text ( if ( serviceEnabled ) " Running " else " Stopped " , modifier = Modifier . weight ( 1f ) ,
color = if ( serviceEnabled ) Color ( 0xFF27AE60 ) else MaterialTheme . colorScheme . error ,
style = MaterialTheme . typography . bodyMedium )
Button ( onClick = {
runBlocking { provider . setServiceEnabled ( true ) }
ContextCompat . startForegroundService ( ChatApplication . instance ,
Intent ( ChatApplication . instance , HeadlessService :: class . java ) )
} , enabled = ! serviceEnabled ) { Text ( " Start service " ) }
Button ( onClick = {
runBlocking { provider . setServiceEnabled ( false ) }
ChatApplication . instance . stopService ( Intent ( ChatApplication . instance , HeadlessService :: class . java ) )
} , enabled = serviceEnabled ,
colors = ButtonDefaults . buttonColors ( containerColor = MaterialTheme . colorScheme . error ) ) { Text ( " Stop service " ) }
}
HorizontalDivider ( Modifier . padding ( vertical = 8. dp ) )
Button ( onClick = {
Button ( onClick = {
scope . launch ( Dispatchers . IO ) {
scope . launch ( Dispatchers . IO ) {
val path = provider . getString ( " db.path " ) ?: " " ; val port = provider . getInt ( " control.port " )
if ( NativeLib . isReady ( ) ) {
NativeLib . restart ( path , port , provider . buildConfigText ( ) )
val path = provider . getString ( " db.path " ) ?: " " ; val port = provider . getInt ( " control.port " )
NativeLib . restart ( path , port , provider . buildConfigText ( ) )
}
}
}
} , modifier = Modifier . fillMaxWidth ( ) , colors = ButtonDefaults . buttonColors ( containerColor = MaterialTheme . colorScheme . secondary ) ) {
} , modifier = Modifier . fillMaxWidth ( ) , colors = ButtonDefaults . buttonColors ( containerColor = MaterialTheme . colorScheme . secondary ) ) {
Icon ( Icons . Default . Refresh , null , modifier = Modifier . size ( 18. dp ) ) ; Spacer ( Modifier . width ( 8. dp ) ) ; Text ( " Restart instance " )
Icon ( Icons . Default . Refresh , null , modifier = Modifier . size ( 18. dp ) ) ; Spacer ( Modifier . width ( 8. dp ) ) ; Text ( " Restart instance " )
}
}
Spacer ( Modifier . height ( 8. dp ) )
Spacer ( Modifier . height ( 8. dp ) )
Button ( onClick = { NativeLib . stop ( ) ; onExit ( ) } , modifier = Modifier . fillMaxWidth ( ) ,
Button ( onClick = {
runBlocking { provider . setServiceEnabled ( false ) }
ChatApplication . instance . stopService ( Intent ( ChatApplication . instance , HeadlessService :: class . java ) )
onExit ( )
} , modifier = Modifier . fillMaxWidth ( ) ,
colors = ButtonDefaults . buttonColors ( containerColor = MaterialTheme . colorScheme . error ) ) {
colors = ButtonDefaults . buttonColors ( containerColor = MaterialTheme . colorScheme . error ) ) {
Icon ( Icons . Default . PowerSettingsNew , null , modifier = Modifier . size ( 18. dp ) ) ; Spacer ( Modifier . width ( 8. dp ) ) ; Text ( " Stop All & Exit " )
Icon ( Icons . Default . PowerSettingsNew , null , modifier = Modifier . size ( 18. dp ) ) ; Spacer ( Modifier . width ( 8. dp ) ) ; Text ( " Stop All & Exit " )
}
}
@ -389,9 +422,13 @@ private fun GeneralSettingsTab(vm: ChatViewModel, dirty: Boolean, onDirty: (Bool
confirmButton = { Button ( onClick = {
confirmButton = { Button ( onClick = {
showClearDb = false ; clearingDb = true
showClearDb = false ; clearingDb = true
scope . launch ( Dispatchers . IO ) {
scope . launch ( Dispatchers . IO ) {
vm . clearState ( ) ; NativeLib . stop ( ) ; NativeLib . clearDatabase ( )
vm . clearState ( )
val path = provider . getString ( " db.path " ) ?: " " ; val port = provider . getInt ( " control.port " )
val wasRunning = NativeLib . isReady ( )
NativeLib . restart ( path , port , provider . buildConfigText ( ) )
NativeLib . stop ( ) ; NativeLib . clearDatabase ( )
if ( wasRunning ) {
val path = provider . getString ( " db.path " ) ?: " " ; val port = provider . getInt ( " control.port " )
NativeLib . restart ( path , port , provider . buildConfigText ( ) )
}
withContext ( Dispatchers . Main ) { clearingDb = false }
withContext ( Dispatchers . Main ) { clearingDb = false }
}
}
} , enabled = ! clearingDb , colors = ButtonDefaults . buttonColors ( containerColor = MaterialTheme . colorScheme . error ) ) { Text ( " Delete All " ) } } ,
} , enabled = ! clearingDb , colors = ButtonDefaults . buttonColors ( containerColor = MaterialTheme . colorScheme . error ) ) { Text ( " Delete All " ) } } ,