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.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)
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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}")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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.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)
|
||||||
|
}
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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
|
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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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.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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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="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>
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue