diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 46bbd8b..a1012e2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -108,6 +108,7 @@ dependencies { implementation(libs.okhttp) implementation(libs.androidx.material3.android) implementation(libs.androidx.material.icons.core) + implementation(libs.androidx.material.icons.extended) implementation(libs.kotlinx.serialization.json) implementation(libs.androidx.ui.tooling.preview.android) implementation(libs.androidx.navigation.compose) diff --git a/app/src/main/java/app/lonecloud/prism/PrismPreferences.kt b/app/src/main/java/app/lonecloud/prism/PrismPreferences.kt index 5c755b8..17f97a8 100644 --- a/app/src/main/java/app/lonecloud/prism/PrismPreferences.kt +++ b/app/src/main/java/app/lonecloud/prism/PrismPreferences.kt @@ -97,6 +97,14 @@ class PrismPreferences(context: Context) : putOrRemove(PREF_PRISM_API_KEY, value) } + var introCompleted: Boolean + get() = sharedPreferences + .getBoolean(PREF_INTRO_COMPLETED, false) + set(value) = sharedPreferences + .edit { + putBoolean(PREF_INTRO_COMPLETED, value) + } + override fun wipe() { uaid = null } @@ -113,5 +121,6 @@ class PrismPreferences(context: Context) : private const val PREF_SHOW_TOASTS = "show_toasts" private const val PREF_PRISM_SERVER_URL = "prism_server_url" private const val PREF_PRISM_API_KEY = "prism_api_key" + private const val PREF_INTRO_COMPLETED = "intro_completed" } } diff --git a/app/src/main/java/app/lonecloud/prism/PrismServerClient.kt b/app/src/main/java/app/lonecloud/prism/PrismServerClient.kt index 81c6d2d..7b30d2a 100644 --- a/app/src/main/java/app/lonecloud/prism/PrismServerClient.kt +++ b/app/src/main/java/app/lonecloud/prism/PrismServerClient.kt @@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import okhttp3.MediaType.Companion.toMediaType import okhttp3.OkHttpClient import okhttp3.Request @@ -54,7 +55,7 @@ object PrismServerClient { } val request = Request.Builder() - .url("$serverUrl/webpush/app") + .url("$serverUrl/api/v1/webpush/app") .addHeader("Authorization", getAuthHeader(apiKey)) .addHeader("Content-Type", "application/json") .post(json.toString().toRequestBody("application/json".toMediaType())) @@ -65,17 +66,17 @@ object PrismServerClient { client.newCall(request).execute().use { response -> if (response.isSuccessful) { Log.d(TAG, "Successfully registered app: $appName") - onSuccess() + withContext(Dispatchers.Main) { onSuccess() } } else { val error = "Failed to register app: ${response.code} ${response.message}" Log.e(TAG, error) - onError(error) + withContext(Dispatchers.Main) { onError(error) } } } } catch (e: IOException) { val error = "Error registering app: ${e.message}" Log.e(TAG, error, e) - onError(error) + withContext(Dispatchers.Main) { onError(error) } } } } @@ -135,7 +136,7 @@ object PrismServerClient { CoroutineScope(Dispatchers.IO).launch { try { val request = Request.Builder() - .url("$url/webpush/app/$appName") + .url("$url/api/v1/webpush/app/$appName") .addHeader("Authorization", getAuthHeader(key)) .delete() .build() @@ -145,17 +146,17 @@ object PrismServerClient { client.newCall(request).execute().use { response -> if (response.isSuccessful) { Log.d(TAG, "Successfully deleted app: $appName") - onSuccess() + withContext(Dispatchers.Main) { onSuccess() } } else { val error = "Failed to delete app: ${response.code} ${response.message}" Log.e(TAG, error) - onError(error) + withContext(Dispatchers.Main) { onError(error) } } } } catch (e: IOException) { val error = "Error deleting app: ${e.message}" Log.e(TAG, error, e) - onError(error) + withContext(Dispatchers.Main) { onError(error) } } } } @@ -201,7 +202,7 @@ object PrismServerClient { ) { CoroutineScope(Dispatchers.IO).launch { try { - val healthUrl = "$serverUrl/api/health" + val healthUrl = "$serverUrl/api/v1/health" val request = Request.Builder() .url(healthUrl) .addHeader("Authorization", getAuthHeader(apiKey)) @@ -210,13 +211,17 @@ object PrismServerClient { client.newCall(request).execute().use { response -> if (response.isSuccessful) { - onSuccess() + withContext(Dispatchers.Main) { onSuccess() } } else { - onError("Connection failed: ${response.code} ${response.message}") + withContext(Dispatchers.Main) { + onError("Connection failed: ${response.code} ${response.message}") + } } } } catch (e: IOException) { - onError("Connection error: ${e.message}") + withContext(Dispatchers.Main) { + onError("Connection error: ${e.message}") + } } } } diff --git a/app/src/main/java/app/lonecloud/prism/activities/MainActivity.kt b/app/src/main/java/app/lonecloud/prism/activities/MainActivity.kt index 400deba..6130d1e 100644 --- a/app/src/main/java/app/lonecloud/prism/activities/MainActivity.kt +++ b/app/src/main/java/app/lonecloud/prism/activities/MainActivity.kt @@ -9,12 +9,20 @@ package app.lonecloud.prism.activities import android.os.Bundle +import android.util.Log import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.activity.enableEdgeToEdge import androidx.lifecycle.viewmodel.compose.viewModel import app.lonecloud.prism.activities.ui.App import app.lonecloud.prism.activities.ui.theme.AppTheme +import app.lonecloud.prism.utils.TAG +import kotlin.system.exitProcess +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import org.unifiedpush.android.distributor.ipc.InternalMessenger class MainActivity : ComponentActivity() { @@ -24,6 +32,11 @@ class MainActivity : ComponentActivity() { super.onCreate(savedInstanceState) messenger = InternalMessenger(this) + jobs.removeAll { + Log.d(TAG, "Cancelling exitProcess job") + it.cancel() + true + } enableEdgeToEdge() @@ -39,6 +52,15 @@ class MainActivity : ComponentActivity() { } override fun onDestroy() { + Log.d(TAG, "Destroy") super.onDestroy() + jobs += CoroutineScope(Dispatchers.Main + Job()).launch { + delay(10_000) + exitProcess(0) + } + } + + companion object { + val jobs = emptyList().toMutableList() } } diff --git a/app/src/main/java/app/lonecloud/prism/activities/MainViewModel.kt b/app/src/main/java/app/lonecloud/prism/activities/MainViewModel.kt index bf1ec7b..743c84e 100644 --- a/app/src/main/java/app/lonecloud/prism/activities/MainViewModel.kt +++ b/app/src/main/java/app/lonecloud/prism/activities/MainViewModel.kt @@ -6,8 +6,6 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.util.Log import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableIntStateOf -import androidx.compose.runtime.mutableLongStateOf import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.setValue import androidx.lifecycle.ViewModel @@ -55,14 +53,13 @@ class MainViewModel( var mainUiState by mutableStateOf(mainUiState) + var selectedApp by mutableStateOf(null) + private set + fun updatePrismServerConfigured(configured: Boolean) { mainUiState = mainUiState.copy(prismServerConfigured = configured) } - private var lastDebugClickTime by mutableLongStateOf(0L) - - private var debugClickCount by mutableIntStateOf(0) - init { loadInstalledApps() } @@ -123,21 +120,20 @@ class MainViewModel( mainUiState = mainUiState.copy(showAppDetails = false) } - fun addDebugClick() { - val currentTime = System.currentTimeMillis() - if (currentTime - lastDebugClickTime < 500) { - debugClickCount++ - if (debugClickCount == 5) { - mainUiState = mainUiState.copy(showDebugInfo = true) - } - } else { - debugClickCount = 1 - } - lastDebugClickTime = currentTime + fun selectApp(app: InstalledApp) { + selectedApp = app } - fun dismissDebugInfo() { - mainUiState = mainUiState.copy(showDebugInfo = false) + fun clearSelectedApp() { + selectedApp = null + } + + fun addManualApp( + name: String, + packageName: String, + description: String? + ) { + addApp(name, packageName, description) } private fun hasUnifiedPushSupport(pm: PackageManager, packageName: String): Boolean { @@ -174,14 +170,6 @@ class MainViewModel( } } - fun showAddAppDialog() { - mainUiState = mainUiState.copy(showAddAppDialog = true) - } - - fun hideAddAppDialog() { - mainUiState = mainUiState.copy(showAddAppDialog = false) - } - fun addApp( name: String, targetPackageName: String, @@ -220,7 +208,6 @@ class MainViewModel( ) refreshRegistrations() - hideAddAppDialog() var endpoint: String? var attempts = 0 diff --git a/app/src/main/java/app/lonecloud/prism/activities/SettingsViewModel.kt b/app/src/main/java/app/lonecloud/prism/activities/SettingsViewModel.kt index 55f9eaf..3329ab4 100644 --- a/app/src/main/java/app/lonecloud/prism/activities/SettingsViewModel.kt +++ b/app/src/main/java/app/lonecloud/prism/activities/SettingsViewModel.kt @@ -38,7 +38,7 @@ class SettingsViewModel( } } - fun updatePrismServerUrl(url: String) { + fun updatePrismServerUrl(url: String, sendAction: Boolean = true) { viewModelScope.launch { val trimmedUrl = url.trim() state = state.copy(prismServerUrl = trimmedUrl) @@ -55,12 +55,14 @@ class SettingsViewModel( PrismServerClient.registerAllApps(it) } - sendUiAction(it, "UpdatePrismServerConfigured") + if (sendAction) { + sendUiAction(it, "UpdatePrismServerConfigured") + } } } } - fun updatePrismApiKey(apiKey: String) { + fun updatePrismApiKey(apiKey: String, sendAction: Boolean = true) { viewModelScope.launch { val trimmedKey = apiKey.trim() state = state.copy(prismApiKey = trimmedKey) @@ -77,7 +79,46 @@ class SettingsViewModel( PrismServerClient.registerAllApps(it) } - sendUiAction(it, "UpdatePrismServerConfigured") + if (sendAction) { + sendUiAction(it, "UpdatePrismServerConfigured") + } + } + } + } + + fun savePrismConfig(url: String, apiKey: String) { + viewModelScope.launch { + val trimmedUrl = url.trim() + val trimmedKey = apiKey.trim() + + state = state.copy( + prismServerUrl = trimmedUrl, + prismApiKey = trimmedKey + ) + + application?.let { app -> + PrismPreferences(app).apply { + prismServerUrl = trimmedUrl.ifBlank { null } + prismApiKey = trimmedKey.ifBlank { null } + } + + val urlIntent = Intent(PrismConfigReceiver.ACTION_SET_PRISM_SERVER_URL).apply { + putExtra(PrismConfigReceiver.EXTRA_URL, trimmedUrl) + setPackage(app.packageName) + } + app.sendBroadcast(urlIntent) + + val keyIntent = Intent(PrismConfigReceiver.ACTION_SET_PRISM_API_KEY).apply { + putExtra(PrismConfigReceiver.EXTRA_API_KEY, trimmedKey) + setPackage(app.packageName) + } + app.sendBroadcast(keyIntent) + + if (trimmedUrl.isNotBlank() && trimmedKey.isNotBlank()) { + PrismServerClient.registerAllApps(app) + } + + sendUiAction(app, "UpdatePrismServerConfigured") } } } diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/AddAppDialog.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/AddAppDialog.kt deleted file mode 100644 index 231d69b..0000000 --- a/app/src/main/java/app/lonecloud/prism/activities/ui/AddAppDialog.kt +++ /dev/null @@ -1,127 +0,0 @@ -package app.lonecloud.prism.activities.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedCard -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.core.graphics.drawable.toBitmap -import app.lonecloud.prism.R - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun AddAppDialog( - installedApps: List, - onDismiss: () -> Unit, - onConfirm: (name: String, packageName: String, description: String?) -> Unit -) { - var name by remember { mutableStateOf("") } - var selectedApp by remember { mutableStateOf(null) } - var showAppPicker by remember { mutableStateOf(false) } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.add_custom_app_title)) }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(12.dp) - ) { - OutlinedTextField( - value = name, - onValueChange = { name = it }, - label = { Text(stringResource(R.string.app_name_label)) }, - placeholder = { Text(stringResource(R.string.app_name_placeholder)) }, - singleLine = true, - modifier = Modifier.fillMaxWidth() - ) - - OutlinedCard( - onClick = { showAppPicker = true }, - modifier = Modifier.fillMaxWidth() - ) { - Row( - modifier = Modifier - .fillMaxWidth() - .padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, - verticalAlignment = Alignment.CenterVertically - ) { - Column { - Text( - text = stringResource(R.string.target_app_label), - style = MaterialTheme.typography.labelMedium, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - Text( - text = selectedApp?.appName ?: stringResource(R.string.select_an_app), - style = MaterialTheme.typography.bodyLarge - ) - } - selectedApp?.icon?.let { icon -> - val bitmap = icon.toBitmap(48, 48) - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = null, - modifier = Modifier.size(48.dp) - ) - } - } - } - } - }, - confirmButton = { - TextButton( - onClick = { - if (name.isNotBlank()) { - onConfirm( - name.trim(), - selectedApp?.packageName ?: "", - null - ) - } - }, - enabled = name.isNotBlank() - ) { - Text(stringResource(R.string.add_button)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel_button)) - } - } - ) - - if (showAppPicker) { - AppPickerDialog( - apps = installedApps, - onDismiss = { showAppPicker = false }, - onSelect = { app -> - selectedApp = app - if (name.isBlank()) { - name = app.appName - } - showAppPicker = false - } - ) - } -} diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/AddAppScreen.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/AddAppScreen.kt new file mode 100644 index 0000000..653bac9 --- /dev/null +++ b/app/src/main/java/app/lonecloud/prism/activities/ui/AddAppScreen.kt @@ -0,0 +1,113 @@ +package app.lonecloud.prism.activities.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedCard +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import app.lonecloud.prism.R + +@Composable +fun AddAppScreen( + selectedApp: InstalledApp?, + onNavigateBack: () -> Unit, + onNavigateToAppPicker: () -> Unit, + onConfirm: (name: String, packageName: String, description: String?) -> Unit +) { + var name by remember { mutableStateOf("") } + + // Auto-fill name when app is selected + androidx.compose.runtime.LaunchedEffect(selectedApp) { + if (name.isBlank() && selectedApp != null) { + name = selectedApp.appName + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + OutlinedTextField( + value = name, + onValueChange = { name = it }, + label = { Text(stringResource(R.string.app_name_label)) }, + placeholder = { Text(stringResource(R.string.app_name_placeholder)) }, + singleLine = true, + modifier = Modifier.fillMaxWidth() + ) + + OutlinedCard( + onClick = onNavigateToAppPicker, + modifier = Modifier.fillMaxWidth() + ) { + Row( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + horizontalArrangement = Arrangement.SpaceBetween, + verticalAlignment = Alignment.CenterVertically + ) { + Column { + Text( + text = stringResource(R.string.target_app_label), + style = MaterialTheme.typography.labelMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + Text( + text = selectedApp?.appName ?: stringResource(R.string.select_an_app), + style = MaterialTheme.typography.bodyLarge + ) + } + selectedApp?.icon?.let { icon -> + val bitmap = icon.toBitmap(48, 48) + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = null, + modifier = Modifier.size(48.dp) + ) + } + } + } + + Button( + onClick = { + if (name.isNotBlank()) { + onConfirm( + name.trim(), + selectedApp?.packageName ?: "", + null + ) + onNavigateBack() + } + }, + enabled = name.isNotBlank(), + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.add_button)) + } + } +} diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/AppPickerDialog.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/AppPickerDialog.kt deleted file mode 100644 index 9ab44b0..0000000 --- a/app/src/main/java/app/lonecloud/prism/activities/ui/AppPickerDialog.kt +++ /dev/null @@ -1,106 +0,0 @@ -package app.lonecloud.prism.activities.ui - -import androidx.compose.foundation.Image -import androidx.compose.foundation.clickable -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.dp -import androidx.core.graphics.drawable.toBitmap -import app.lonecloud.prism.R - -@Composable -fun AppPickerDialog( - apps: List, - onDismiss: () -> Unit, - onSelect: (InstalledApp) -> Unit -) { - var searchQuery by remember { mutableStateOf("") } - val filteredApps = remember(apps, searchQuery) { - if (searchQuery.isBlank()) { - apps - } else { - apps.filter { app -> - app.appName.contains(searchQuery, ignoreCase = true) || - app.packageName.contains(searchQuery, ignoreCase = true) - } - } - } - - AlertDialog( - onDismissRequest = onDismiss, - modifier = Modifier.fillMaxWidth(), - title = { Text(stringResource(R.string.select_target_app_title)) }, - text = { - Column { - OutlinedTextField( - value = searchQuery, - onValueChange = { searchQuery = it }, - label = { Text(stringResource(R.string.search_apps_label)) }, - placeholder = { Text(stringResource(R.string.search_apps_placeholder)) }, - singleLine = true, - modifier = Modifier - .fillMaxWidth() - .padding(bottom = 8.dp) - ) - - LazyColumn { - items(filteredApps) { app -> - Row( - modifier = Modifier - .fillMaxWidth() - .clickable { onSelect(app) } - .padding(12.dp), - horizontalArrangement = Arrangement.spacedBy(12.dp), - verticalAlignment = Alignment.CenterVertically - ) { - app.icon?.let { icon -> - val bitmap = icon.toBitmap(48, 48) - Image( - bitmap = bitmap.asImageBitmap(), - contentDescription = null, - modifier = Modifier.size(48.dp) - ) - } - Column { - Text( - text = app.appName, - style = MaterialTheme.typography.bodyLarge - ) - Text( - text = app.packageName, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - } - } - } - }, - confirmButton = { - TextButton(onClick = onDismiss) { - Text(stringResource(R.string.cancel_button)) - } - } - ) -} diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/AppPickerScreen.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/AppPickerScreen.kt new file mode 100644 index 0000000..8fbcd4b --- /dev/null +++ b/app/src/main/java/app/lonecloud/prism/activities/ui/AppPickerScreen.kt @@ -0,0 +1,102 @@ +package app.lonecloud.prism.activities.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.asImageBitmap +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import androidx.core.graphics.drawable.toBitmap +import app.lonecloud.prism.R + +@Composable +fun AppPickerScreen( + apps: List, + onNavigateBack: () -> Unit, + onSelect: (InstalledApp) -> Unit +) { + var searchQuery by remember { mutableStateOf("") } + val filteredApps = remember(apps, searchQuery) { + if (searchQuery.isBlank()) { + apps + } else { + apps.filter { app -> + app.appName.contains(searchQuery, ignoreCase = true) || + app.packageName.contains(searchQuery, ignoreCase = true) + } + } + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + ) { + OutlinedTextField( + value = searchQuery, + onValueChange = { searchQuery = it }, + label = { Text(stringResource(R.string.search_apps_label)) }, + placeholder = { Text(stringResource(R.string.search_apps_placeholder)) }, + singleLine = true, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 16.dp) + ) + + LazyColumn( + verticalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(filteredApps) { app -> + Row( + modifier = Modifier + .fillMaxWidth() + .clickable { + onSelect(app) + onNavigateBack() + } + .padding(12.dp), + horizontalArrangement = Arrangement.spacedBy(12.dp), + verticalAlignment = Alignment.CenterVertically + ) { + app.icon?.let { icon -> + val bitmap = icon.toBitmap(48, 48) + Image( + bitmap = bitmap.asImageBitmap(), + contentDescription = null, + modifier = Modifier.size(48.dp) + ) + } + Column { + Text( + text = app.appName, + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = app.packageName, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } + } + } + } + } +} diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/AppScreen.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/AppScreen.kt index 8b33cbd..057eb28 100644 --- a/app/src/main/java/app/lonecloud/prism/activities/ui/AppScreen.kt +++ b/app/src/main/java/app/lonecloud/prism/activities/ui/AppScreen.kt @@ -21,6 +21,7 @@ import androidx.compose.material3.Icon import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource @@ -32,6 +33,7 @@ import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.rememberNavController +import app.lonecloud.prism.PrismPreferences import app.lonecloud.prism.R import app.lonecloud.prism.activities.MainViewModel import app.lonecloud.prism.activities.PreviewFactory @@ -42,8 +44,12 @@ import org.unifiedpush.android.distributor.ui.compose.AppBar import org.unifiedpush.android.distributor.ui.vm.DistribMigrationViewModel enum class AppScreen(@param:StringRes val title: Int) { + Intro(R.string.app_name), Main(R.string.app_name), - Settings(R.string.settings) + Settings(R.string.settings), + ServerConfig(R.string.configure_server), + AddApp(R.string.add_custom_app_title), + AppPicker(R.string.select_target_app_title) } @OptIn(ExperimentalMaterial3Api::class) @@ -86,6 +92,8 @@ fun App( ) { val context = LocalContext.current val uiActionsFlow = subscribeUiActions(context) + val prefs = remember { PrismPreferences(context) } + val startDestination = if (prefs.introCompleted) AppScreen.Main.name else AppScreen.Intro.name val backStackEntry by navController.currentBackStackEntryAsState() val currentScreen = AppScreen.valueOf( @@ -96,26 +104,34 @@ fun App( Scaffold( topBar = { - when (currentScreen) { - AppScreen.Main -> { - MainAppBarOrSelection( - mainViewModel, - onGoToSettings = { - navController.navigate(AppScreen.Settings.name) - } - ) - } - else -> null - } ?: DefaultTopBar( - currentScreen, - canNavigateBack = navController.previousBackStackEntry != null, - navigateUp = { navController.navigateUp() } - ) + if (currentScreen == AppScreen.Intro) { + null + } else { + when (currentScreen) { + AppScreen.Main -> { + MainAppBarOrSelection( + mainViewModel, + onGoToSettings = { + navController.navigate(AppScreen.Settings.name) + } + ) + } + else -> null + } ?: DefaultTopBar( + currentScreen, + canNavigateBack = navController.previousBackStackEntry != null, + navigateUp = { navController.navigateUp() } + ) + } }, floatingActionButton = { - if (currentScreen == AppScreen.Main && mainViewModel.mainUiState.prismServerConfigured) { + val prefs = PrismPreferences(context) + if (currentScreen == AppScreen.Main && !prefs.prismServerUrl.isNullOrBlank() && !prefs.prismApiKey.isNullOrBlank()) { FloatingActionButton( - onClick = { mainViewModel.showAddAppDialog() } + onClick = { + mainViewModel.clearSelectedApp() + navController.navigate(AppScreen.AddApp.name) + } ) { Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_manual_app_content_description)) } @@ -125,24 +141,42 @@ fun App( ) { innerPadding -> NavHost( navController = navController, - startDestination = AppScreen.Main.name, + startDestination = startDestination, modifier = Modifier .fillMaxSize() .padding(innerPadding) ) { + composable(route = AppScreen.Intro.name) { + val settingsViewModel = viewModel(factory = factory) + IntroScreen( + onComplete = { url, apiKey -> + PrismPreferences(context).introCompleted = true + settingsViewModel.savePrismConfig(url, apiKey) + navController.navigate(AppScreen.Main.name) { + popUpTo(AppScreen.Intro.name) { inclusive = true } + } + }, + onSkip = { + PrismPreferences(context).introCompleted = true + navController.navigate(AppScreen.Main.name) { + popUpTo(AppScreen.Intro.name) { inclusive = true } + } + } + ) + } composable( route = AppScreen.Main.name, exitTransition = { when (targetState.destination.route) { - AppScreen.Settings.name -> slideOutFrom( - Dir.Right - ) + AppScreen.Settings.name -> slideOutFrom(Dir.Right) + AppScreen.AddApp.name -> slideOutFrom(Dir.Right) else -> fadeOut() } }, popEnterTransition = { when (initialState.destination.route) { AppScreen.Settings.name -> slideInTo(Dir.Right) + AppScreen.AddApp.name -> slideInTo(Dir.Right) else -> fadeIn() } } @@ -156,10 +190,91 @@ fun App( composable( route = AppScreen.Settings.name, enterTransition = { slideInTo(Dir.Left) }, + exitTransition = { + when (targetState.destination.route) { + AppScreen.ServerConfig.name -> slideOutFrom(Dir.Right) + else -> fadeOut() + } + }, + popEnterTransition = { + when (initialState.destination.route) { + AppScreen.ServerConfig.name -> slideInTo(Dir.Right) + else -> fadeIn() + } + }, popExitTransition = { slideOutFrom(Dir.Left) } ) { val vm = viewModel(factory = factory) - SettingsScreen(vm, themeViewModel, migrationViewModel) + SettingsScreen( + vm, + themeViewModel, + migrationViewModel, + onNavigateToServerConfig = { + navController.navigate(AppScreen.ServerConfig.name) + } + ) + } + composable( + route = AppScreen.ServerConfig.name, + enterTransition = { slideInTo(Dir.Left) }, + popEnterTransition = { slideInTo(Dir.Right) }, + popExitTransition = { slideOutFrom(Dir.Left) } + ) { + val settingsEntry = remember(it) { + navController.getBackStackEntry(AppScreen.Settings.name) + } + val vm = viewModel( + viewModelStoreOwner = settingsEntry, + factory = factory + ) + ServerConfigScreen( + initialUrl = vm.state.prismServerUrl, + initialApiKey = vm.state.prismApiKey, + onNavigateBack = { navController.navigateUp() }, + onSave = { url, apiKey -> vm.savePrismConfig(url, apiKey) } + ) + } + composable( + route = AppScreen.AddApp.name, + enterTransition = { slideInTo(Dir.Left) }, + exitTransition = { + when (targetState.destination.route) { + AppScreen.AppPicker.name -> slideOutFrom(Dir.Right) + else -> fadeOut() + } + }, + popEnterTransition = { + when (initialState.destination.route) { + AppScreen.AppPicker.name -> slideInTo(Dir.Right) + else -> fadeIn() + } + }, + popExitTransition = { slideOutFrom(Dir.Left) } + ) { + AddAppScreen( + selectedApp = mainViewModel.selectedApp, + onNavigateBack = { navController.navigateUp() }, + onNavigateToAppPicker = { + navController.navigate(AppScreen.AppPicker.name) + }, + onConfirm = { name, packageName, description -> + mainViewModel.addManualApp(name, packageName, description) + } + ) + } + composable( + route = AppScreen.AppPicker.name, + enterTransition = { slideInTo(Dir.Left) }, + popEnterTransition = { slideInTo(Dir.Right) }, + popExitTransition = { slideOutFrom(Dir.Left) } + ) { + AppPickerScreen( + apps = mainViewModel.mainUiState.installedApps, + onNavigateBack = { navController.navigateUp() }, + onSelect = { app -> + mainViewModel.selectApp(app) + } + ) } } } diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/DebugDialog.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/DebugDialog.kt deleted file mode 100644 index f6b99a6..0000000 --- a/app/src/main/java/app/lonecloud/prism/activities/ui/DebugDialog.kt +++ /dev/null @@ -1,31 +0,0 @@ -package app.lonecloud.prism.activities.ui - -import androidx.compose.foundation.text.selection.SelectionContainer -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.ui.res.stringResource -import app.lonecloud.prism.R -import app.lonecloud.prism.utils.getDebugInfo - -@Composable -fun DebugDialog(onDismissRequest: () -> Unit) { - val text = getDebugInfo() - AlertDialog( - title = { Text(stringResource(R.string.debug_title)) }, - text = { - SelectionContainer { - Text(text) - } - }, - onDismissRequest = onDismissRequest, - confirmButton = { - TextButton( - onClick = onDismissRequest - ) { - Text(stringResource(android.R.string.ok)) - } - } - ) -} diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/IntroScreen.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/IntroScreen.kt new file mode 100644 index 0000000..870a0fe --- /dev/null +++ b/app/src/main/java/app/lonecloud/prism/activities/ui/IntroScreen.kt @@ -0,0 +1,171 @@ +package app.lonecloud.prism.activities.ui + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.style.TextAlign +import androidx.compose.ui.unit.dp +import app.lonecloud.prism.R +import app.lonecloud.prism.activities.ui.components.PasswordTextField +import app.lonecloud.prism.utils.normalizeUrl +import app.lonecloud.prism.utils.testServerConnection + +@Composable +fun IntroScreen(onComplete: (url: String, apiKey: String) -> Unit, onSkip: () -> Unit) { + var url by remember { mutableStateOf("") } + var apiKey by remember { mutableStateOf("") } + var isTesting by remember { mutableStateOf(false) } + var testResult by remember { mutableStateOf(null) } + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + val successMessage = stringResource(R.string.connection_successful) + val failedMessageTemplate = stringResource(R.string.connection_failed) + + fun testAndSave(normalizedUrl: String) { + isTesting = true + testServerConnection( + normalizedUrl, + apiKey, + onSuccess = { + isTesting = false + testResult = successMessage + onComplete(normalizedUrl, apiKey) + }, + onError = { error -> + isTesting = false + testResult = failedMessageTemplate.replace("%s", error) + } + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(24.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(20.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(modifier = Modifier.height(32.dp)) + + Image( + painter = painterResource(R.drawable.app_logo), + contentDescription = stringResource(R.string.app_name), + modifier = Modifier.size(120.dp) + ) + + Text( + text = stringResource(R.string.intro_welcome_message), + style = MaterialTheme.typography.bodyLarge, + textAlign = TextAlign.Center, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + PrismInfoWithLink(uriHandler = uriHandler) + + OutlinedTextField( + value = url, + onValueChange = { + url = it + testResult = null + }, + label = { Text(stringResource(R.string.prism_server_url_label)) }, + placeholder = { Text(stringResource(R.string.prism_server_url_placeholder)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !isTesting + ) + + PasswordTextField( + value = apiKey, + onValueChange = { + apiKey = it + testResult = null + }, + label = stringResource(R.string.prism_api_key_label), + placeholder = stringResource(R.string.prism_api_key_placeholder), + modifier = Modifier.fillMaxWidth(), + enabled = !isTesting + ) + + if (isTesting) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + Spacer(modifier = Modifier.size(8.dp)) + Text(stringResource(R.string.testing_connection)) + } + } + + testResult?.let { result -> + Text( + text = result, + color = if (result.contains("successful")) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.error + } + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Button( + onClick = { + if (url.isBlank() || apiKey.isBlank()) { + onSkip() + return@Button + } + val normalizedUrl = normalizeUrl(url) + testAndSave(normalizedUrl) + }, + enabled = !isTesting, + modifier = Modifier.fillMaxWidth() + ) { + Text( + if (url.isNotBlank() && apiKey.isNotBlank()) { + stringResource(R.string.test_and_save_button) + } else { + stringResource(R.string.intro_continue_button) + } + ) + } + + TextButton( + onClick = onSkip, + enabled = !isTesting, + modifier = Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.intro_skip_button)) + } + } +} diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/MainScreen.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/MainScreen.kt index 2ffca29..bd57066 100644 --- a/app/src/main/java/app/lonecloud/prism/activities/ui/MainScreen.kt +++ b/app/src/main/java/app/lonecloud/prism/activities/ui/MainScreen.kt @@ -1,11 +1,10 @@ package app.lonecloud.prism.activities.ui -import androidx.compose.foundation.clickable -import androidx.compose.foundation.interaction.MutableInteractionSource import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.ExperimentalMaterial3Api @@ -13,7 +12,6 @@ import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -31,7 +29,7 @@ import app.lonecloud.prism.activities.PreviewFactory import org.unifiedpush.android.distributor.ui.compose.AppBar import org.unifiedpush.android.distributor.ui.compose.CardDisableBatteryOptimisation import org.unifiedpush.android.distributor.ui.compose.CardDisabledForMigration -import org.unifiedpush.android.distributor.ui.compose.DistribMigrationUi +import org.unifiedpush.android.distributor.ui.compose.DistribMigrationDialogs import org.unifiedpush.android.distributor.ui.compose.PermissionsUi import org.unifiedpush.android.distributor.ui.compose.RegistrationList import org.unifiedpush.android.distributor.ui.compose.RegistrationListHeading @@ -125,17 +123,14 @@ fun MainScreen( CardDisableBatteryOptimisation(viewModel.batteryOptimisationViewModel) - RegistrationListHeading( - modifier = Modifier.clickable( - indication = null, - interactionSource = remember { MutableInteractionSource() } - ) { - viewModel.addDebugClick() - } - ) + RegistrationListHeading() } - RegistrationList(viewModel.registrationsViewModel) + LazyColumn( + modifier = Modifier.padding(horizontal = 8.dp) + ) { + RegistrationList(viewModel.registrationsViewModel) + } } if (viewModel.mainUiState.showPermissionDialog) { PermissionsUi { @@ -143,22 +138,8 @@ fun MainScreen( migrationViewModel.mayShowFallbackIntro() } } - if (viewModel.mainUiState.showDebugInfo) { - DebugDialog { - viewModel.dismissDebugInfo() - } - } - if (viewModel.mainUiState.showAddAppDialog) { - AddAppDialog( - installedApps = viewModel.mainUiState.installedApps, - onDismiss = { viewModel.hideAddAppDialog() }, - onConfirm = { name, packageName, description -> - viewModel.addApp(name, packageName, description) - } - ) - } if (migrationViewModel.state.canMigrate) { - DistribMigrationUi(migrationViewModel) + DistribMigrationDialogs(migrationViewModel) } } diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/MainUiState.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/MainUiState.kt index eae2318..e02cd6e 100644 --- a/app/src/main/java/app/lonecloud/prism/activities/ui/MainUiState.kt +++ b/app/src/main/java/app/lonecloud/prism/activities/ui/MainUiState.kt @@ -7,12 +7,10 @@ data class InstalledApp( ) data class MainUiState( - val showDebugInfo: Boolean = false, val showPermissionDialog: Boolean = true, val showAppDetails: Boolean = false, val isLoadingEndpoint: Boolean = false, val currentEndpoint: String = "", - val showAddAppDialog: Boolean = false, val installedApps: List = emptyList(), val prismServerConfigured: Boolean = false ) diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/PrismPreferences.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/PrismPreferences.kt index d89cd34..46a25d8 100644 --- a/app/src/main/java/app/lonecloud/prism/activities/ui/PrismPreferences.kt +++ b/app/src/main/java/app/lonecloud/prism/activities/ui/PrismPreferences.kt @@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material3.Icon import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Surface import androidx.compose.material3.Switch @@ -13,12 +15,14 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.RectangleShape +import androidx.compose.ui.graphics.vector.ImageVector import androidx.compose.ui.unit.dp @Composable fun PrismTogglePreference( title: String, description: String? = null, + icon: ImageVector? = null, checked: Boolean, onCheckedChange: (Boolean) -> Unit ) { @@ -29,9 +33,17 @@ fun PrismTogglePreference( ) { Row( modifier = Modifier.padding(16.dp), - horizontalArrangement = Arrangement.SpaceBetween, + horizontalArrangement = Arrangement.spacedBy(16.dp), verticalAlignment = Alignment.CenterVertically ) { + icon?.let { + Icon( + imageVector = it, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + } Column( modifier = Modifier.weight(1f), verticalArrangement = Arrangement.spacedBy(4.dp) diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/PrismServerConfig.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/PrismServerConfig.kt deleted file mode 100644 index 08580c5..0000000 --- a/app/src/main/java/app/lonecloud/prism/activities/ui/PrismServerConfig.kt +++ /dev/null @@ -1,328 +0,0 @@ -package app.lonecloud.prism.activities.ui - -import androidx.compose.foundation.layout.Arrangement -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Row -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding -import androidx.compose.foundation.layout.size -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material3.AlertDialog -import androidx.compose.material3.Button -import androidx.compose.material3.CircularProgressIndicator -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.OutlinedTextField -import androidx.compose.material3.Surface -import androidx.compose.material3.Text -import androidx.compose.material3.TextButton -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.RectangleShape -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.stringResource -import androidx.compose.ui.text.SpanStyle -import androidx.compose.ui.text.buildAnnotatedString -import androidx.compose.ui.text.input.PasswordVisualTransformation -import androidx.compose.ui.text.style.TextDecoration -import androidx.compose.ui.text.withStyle -import androidx.compose.ui.unit.dp -import app.lonecloud.prism.R - -@Composable -fun PrismServerConfigButton( - currentUrl: String, - currentApiKey: String, - onConfigure: (url: String, apiKey: String) -> Unit -) { - var showDialog by remember { mutableStateOf(false) } - - Surface( - onClick = { showDialog = true }, - modifier = Modifier.fillMaxWidth(), - shape = RectangleShape - ) { - Column( - modifier = Modifier.padding(16.dp), - horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(4.dp) - ) { - Text( - text = stringResource(R.string.configure_server), - style = MaterialTheme.typography.bodyLarge - ) - Text( - text = if (currentUrl.isNotBlank()) { - currentUrl - } else { - stringResource(R.string.prism_server_not_configured) - }, - style = MaterialTheme.typography.bodySmall, - color = MaterialTheme.colorScheme.onSurfaceVariant - ) - } - } - - if (showDialog) { - PrismServerConfigDialog( - initialUrl = currentUrl, - initialApiKey = currentApiKey, - onDismiss = { showDialog = false }, - onSave = { url, apiKey -> - onConfigure(url, apiKey) - showDialog = false - } - ) - } -} - -@Composable -fun PrismServerConfigDialog( - initialUrl: String, - initialApiKey: String, - onDismiss: () -> Unit, - onSave: (url: String, apiKey: String) -> Unit -) { - var url by remember { mutableStateOf(initialUrl) } - var apiKey by remember { mutableStateOf(initialApiKey) } - var isTesting by remember { mutableStateOf(false) } - var testResult by remember { mutableStateOf(null) } - var showServerChangeWarning by remember { mutableStateOf(false) } - val context = LocalContext.current - val uriHandler = LocalUriHandler.current - - fun normalizeUrl(rawUrl: String): String = rawUrl.trim().let { - if (!it.startsWith("http://") && !it.startsWith("https://")) { - "https://$it" - } else { - it - } - }.trimEnd('/') - - fun testAndSave(normalizedUrl: String) { - val successMessage = context.getString(R.string.connection_successful) - val failedMessageTemplate = context.getString(R.string.connection_failed) - - isTesting = true - app.lonecloud.prism.PrismServerClient.testConnection( - normalizedUrl, - apiKey, - onSuccess = { - isTesting = false - testResult = successMessage - onSave(normalizedUrl, apiKey) - }, - onError = { error -> - isTesting = false - testResult = failedMessageTemplate.replace("%s", error) - } - ) - } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text(stringResource(R.string.configure_server)) }, - text = { - Column( - verticalArrangement = Arrangement.spacedBy(16.dp), - modifier = Modifier.fillMaxWidth() - ) { - ServerDescriptionLink(uriHandler = uriHandler) - - OutlinedTextField( - value = url, - onValueChange = { - url = it - testResult = null - }, - label = { Text(stringResource(R.string.prism_server_url_label)) }, - placeholder = { Text(stringResource(R.string.prism_server_url_placeholder)) }, - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !isTesting - ) - - OutlinedTextField( - value = apiKey, - onValueChange = { - apiKey = it - testResult = null - }, - label = { Text(stringResource(R.string.prism_api_key_label)) }, - placeholder = { Text(stringResource(R.string.prism_api_key_placeholder)) }, - visualTransformation = PasswordVisualTransformation(), - modifier = Modifier.fillMaxWidth(), - singleLine = true, - enabled = !isTesting - ) - - if (isTesting) { - Row( - horizontalArrangement = Arrangement.Center, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - CircularProgressIndicator(modifier = Modifier.size(20.dp)) - Spacer(modifier = Modifier.size(8.dp)) - Text(stringResource(R.string.testing_connection)) - } - } - - testResult?.let { result -> - Text( - text = result, - color = if (result.contains("successful")) { - MaterialTheme.colorScheme.primary - } else { - MaterialTheme.colorScheme.error - } - ) - } - } - }, - confirmButton = { - Button( - onClick = { - if (url.isBlank() || apiKey.isBlank()) { - onDismiss() - return@Button - } - - val normalizedUrl = normalizeUrl(url) - val isServerChanging = initialUrl.isNotBlank() && normalizedUrl != initialUrl - - if (isServerChanging) { - val db = app.lonecloud.prism.DatabaseFactory.getDb(context) - val manualAppsCount = db.listApps() - .count { it.description?.startsWith("target:") == true } - if (manualAppsCount > 0) { - showServerChangeWarning = true - return@Button - } - } - - testAndSave(normalizedUrl) - }, - enabled = !isTesting && url.isNotBlank() && apiKey.isNotBlank() - ) { - Text(stringResource(R.string.test_and_save_button)) - } - }, - dismissButton = { - TextButton(onClick = onDismiss, enabled = !isTesting) { - Text(stringResource(R.string.cancel_button)) - } - } - ) - - if (showServerChangeWarning) { - ServerChangeWarningDialog( - newUrl = normalizeUrl(url), - newApiKey = apiKey, - initialUrl = initialUrl, - onConfirm = { normalizedUrl -> - showServerChangeWarning = false - testAndSave(normalizedUrl) - }, - onDismiss = { showServerChangeWarning = false } - ) - } -} - -@Composable -private fun ServerDescriptionLink(uriHandler: androidx.compose.ui.platform.UriHandler) { - val description = stringResource(R.string.prism_server_description) - val repoUrl = stringResource(R.string.prism_server_repo_link) - val displayUrl = repoUrl.removePrefix("https://") - val annotatedString = buildAnnotatedString { - append(description) - append("\n\n") - - val linkStart = length - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - textDecoration = TextDecoration.Underline - ) - ) { - append(displayUrl) - } - addStringAnnotation( - tag = "URL", - annotation = repoUrl, - start = linkStart, - end = length - ) - } - - ClickableText( - text = annotatedString, - style = MaterialTheme.typography.bodySmall.copy( - color = MaterialTheme.colorScheme.onSurfaceVariant - ), - onClick = { offset -> - annotatedString.getStringAnnotations( - tag = "URL", - start = offset, - end = offset - ).firstOrNull()?.let { annotation -> - uriHandler.openUri(annotation.item) - } - } - ) -} - -@Composable -private fun ServerChangeWarningDialog( - newUrl: String, - newApiKey: String, - initialUrl: String, - onConfirm: (String) -> Unit, - onDismiss: () -> Unit -) { - val context = LocalContext.current - val db = app.lonecloud.prism.DatabaseFactory.getDb(context) - val manualAppsCount = db.listApps() - .count { it.description?.startsWith("target:") == true } - - AlertDialog( - onDismissRequest = onDismiss, - title = { Text("Change Prism Server?") }, - text = { - Text( - "You have $manualAppsCount manual app${if (manualAppsCount == 1) "" else "s"}" + - " registered with the current server.\n\n" + - "Changing to $newUrl will delete registrations from the old server" + - " and re-register with the new one." - ) - }, - confirmButton = { - Button( - onClick = { - val oldKey = app.lonecloud.prism.PrismPreferences(context).prismApiKey - if (initialUrl.isNotBlank() && !oldKey.isNullOrBlank()) { - app.lonecloud.prism.PrismServerClient.deleteAllApps( - context, - serverUrl = initialUrl, - apiKey = oldKey - ) - } - onConfirm(newUrl) - } - ) { - Text("Continue") - } - }, - dismissButton = { - TextButton(onClick = onDismiss) { - Text("Cancel") - } - } - ) -} diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/ServerConfigScreen.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/ServerConfigScreen.kt new file mode 100644 index 0000000..4ad4d5c --- /dev/null +++ b/app/src/main/java/app/lonecloud/prism/activities/ui/ServerConfigScreen.kt @@ -0,0 +1,321 @@ +package app.lonecloud.prism.activities.ui + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Button +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.LocalUriHandler +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.unit.dp +import app.lonecloud.prism.R +import app.lonecloud.prism.activities.ui.components.PasswordTextField +import app.lonecloud.prism.utils.normalizeUrl +import app.lonecloud.prism.utils.testServerConnection + +@Composable +fun ServerConfigScreen( + initialUrl: String, + initialApiKey: String, + onNavigateBack: () -> Unit, + onSave: (url: String, apiKey: String) -> Unit +) { + var url by remember { mutableStateOf(initialUrl) } + var apiKey by remember { mutableStateOf("") } + var isTesting by remember { mutableStateOf(false) } + var testResult by remember { mutableStateOf(null) } + var showServerChangeWarning by remember { mutableStateOf(false) } + var showClearConfirmation by remember { mutableStateOf(false) } + val context = LocalContext.current + val uriHandler = LocalUriHandler.current + val successMessage = stringResource(R.string.connection_successful) + val failedMessageTemplate = stringResource(R.string.connection_failed) + + fun testAndSave(normalizedUrl: String) { + isTesting = true + testServerConnection( + normalizedUrl, + apiKey, + onSuccess = { + isTesting = false + testResult = successMessage + onSave(normalizedUrl, apiKey) + onNavigateBack() + }, + onError = { error -> + isTesting = false + testResult = failedMessageTemplate.replace("%s", error) + } + ) + } + + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp) + .verticalScroll(rememberScrollState()), + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = stringResource(R.string.prism_server_info), + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + + PrismInfoWithLink(uriHandler = uriHandler) + + OutlinedTextField( + value = url, + onValueChange = { + url = it + testResult = null + }, + label = { Text(stringResource(R.string.prism_server_url_label)) }, + placeholder = { Text(stringResource(R.string.prism_server_url_placeholder)) }, + modifier = Modifier.fillMaxWidth(), + singleLine = true, + enabled = !isTesting + ) + + PasswordTextField( + value = apiKey, + onValueChange = { + apiKey = it + testResult = null + }, + label = stringResource(R.string.prism_api_key_label), + placeholder = stringResource(R.string.prism_api_key_placeholder), + modifier = Modifier.fillMaxWidth(), + enabled = !isTesting + ) + + if (isTesting) { + Row( + horizontalArrangement = Arrangement.Center, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.fillMaxWidth() + ) { + CircularProgressIndicator(modifier = Modifier.size(20.dp)) + Spacer(modifier = Modifier.size(8.dp)) + Text(stringResource(R.string.testing_connection)) + } + } + + testResult?.let { result -> + Text( + text = result, + color = if (result.contains("successful")) { + MaterialTheme.colorScheme.primary + } else { + MaterialTheme.colorScheme.error + } + ) + } + + Spacer(modifier = Modifier.weight(1f)) + + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + if (initialUrl.isNotBlank()) { + OutlinedButton( + onClick = { showClearConfirmation = true }, + modifier = Modifier.weight(1f) + ) { + Text(stringResource(R.string.clear_server_button)) + } + } + + Button( + onClick = { + if (url.isBlank() || (apiKey.isBlank() && initialApiKey.isBlank())) { + onNavigateBack() + return@Button + } + + val normalizedUrl = normalizeUrl(url) + val finalApiKey = apiKey.ifBlank { initialApiKey } + val isServerChanging = initialUrl.isNotBlank() && normalizedUrl != initialUrl + + if (isServerChanging) { + val db = app.lonecloud.prism.DatabaseFactory.getDb(context) + val manualAppsCount = db.listApps() + .count { it.description?.startsWith("target:") == true } + if (manualAppsCount > 0) { + showServerChangeWarning = true + return@Button + } + } + + testAndSave(normalizedUrl) + }, + enabled = !isTesting && url.isNotBlank() && apiKey.isNotBlank(), + modifier = if (initialUrl.isNotBlank()) Modifier.weight(1f) else Modifier.fillMaxWidth() + ) { + Text(stringResource(R.string.test_and_save_button)) + } + } + } + + if (showServerChangeWarning) { + ServerChangeWarningDialog( + newUrl = normalizeUrl(url), + newApiKey = apiKey, + initialUrl = initialUrl, + onConfirm = { normalizedUrl -> + showServerChangeWarning = false + testAndSave(normalizedUrl) + }, + onDismiss = { showServerChangeWarning = false } + ) + } + + if (showClearConfirmation) { + ClearServerConfirmationDialog( + initialUrl = initialUrl, + onConfirm = { + showClearConfirmation = false + onSave("", "") + onNavigateBack() + }, + onDismiss = { showClearConfirmation = false } + ) + } +} + +@Composable +internal fun PrismInfoWithLink(uriHandler: androidx.compose.ui.platform.UriHandler) { + val learnMore = stringResource(R.string.prism_server_learn_more) + + Text( + text = learnMore, + style = MaterialTheme.typography.bodyMedium, + color = MaterialTheme.colorScheme.primary, + modifier = Modifier.clickable { + uriHandler.openUri("https://github.com/lone-cloud/prism") + } + ) +} + +@Composable +private fun ServerChangeWarningDialog( + newUrl: String, + newApiKey: String, + initialUrl: String, + onConfirm: (String) -> Unit, + onDismiss: () -> Unit +) { + val context = LocalContext.current + val db = app.lonecloud.prism.DatabaseFactory.getDb(context) + val manualAppsCount = db.listApps() + .count { it.description?.startsWith("target:") == true } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text("Change Prism Server?") }, + text = { + Text( + "You have $manualAppsCount manual app${if (manualAppsCount == 1) "" else "s"}" + + " registered with the current server.\n\n" + + "Changing to $newUrl will delete registrations from the old server" + + " and re-register with the new one." + ) + }, + confirmButton = { + Button( + onClick = { + val oldKey = app.lonecloud.prism.PrismPreferences(context).prismApiKey + if (initialUrl.isNotBlank() && !oldKey.isNullOrBlank()) { + app.lonecloud.prism.PrismServerClient.deleteAllApps( + context, + serverUrl = initialUrl, + apiKey = oldKey + ) + } + onConfirm(newUrl) + } + ) { + Text("Continue") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) +} + +@Composable +private fun ClearServerConfirmationDialog( + initialUrl: String, + onConfirm: () -> Unit, + onDismiss: () -> Unit +) { + val context = LocalContext.current + val db = app.lonecloud.prism.DatabaseFactory.getDb(context) + val manualAppsCount = db.listApps() + .count { it.description?.startsWith("target:") == true } + + AlertDialog( + onDismissRequest = onDismiss, + title = { Text(stringResource(R.string.clear_server_confirm_title)) }, + text = { + Text( + if (manualAppsCount > 0) { + stringResource( + R.string.clear_server_confirm_message_with_apps, + manualAppsCount + ) + } else { + stringResource(R.string.clear_server_confirm_message_no_apps) + } + ) + }, + confirmButton = { + TextButton( + onClick = { + val oldKey = app.lonecloud.prism.PrismPreferences(context).prismApiKey + if (initialUrl.isNotBlank() && !oldKey.isNullOrBlank()) { + app.lonecloud.prism.PrismServerClient.deleteAllApps( + context, + serverUrl = initialUrl, + apiKey = oldKey + ) + } + onConfirm() + } + ) { + Text("Remove") + } + }, + dismissButton = { + TextButton(onClick = onDismiss) { + Text("Cancel") + } + } + ) +} diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/SettingsScreen.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/SettingsScreen.kt index 73c9de3..59afb18 100644 --- a/app/src/main/java/app/lonecloud/prism/activities/ui/SettingsScreen.kt +++ b/app/src/main/java/app/lonecloud/prism/activities/ui/SettingsScreen.kt @@ -2,9 +2,23 @@ package app.lonecloud.prism.activities.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.size +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Cloud +import androidx.compose.material.icons.filled.Notifications +import androidx.compose.material.icons.filled.Palette +import androidx.compose.material3.Icon +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Surface +import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.tooling.preview.Preview @@ -17,14 +31,15 @@ import app.lonecloud.prism.R import app.lonecloud.prism.activities.PreviewFactory import app.lonecloud.prism.activities.SettingsViewModel import app.lonecloud.prism.activities.ThemeViewModel -import org.unifiedpush.android.distributor.ui.compose.DistribMigrationUi +import org.unifiedpush.android.distributor.ui.compose.DistribMigrationDialogs import org.unifiedpush.android.distributor.ui.vm.DistribMigrationViewModel @Composable fun SettingsScreen( viewModel: SettingsViewModel, themeViewModel: ThemeViewModel, - migrationViewModel: DistribMigrationViewModel + migrationViewModel: DistribMigrationViewModel, + onNavigateToServerConfig: () -> Unit = {} ) { val lifecycleOwner = LocalLifecycleOwner.current LaunchedEffect(Unit) { @@ -34,31 +49,64 @@ fun SettingsScreen( } Column( horizontalAlignment = Alignment.Start, - verticalArrangement = Arrangement.spacedBy(20.dp) + verticalArrangement = Arrangement.spacedBy(4.dp) ) { - PrismServerConfigButton( - currentUrl = viewModel.state.prismServerUrl, - currentApiKey = viewModel.state.prismApiKey, - onConfigure = { url, apiKey -> - viewModel.updatePrismServerUrl(url) - viewModel.updatePrismApiKey(apiKey) + Surface( + onClick = onNavigateToServerConfig, + modifier = Modifier.fillMaxWidth(), + shape = RectangleShape + ) { + Row( + modifier = Modifier.padding(16.dp), + horizontalArrangement = Arrangement.spacedBy(16.dp), + verticalAlignment = Alignment.CenterVertically + ) { + Icon( + imageVector = Icons.Filled.Cloud, + contentDescription = null, + modifier = Modifier.size(24.dp), + tint = MaterialTheme.colorScheme.onSurfaceVariant + ) + Column( + horizontalAlignment = Alignment.Start, + verticalArrangement = Arrangement.spacedBy(4.dp), + modifier = Modifier.weight(1f) + ) { + Text( + text = stringResource(R.string.configure_server), + style = MaterialTheme.typography.bodyLarge + ) + Text( + text = if (viewModel.state.prismServerUrl.isNotBlank()) { + viewModel.state.prismServerUrl + } else { + stringResource(R.string.prism_server_not_configured) + }, + style = MaterialTheme.typography.bodySmall, + color = MaterialTheme.colorScheme.onSurfaceVariant + ) + } } - ) + } PrismTogglePreference( title = stringResource(R.string.app_dropdown_show_toasts), + description = stringResource(R.string.show_toasts_description), + icon = Icons.Filled.Notifications, checked = viewModel.state.showToasts, onCheckedChange = { viewModel.toggleShowToasts() } ) PrismTogglePreference( title = stringResource(R.string.dynamic_colors_title), + description = stringResource(R.string.dynamic_colors_description), + icon = Icons.Filled.Palette, checked = themeViewModel.dynamicColors, onCheckedChange = { themeViewModel.toggleDynamicColors() } ) } if (migrationViewModel.state.canMigrate) { - DistribMigrationUi(migrationViewModel) + DistribMigrationDialogs(migrationViewModel) } } diff --git a/app/src/main/java/app/lonecloud/prism/activities/ui/components/PasswordTextField.kt b/app/src/main/java/app/lonecloud/prism/activities/ui/components/PasswordTextField.kt new file mode 100644 index 0000000..9f3b8d1 --- /dev/null +++ b/app/src/main/java/app/lonecloud/prism/activities/ui/components/PasswordTextField.kt @@ -0,0 +1,49 @@ +package app.lonecloud.prism.activities.ui.components + +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Visibility +import androidx.compose.material.icons.filled.VisibilityOff +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.OutlinedTextField +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.input.PasswordVisualTransformation +import androidx.compose.ui.text.input.VisualTransformation + +@Composable +fun PasswordTextField( + value: String, + onValueChange: (String) -> Unit, + label: String, + placeholder: String, + modifier: Modifier = Modifier, + enabled: Boolean = true, + singleLine: Boolean = true +) { + var passwordVisible by remember { mutableStateOf(false) } + + OutlinedTextField( + value = value, + onValueChange = onValueChange, + label = { Text(label) }, + placeholder = { Text(placeholder) }, + visualTransformation = if (passwordVisible) VisualTransformation.None else PasswordVisualTransformation(), + trailingIcon = { + IconButton(onClick = { passwordVisible = !passwordVisible }) { + Icon( + imageVector = if (passwordVisible) Icons.Filled.Visibility else Icons.Filled.VisibilityOff, + contentDescription = if (passwordVisible) "Hide password" else "Show password" + ) + } + }, + modifier = modifier, + singleLine = singleLine, + enabled = enabled + ) +} diff --git a/app/src/main/java/app/lonecloud/prism/utils/DebugInformation.kt b/app/src/main/java/app/lonecloud/prism/utils/DebugInformation.kt deleted file mode 100644 index 4f372d0..0000000 --- a/app/src/main/java/app/lonecloud/prism/utils/DebugInformation.kt +++ /dev/null @@ -1,15 +0,0 @@ -package app.lonecloud.prism.utils - -import app.lonecloud.prism.api.ServerConnection -import app.lonecloud.prism.services.FgService -import app.lonecloud.prism.services.SourceManager -import java.text.SimpleDateFormat - -fun getDebugInfo(): String { - val date = ServerConnection.lastEventDate?.let { - SimpleDateFormat.getDateTimeInstance().format(it.time) - } ?: "None" - return "ServiceStarted: ${FgService.isServiceStarted()}\n" + - "Last Event: $date\n" + - SourceManager.getDebugInfo() -} diff --git a/app/src/main/java/app/lonecloud/prism/utils/ServerUtils.kt b/app/src/main/java/app/lonecloud/prism/utils/ServerUtils.kt new file mode 100644 index 0000000..359af5c --- /dev/null +++ b/app/src/main/java/app/lonecloud/prism/utils/ServerUtils.kt @@ -0,0 +1,23 @@ +package app.lonecloud.prism.utils + +fun normalizeUrl(rawUrl: String): String = rawUrl.trim().let { + if (!it.startsWith("http://") && !it.startsWith("https://")) { + "https://$it" + } else { + it + } +}.trimEnd('/') + +fun testServerConnection( + url: String, + apiKey: String, + onSuccess: () -> Unit, + onError: (String) -> Unit +) { + app.lonecloud.prism.PrismServerClient.testConnection( + url, + apiKey, + onSuccess, + onError + ) +} diff --git a/app/src/main/res/drawable/app_logo.webp b/app/src/main/res/drawable/app_logo.webp new file mode 100644 index 0000000..b6f5a9d Binary files /dev/null and b/app/src/main/res/drawable/app_logo.webp differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 81a3752..b4d31f0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -47,8 +47,8 @@ Add manual app Debug Information Configure Prism Server - Self-host a Prism server to unlock manual app registrations. Otherwise, this distributor works normally with UnifiedPush apps. - https://github.com/lone-cloud/prism + Prism server enables manual app registrations and multi-device sync. Self-host your own or use a public instance. + Learn more about Prism Prism server configured %1$s (v%2$s) Prism server not configured @@ -60,10 +60,19 @@ Connection successful Connection failed Test and Save - Clear + Remove Remove Prism Server? This will remove your Prism server configuration. You have %d manual app registration(s). Clearing the server will delete them from the server and remove the configuration. Notify when apps register + Show a notification when apps register or unregister Dynamic Colors + Use colors from your wallpaper + + + Welcome to Prism + Prism is a UnifiedPush distributor that supports manual app registrations through an optional Prism server. + Configure a Prism server now or skip to set it up later in Settings. + Continue + Skip for now diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 56a1c6f..a3a1446 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -4,7 +4,7 @@ androidx-activityCompose = "1.12.1" androidx-lifecycle = "2.10.0" androidx-work = "2.11.0" appcompat = "1.7.1" -unifiedpush_distributor = "0.7.1" +unifiedpush_distributor = "0.7.2" unifiedpush_distributor_base = "0.7.0" accompanist_permissions = "0.37.3" tink = "1.15.0"