mirror of
https://github.com/lone-cloud/prism-android
synced 2026-06-03 19:54:44 -07:00
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:
parent
5fcc504121
commit
8b41645f82
25 changed files with 1120 additions and 720 deletions
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,14 +211,18 @@ object PrismServerClient {
|
|||
|
||||
client.newCall(request).execute().use { response ->
|
||||
if (response.isSuccessful) {
|
||||
onSuccess()
|
||||
withContext(Dispatchers.Main) { onSuccess() }
|
||||
} else {
|
||||
withContext(Dispatchers.Main) {
|
||||
onError("Connection failed: ${response.code} ${response.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
withContext(Dispatchers.Main) {
|
||||
onError("Connection error: ${e.message}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<Job>().toMutableList()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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<InstalledApp?>(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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
||||
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,8 +79,47 @@ class SettingsViewModel(
|
|||
PrismServerClient.registerAllApps(it)
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,6 +104,9 @@ fun App(
|
|||
|
||||
Scaffold(
|
||||
topBar = {
|
||||
if (currentScreen == AppScreen.Intro) {
|
||||
null
|
||||
} else {
|
||||
when (currentScreen) {
|
||||
AppScreen.Main -> {
|
||||
MainAppBarOrSelection(
|
||||
|
|
@ -111,11 +122,16 @@ fun App(
|
|||
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<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(
|
||||
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<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)
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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,40 +123,23 @@ fun MainScreen(
|
|||
|
||||
CardDisableBatteryOptimisation(viewModel.batteryOptimisationViewModel)
|
||||
|
||||
RegistrationListHeading(
|
||||
modifier = Modifier.clickable(
|
||||
indication = null,
|
||||
interactionSource = remember { MutableInteractionSource() }
|
||||
) {
|
||||
viewModel.addDebugClick()
|
||||
}
|
||||
)
|
||||
RegistrationListHeading()
|
||||
}
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.padding(horizontal = 8.dp)
|
||||
) {
|
||||
RegistrationList(viewModel.registrationsViewModel)
|
||||
}
|
||||
}
|
||||
if (viewModel.mainUiState.showPermissionDialog) {
|
||||
PermissionsUi {
|
||||
viewModel.closePermissionDialog()
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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<InstalledApp> = emptyList(),
|
||||
val prismServerConfigured: Boolean = false
|
||||
)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
23
app/src/main/java/app/lonecloud/prism/utils/ServerUtils.kt
Normal file
23
app/src/main/java/app/lonecloud/prism/utils/ServerUtils.kt
Normal 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
|
||||
)
|
||||
}
|
||||
BIN
app/src/main/res/drawable/app_logo.webp
Normal file
BIN
app/src/main/res/drawable/app_logo.webp
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 43 KiB |
|
|
@ -47,8 +47,8 @@
|
|||
<string name="add_manual_app_content_description">Add manual app</string>
|
||||
<string name="debug_title">Debug Information</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_repo_link">https://github.com/lone-cloud/prism</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_learn_more">Learn more about Prism</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_not_configured">Prism server not configured</string>
|
||||
|
|
@ -60,10 +60,19 @@
|
|||
<string name="connection_successful">Connection successful</string>
|
||||
<string name="connection_failed">Connection failed</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_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="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_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>
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue