new intro screen for prism server, distributor dep update to latest, better adopt the material patterns instead of crappy modals

This commit is contained in:
Egor 2026-02-14 19:42:28 -08:00
parent 5fcc504121
commit 8b41645f82
25 changed files with 1120 additions and 720 deletions

View file

@ -108,6 +108,7 @@ dependencies {
implementation(libs.okhttp) implementation(libs.okhttp)
implementation(libs.androidx.material3.android) implementation(libs.androidx.material3.android)
implementation(libs.androidx.material.icons.core) implementation(libs.androidx.material.icons.core)
implementation(libs.androidx.material.icons.extended)
implementation(libs.kotlinx.serialization.json) implementation(libs.kotlinx.serialization.json)
implementation(libs.androidx.ui.tooling.preview.android) implementation(libs.androidx.ui.tooling.preview.android)
implementation(libs.androidx.navigation.compose) implementation(libs.androidx.navigation.compose)

View file

@ -97,6 +97,14 @@ class PrismPreferences(context: Context) :
putOrRemove(PREF_PRISM_API_KEY, value) 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() { override fun wipe() {
uaid = null uaid = null
} }
@ -113,5 +121,6 @@ class PrismPreferences(context: Context) :
private const val PREF_SHOW_TOASTS = "show_toasts" private const val PREF_SHOW_TOASTS = "show_toasts"
private const val PREF_PRISM_SERVER_URL = "prism_server_url" private const val PREF_PRISM_SERVER_URL = "prism_server_url"
private const val PREF_PRISM_API_KEY = "prism_api_key" private const val PREF_PRISM_API_KEY = "prism_api_key"
private const val PREF_INTRO_COMPLETED = "intro_completed"
} }
} }

View file

@ -9,6 +9,7 @@ import java.util.concurrent.TimeUnit
import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import okhttp3.MediaType.Companion.toMediaType import okhttp3.MediaType.Companion.toMediaType
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
@ -54,7 +55,7 @@ object PrismServerClient {
} }
val request = Request.Builder() val request = Request.Builder()
.url("$serverUrl/webpush/app") .url("$serverUrl/api/v1/webpush/app")
.addHeader("Authorization", getAuthHeader(apiKey)) .addHeader("Authorization", getAuthHeader(apiKey))
.addHeader("Content-Type", "application/json") .addHeader("Content-Type", "application/json")
.post(json.toString().toRequestBody("application/json".toMediaType())) .post(json.toString().toRequestBody("application/json".toMediaType()))
@ -65,17 +66,17 @@ object PrismServerClient {
client.newCall(request).execute().use { response -> client.newCall(request).execute().use { response ->
if (response.isSuccessful) { if (response.isSuccessful) {
Log.d(TAG, "Successfully registered app: $appName") Log.d(TAG, "Successfully registered app: $appName")
onSuccess() withContext(Dispatchers.Main) { onSuccess() }
} else { } else {
val error = "Failed to register app: ${response.code} ${response.message}" val error = "Failed to register app: ${response.code} ${response.message}"
Log.e(TAG, error) Log.e(TAG, error)
onError(error) withContext(Dispatchers.Main) { onError(error) }
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
val error = "Error registering app: ${e.message}" val error = "Error registering app: ${e.message}"
Log.e(TAG, error, e) Log.e(TAG, error, e)
onError(error) withContext(Dispatchers.Main) { onError(error) }
} }
} }
} }
@ -135,7 +136,7 @@ object PrismServerClient {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val request = Request.Builder() val request = Request.Builder()
.url("$url/webpush/app/$appName") .url("$url/api/v1/webpush/app/$appName")
.addHeader("Authorization", getAuthHeader(key)) .addHeader("Authorization", getAuthHeader(key))
.delete() .delete()
.build() .build()
@ -145,17 +146,17 @@ object PrismServerClient {
client.newCall(request).execute().use { response -> client.newCall(request).execute().use { response ->
if (response.isSuccessful) { if (response.isSuccessful) {
Log.d(TAG, "Successfully deleted app: $appName") Log.d(TAG, "Successfully deleted app: $appName")
onSuccess() withContext(Dispatchers.Main) { onSuccess() }
} else { } else {
val error = "Failed to delete app: ${response.code} ${response.message}" val error = "Failed to delete app: ${response.code} ${response.message}"
Log.e(TAG, error) Log.e(TAG, error)
onError(error) withContext(Dispatchers.Main) { onError(error) }
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
val error = "Error deleting app: ${e.message}" val error = "Error deleting app: ${e.message}"
Log.e(TAG, error, e) Log.e(TAG, error, e)
onError(error) withContext(Dispatchers.Main) { onError(error) }
} }
} }
} }
@ -201,7 +202,7 @@ object PrismServerClient {
) { ) {
CoroutineScope(Dispatchers.IO).launch { CoroutineScope(Dispatchers.IO).launch {
try { try {
val healthUrl = "$serverUrl/api/health" val healthUrl = "$serverUrl/api/v1/health"
val request = Request.Builder() val request = Request.Builder()
.url(healthUrl) .url(healthUrl)
.addHeader("Authorization", getAuthHeader(apiKey)) .addHeader("Authorization", getAuthHeader(apiKey))
@ -210,13 +211,17 @@ object PrismServerClient {
client.newCall(request).execute().use { response -> client.newCall(request).execute().use { response ->
if (response.isSuccessful) { if (response.isSuccessful) {
onSuccess() withContext(Dispatchers.Main) { onSuccess() }
} else { } else {
onError("Connection failed: ${response.code} ${response.message}") withContext(Dispatchers.Main) {
onError("Connection failed: ${response.code} ${response.message}")
}
} }
} }
} catch (e: IOException) { } catch (e: IOException) {
onError("Connection error: ${e.message}") withContext(Dispatchers.Main) {
onError("Connection error: ${e.message}")
}
} }
} }
} }

View file

@ -9,12 +9,20 @@
package app.lonecloud.prism.activities package app.lonecloud.prism.activities
import android.os.Bundle import android.os.Bundle
import android.util.Log
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.activity.enableEdgeToEdge import androidx.activity.enableEdgeToEdge
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.lifecycle.viewmodel.compose.viewModel
import app.lonecloud.prism.activities.ui.App import app.lonecloud.prism.activities.ui.App
import app.lonecloud.prism.activities.ui.theme.AppTheme 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 import org.unifiedpush.android.distributor.ipc.InternalMessenger
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -24,6 +32,11 @@ class MainActivity : ComponentActivity() {
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
messenger = InternalMessenger(this) messenger = InternalMessenger(this)
jobs.removeAll {
Log.d(TAG, "Cancelling exitProcess job")
it.cancel()
true
}
enableEdgeToEdge() enableEdgeToEdge()
@ -39,6 +52,15 @@ class MainActivity : ComponentActivity() {
} }
override fun onDestroy() { override fun onDestroy() {
Log.d(TAG, "Destroy")
super.onDestroy() super.onDestroy()
jobs += CoroutineScope(Dispatchers.Main + Job()).launch {
delay(10_000)
exitProcess(0)
}
}
companion object {
val jobs = emptyList<Job>().toMutableList()
} }
} }

View file

@ -6,8 +6,6 @@ import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.util.Log import android.util.Log
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableIntStateOf
import androidx.compose.runtime.mutableLongStateOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
@ -55,14 +53,13 @@ class MainViewModel(
var mainUiState by mutableStateOf(mainUiState) var mainUiState by mutableStateOf(mainUiState)
var selectedApp by mutableStateOf<InstalledApp?>(null)
private set
fun updatePrismServerConfigured(configured: Boolean) { fun updatePrismServerConfigured(configured: Boolean) {
mainUiState = mainUiState.copy(prismServerConfigured = configured) mainUiState = mainUiState.copy(prismServerConfigured = configured)
} }
private var lastDebugClickTime by mutableLongStateOf(0L)
private var debugClickCount by mutableIntStateOf(0)
init { init {
loadInstalledApps() loadInstalledApps()
} }
@ -123,21 +120,20 @@ class MainViewModel(
mainUiState = mainUiState.copy(showAppDetails = false) mainUiState = mainUiState.copy(showAppDetails = false)
} }
fun addDebugClick() { fun selectApp(app: InstalledApp) {
val currentTime = System.currentTimeMillis() selectedApp = app
if (currentTime - lastDebugClickTime < 500) {
debugClickCount++
if (debugClickCount == 5) {
mainUiState = mainUiState.copy(showDebugInfo = true)
}
} else {
debugClickCount = 1
}
lastDebugClickTime = currentTime
} }
fun dismissDebugInfo() { fun clearSelectedApp() {
mainUiState = mainUiState.copy(showDebugInfo = false) selectedApp = null
}
fun addManualApp(
name: String,
packageName: String,
description: String?
) {
addApp(name, packageName, description)
} }
private fun hasUnifiedPushSupport(pm: PackageManager, packageName: String): Boolean { 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( fun addApp(
name: String, name: String,
targetPackageName: String, targetPackageName: String,
@ -220,7 +208,6 @@ class MainViewModel(
) )
refreshRegistrations() refreshRegistrations()
hideAddAppDialog()
var endpoint: String? var endpoint: String?
var attempts = 0 var attempts = 0

View file

@ -38,7 +38,7 @@ class SettingsViewModel(
} }
} }
fun updatePrismServerUrl(url: String) { fun updatePrismServerUrl(url: String, sendAction: Boolean = true) {
viewModelScope.launch { viewModelScope.launch {
val trimmedUrl = url.trim() val trimmedUrl = url.trim()
state = state.copy(prismServerUrl = trimmedUrl) state = state.copy(prismServerUrl = trimmedUrl)
@ -55,12 +55,14 @@ class SettingsViewModel(
PrismServerClient.registerAllApps(it) PrismServerClient.registerAllApps(it)
} }
sendUiAction(it, "UpdatePrismServerConfigured") if (sendAction) {
sendUiAction(it, "UpdatePrismServerConfigured")
}
} }
} }
} }
fun updatePrismApiKey(apiKey: String) { fun updatePrismApiKey(apiKey: String, sendAction: Boolean = true) {
viewModelScope.launch { viewModelScope.launch {
val trimmedKey = apiKey.trim() val trimmedKey = apiKey.trim()
state = state.copy(prismApiKey = trimmedKey) state = state.copy(prismApiKey = trimmedKey)
@ -77,7 +79,46 @@ class SettingsViewModel(
PrismServerClient.registerAllApps(it) 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")
} }
} }
} }

View file

@ -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<InstalledApp>,
onDismiss: () -> Unit,
onConfirm: (name: String, packageName: String, description: String?) -> Unit
) {
var name by remember { mutableStateOf("") }
var selectedApp by remember { mutableStateOf<InstalledApp?>(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
}
)
}
}

View file

@ -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))
}
}
}

View file

@ -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<InstalledApp>,
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))
}
}
)
}

View file

@ -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<InstalledApp>,
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
)
}
}
}
}
}
}

View file

@ -21,6 +21,7 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.Scaffold import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -32,6 +33,7 @@ import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.currentBackStackEntryAsState import androidx.navigation.compose.currentBackStackEntryAsState
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import app.lonecloud.prism.PrismPreferences
import app.lonecloud.prism.R import app.lonecloud.prism.R
import app.lonecloud.prism.activities.MainViewModel import app.lonecloud.prism.activities.MainViewModel
import app.lonecloud.prism.activities.PreviewFactory 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 import org.unifiedpush.android.distributor.ui.vm.DistribMigrationViewModel
enum class AppScreen(@param:StringRes val title: Int) { enum class AppScreen(@param:StringRes val title: Int) {
Intro(R.string.app_name),
Main(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) @OptIn(ExperimentalMaterial3Api::class)
@ -86,6 +92,8 @@ fun App(
) { ) {
val context = LocalContext.current val context = LocalContext.current
val uiActionsFlow = subscribeUiActions(context) 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 backStackEntry by navController.currentBackStackEntryAsState()
val currentScreen = AppScreen.valueOf( val currentScreen = AppScreen.valueOf(
@ -96,26 +104,34 @@ fun App(
Scaffold( Scaffold(
topBar = { topBar = {
when (currentScreen) { if (currentScreen == AppScreen.Intro) {
AppScreen.Main -> { null
MainAppBarOrSelection( } else {
mainViewModel, when (currentScreen) {
onGoToSettings = { AppScreen.Main -> {
navController.navigate(AppScreen.Settings.name) MainAppBarOrSelection(
} mainViewModel,
) onGoToSettings = {
} navController.navigate(AppScreen.Settings.name)
else -> null }
} ?: DefaultTopBar( )
currentScreen, }
canNavigateBack = navController.previousBackStackEntry != null, else -> null
navigateUp = { navController.navigateUp() } } ?: DefaultTopBar(
) currentScreen,
canNavigateBack = navController.previousBackStackEntry != null,
navigateUp = { navController.navigateUp() }
)
}
}, },
floatingActionButton = { floatingActionButton = {
if (currentScreen == AppScreen.Main && mainViewModel.mainUiState.prismServerConfigured) { val prefs = PrismPreferences(context)
if (currentScreen == AppScreen.Main && !prefs.prismServerUrl.isNullOrBlank() && !prefs.prismApiKey.isNullOrBlank()) {
FloatingActionButton( 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)) Icon(Icons.Default.Add, contentDescription = stringResource(R.string.add_manual_app_content_description))
} }
@ -125,24 +141,42 @@ fun App(
) { innerPadding -> ) { innerPadding ->
NavHost( NavHost(
navController = navController, navController = navController,
startDestination = AppScreen.Main.name, startDestination = startDestination,
modifier = Modifier modifier = Modifier
.fillMaxSize() .fillMaxSize()
.padding(innerPadding) .padding(innerPadding)
) { ) {
composable(route = AppScreen.Intro.name) {
val settingsViewModel = viewModel<SettingsViewModel>(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( composable(
route = AppScreen.Main.name, route = AppScreen.Main.name,
exitTransition = { exitTransition = {
when (targetState.destination.route) { when (targetState.destination.route) {
AppScreen.Settings.name -> slideOutFrom( AppScreen.Settings.name -> slideOutFrom(Dir.Right)
Dir.Right AppScreen.AddApp.name -> slideOutFrom(Dir.Right)
)
else -> fadeOut() else -> fadeOut()
} }
}, },
popEnterTransition = { popEnterTransition = {
when (initialState.destination.route) { when (initialState.destination.route) {
AppScreen.Settings.name -> slideInTo(Dir.Right) AppScreen.Settings.name -> slideInTo(Dir.Right)
AppScreen.AddApp.name -> slideInTo(Dir.Right)
else -> fadeIn() else -> fadeIn()
} }
} }
@ -156,10 +190,91 @@ fun App(
composable( composable(
route = AppScreen.Settings.name, route = AppScreen.Settings.name,
enterTransition = { slideInTo(Dir.Left) }, 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) } popExitTransition = { slideOutFrom(Dir.Left) }
) { ) {
val vm = viewModel<SettingsViewModel>(factory = factory) val vm = viewModel<SettingsViewModel>(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<SettingsViewModel>(
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)
}
)
} }
} }
} }

View file

@ -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))
}
}
)
}

View file

@ -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<String?>(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))
}
}
}

View file

@ -1,11 +1,10 @@
package app.lonecloud.prism.activities.ui 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.Arrangement
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Settings import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -13,7 +12,6 @@ import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.AppBar
import org.unifiedpush.android.distributor.ui.compose.CardDisableBatteryOptimisation import org.unifiedpush.android.distributor.ui.compose.CardDisableBatteryOptimisation
import org.unifiedpush.android.distributor.ui.compose.CardDisabledForMigration 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.PermissionsUi
import org.unifiedpush.android.distributor.ui.compose.RegistrationList import org.unifiedpush.android.distributor.ui.compose.RegistrationList
import org.unifiedpush.android.distributor.ui.compose.RegistrationListHeading import org.unifiedpush.android.distributor.ui.compose.RegistrationListHeading
@ -125,17 +123,14 @@ fun MainScreen(
CardDisableBatteryOptimisation(viewModel.batteryOptimisationViewModel) CardDisableBatteryOptimisation(viewModel.batteryOptimisationViewModel)
RegistrationListHeading( RegistrationListHeading()
modifier = Modifier.clickable(
indication = null,
interactionSource = remember { MutableInteractionSource() }
) {
viewModel.addDebugClick()
}
)
} }
RegistrationList(viewModel.registrationsViewModel) LazyColumn(
modifier = Modifier.padding(horizontal = 8.dp)
) {
RegistrationList(viewModel.registrationsViewModel)
}
} }
if (viewModel.mainUiState.showPermissionDialog) { if (viewModel.mainUiState.showPermissionDialog) {
PermissionsUi { PermissionsUi {
@ -143,22 +138,8 @@ fun MainScreen(
migrationViewModel.mayShowFallbackIntro() 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) { if (migrationViewModel.state.canMigrate) {
DistribMigrationUi(migrationViewModel) DistribMigrationDialogs(migrationViewModel)
} }
} }

View file

@ -7,12 +7,10 @@ data class InstalledApp(
) )
data class MainUiState( data class MainUiState(
val showDebugInfo: Boolean = false,
val showPermissionDialog: Boolean = true, val showPermissionDialog: Boolean = true,
val showAppDetails: Boolean = false, val showAppDetails: Boolean = false,
val isLoadingEndpoint: Boolean = false, val isLoadingEndpoint: Boolean = false,
val currentEndpoint: String = "", val currentEndpoint: String = "",
val showAddAppDialog: Boolean = false,
val installedApps: List<InstalledApp> = emptyList(), val installedApps: List<InstalledApp> = emptyList(),
val prismServerConfigured: Boolean = false val prismServerConfigured: Boolean = false
) )

View file

@ -5,6 +5,8 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding 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.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
@ -13,12 +15,14 @@ import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.RectangleShape import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@Composable @Composable
fun PrismTogglePreference( fun PrismTogglePreference(
title: String, title: String,
description: String? = null, description: String? = null,
icon: ImageVector? = null,
checked: Boolean, checked: Boolean,
onCheckedChange: (Boolean) -> Unit onCheckedChange: (Boolean) -> Unit
) { ) {
@ -29,9 +33,17 @@ fun PrismTogglePreference(
) { ) {
Row( Row(
modifier = Modifier.padding(16.dp), modifier = Modifier.padding(16.dp),
horizontalArrangement = Arrangement.SpaceBetween, horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
icon?.let {
Icon(
imageVector = it,
contentDescription = null,
modifier = Modifier.size(24.dp),
tint = MaterialTheme.colorScheme.onSurfaceVariant
)
}
Column( Column(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
verticalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp)

View file

@ -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<String?>(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")
}
}
)
}

View file

@ -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<String?>(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")
}
}
)
}

View file

@ -2,9 +2,23 @@ package app.lonecloud.prism.activities.ui
import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column 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.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Alignment 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.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview 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.PreviewFactory
import app.lonecloud.prism.activities.SettingsViewModel import app.lonecloud.prism.activities.SettingsViewModel
import app.lonecloud.prism.activities.ThemeViewModel 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 import org.unifiedpush.android.distributor.ui.vm.DistribMigrationViewModel
@Composable @Composable
fun SettingsScreen( fun SettingsScreen(
viewModel: SettingsViewModel, viewModel: SettingsViewModel,
themeViewModel: ThemeViewModel, themeViewModel: ThemeViewModel,
migrationViewModel: DistribMigrationViewModel migrationViewModel: DistribMigrationViewModel,
onNavigateToServerConfig: () -> Unit = {}
) { ) {
val lifecycleOwner = LocalLifecycleOwner.current val lifecycleOwner = LocalLifecycleOwner.current
LaunchedEffect(Unit) { LaunchedEffect(Unit) {
@ -34,31 +49,64 @@ fun SettingsScreen(
} }
Column( Column(
horizontalAlignment = Alignment.Start, horizontalAlignment = Alignment.Start,
verticalArrangement = Arrangement.spacedBy(20.dp) verticalArrangement = Arrangement.spacedBy(4.dp)
) { ) {
PrismServerConfigButton( Surface(
currentUrl = viewModel.state.prismServerUrl, onClick = onNavigateToServerConfig,
currentApiKey = viewModel.state.prismApiKey, modifier = Modifier.fillMaxWidth(),
onConfigure = { url, apiKey -> shape = RectangleShape
viewModel.updatePrismServerUrl(url) ) {
viewModel.updatePrismApiKey(apiKey) 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( PrismTogglePreference(
title = stringResource(R.string.app_dropdown_show_toasts), title = stringResource(R.string.app_dropdown_show_toasts),
description = stringResource(R.string.show_toasts_description),
icon = Icons.Filled.Notifications,
checked = viewModel.state.showToasts, checked = viewModel.state.showToasts,
onCheckedChange = { viewModel.toggleShowToasts() } onCheckedChange = { viewModel.toggleShowToasts() }
) )
PrismTogglePreference( PrismTogglePreference(
title = stringResource(R.string.dynamic_colors_title), title = stringResource(R.string.dynamic_colors_title),
description = stringResource(R.string.dynamic_colors_description),
icon = Icons.Filled.Palette,
checked = themeViewModel.dynamicColors, checked = themeViewModel.dynamicColors,
onCheckedChange = { themeViewModel.toggleDynamicColors() } onCheckedChange = { themeViewModel.toggleDynamicColors() }
) )
} }
if (migrationViewModel.state.canMigrate) { if (migrationViewModel.state.canMigrate) {
DistribMigrationUi(migrationViewModel) DistribMigrationDialogs(migrationViewModel)
} }
} }

View file

@ -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
)
}

View file

@ -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()
}

View file

@ -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
)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 43 KiB

View file

@ -47,8 +47,8 @@
<string name="add_manual_app_content_description">Add manual app</string> <string name="add_manual_app_content_description">Add manual app</string>
<string name="debug_title">Debug Information</string> <string name="debug_title">Debug Information</string>
<string name="configure_server">Configure Prism Server</string> <string name="configure_server">Configure Prism Server</string>
<string name="prism_server_description">Self-host a Prism server to unlock manual app registrations. Otherwise, this distributor works normally with UnifiedPush apps.</string> <string name="prism_server_info">Prism server enables manual app registrations and multi-device sync. Self-host your own or use a public instance.</string>
<string name="prism_server_repo_link">https://github.com/lone-cloud/prism</string> <string name="prism_server_learn_more">Learn more about Prism</string>
<string name="prism_server_configured">Prism server configured</string> <string name="prism_server_configured">Prism server configured</string>
<string name="prism_server_configured_with_version">%1$s (v%2$s)</string> <string name="prism_server_configured_with_version">%1$s (v%2$s)</string>
<string name="prism_server_not_configured">Prism server not configured</string> <string name="prism_server_not_configured">Prism server not configured</string>
@ -60,10 +60,19 @@
<string name="connection_successful">Connection successful</string> <string name="connection_successful">Connection successful</string>
<string name="connection_failed">Connection failed</string> <string name="connection_failed">Connection failed</string>
<string name="test_and_save_button">Test and Save</string> <string name="test_and_save_button">Test and Save</string>
<string name="clear_server_button">Clear</string> <string name="clear_server_button">Remove</string>
<string name="clear_server_confirm_title">Remove Prism Server?</string> <string name="clear_server_confirm_title">Remove Prism Server?</string>
<string name="clear_server_confirm_message_no_apps">This will remove your Prism server configuration.</string> <string name="clear_server_confirm_message_no_apps">This will remove your Prism server configuration.</string>
<string name="clear_server_confirm_message_with_apps">You have %d manual app registration(s). Clearing the server will delete them from the server and remove the configuration.</string> <string name="clear_server_confirm_message_with_apps">You have %d manual app registration(s). Clearing the server will delete them from the server and remove the configuration.</string>
<string name="app_dropdown_show_toasts">Notify when apps register</string> <string name="app_dropdown_show_toasts">Notify when apps register</string>
<string name="show_toasts_description">Show a notification when apps register or unregister</string>
<string name="dynamic_colors_title">Dynamic Colors</string> <string name="dynamic_colors_title">Dynamic Colors</string>
<string name="dynamic_colors_description">Use colors from your wallpaper</string>
<!-- Intro screen strings -->
<string name="intro_welcome_title">Welcome to Prism</string>
<string name="intro_welcome_message">Prism is a UnifiedPush distributor that supports manual app registrations through an optional Prism server.</string>
<string name="intro_server_optional">Configure a Prism server now or skip to set it up later in Settings.</string>
<string name="intro_continue_button">Continue</string>
<string name="intro_skip_button">Skip for now</string>
</resources> </resources>

View file

@ -4,7 +4,7 @@ androidx-activityCompose = "1.12.1"
androidx-lifecycle = "2.10.0" androidx-lifecycle = "2.10.0"
androidx-work = "2.11.0" androidx-work = "2.11.0"
appcompat = "1.7.1" appcompat = "1.7.1"
unifiedpush_distributor = "0.7.1" unifiedpush_distributor = "0.7.2"
unifiedpush_distributor_base = "0.7.0" unifiedpush_distributor_base = "0.7.0"
accompanist_permissions = "0.37.3" accompanist_permissions = "0.37.3"
tink = "1.15.0" tink = "1.15.0"