From 8b41645f8210c574be2a09154eb1c70a2e386845 Mon Sep 17 00:00:00 2001 From: Egor Date: Sat, 14 Feb 2026 19:42:28 -0800 Subject: [PATCH] new intro screen for prism server, distributor dep update to latest, better adopt the material patterns instead of crappy modals --- app/build.gradle.kts | 1 + .../app/lonecloud/prism/PrismPreferences.kt | 9 + .../app/lonecloud/prism/PrismServerClient.kt | 29 +- .../prism/activities/MainActivity.kt | 22 ++ .../prism/activities/MainViewModel.kt | 43 +-- .../prism/activities/SettingsViewModel.kt | 49 ++- .../prism/activities/ui/AddAppDialog.kt | 127 ------- .../prism/activities/ui/AddAppScreen.kt | 113 ++++++ .../prism/activities/ui/AppPickerDialog.kt | 106 ------ .../prism/activities/ui/AppPickerScreen.kt | 102 ++++++ .../prism/activities/ui/AppScreen.kt | 161 +++++++-- .../prism/activities/ui/DebugDialog.kt | 31 -- .../prism/activities/ui/IntroScreen.kt | 171 +++++++++ .../prism/activities/ui/MainScreen.kt | 37 +- .../prism/activities/ui/MainUiState.kt | 2 - .../prism/activities/ui/PrismPreferences.kt | 14 +- .../prism/activities/ui/PrismServerConfig.kt | 328 ------------------ .../prism/activities/ui/ServerConfigScreen.kt | 321 +++++++++++++++++ .../prism/activities/ui/SettingsScreen.kt | 70 +++- .../ui/components/PasswordTextField.kt | 49 +++ .../lonecloud/prism/utils/DebugInformation.kt | 15 - .../app/lonecloud/prism/utils/ServerUtils.kt | 23 ++ app/src/main/res/drawable/app_logo.webp | Bin 0 -> 44346 bytes app/src/main/res/values/strings.xml | 15 +- gradle/libs.versions.toml | 2 +- 25 files changed, 1120 insertions(+), 720 deletions(-) delete mode 100644 app/src/main/java/app/lonecloud/prism/activities/ui/AddAppDialog.kt create mode 100644 app/src/main/java/app/lonecloud/prism/activities/ui/AddAppScreen.kt delete mode 100644 app/src/main/java/app/lonecloud/prism/activities/ui/AppPickerDialog.kt create mode 100644 app/src/main/java/app/lonecloud/prism/activities/ui/AppPickerScreen.kt delete mode 100644 app/src/main/java/app/lonecloud/prism/activities/ui/DebugDialog.kt create mode 100644 app/src/main/java/app/lonecloud/prism/activities/ui/IntroScreen.kt delete mode 100644 app/src/main/java/app/lonecloud/prism/activities/ui/PrismServerConfig.kt create mode 100644 app/src/main/java/app/lonecloud/prism/activities/ui/ServerConfigScreen.kt create mode 100644 app/src/main/java/app/lonecloud/prism/activities/ui/components/PasswordTextField.kt delete mode 100644 app/src/main/java/app/lonecloud/prism/utils/DebugInformation.kt create mode 100644 app/src/main/java/app/lonecloud/prism/utils/ServerUtils.kt create mode 100644 app/src/main/res/drawable/app_logo.webp 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 0000000000000000000000000000000000000000..b6f5a9defa94b802bb3db19434a618e5c6c163e1 GIT binary patch literal 44346 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 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"