mirror of
https://github.com/lone-cloud/prism-android
synced 2026-06-03 19:54:44 -07:00
better app picking experience, minor updates
This commit is contained in:
parent
fb7cf2dcd1
commit
4dc4f173b1
8 changed files with 135 additions and 22 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -16,3 +16,4 @@ local.properties
|
||||||
.idea
|
.idea
|
||||||
prism-release.keystore
|
prism-release.keystore
|
||||||
.kotlin/sessions/kotlin-compiler-*
|
.kotlin/sessions/kotlin-compiler-*
|
||||||
|
.VSCodeCounter
|
||||||
|
|
@ -56,6 +56,8 @@ class MainViewModel(
|
||||||
var selectedApp by mutableStateOf<InstalledApp?>(null)
|
var selectedApp by mutableStateOf<InstalledApp?>(null)
|
||||||
private set
|
private set
|
||||||
|
|
||||||
|
var prefilledName by mutableStateOf<String?>(null)
|
||||||
|
|
||||||
fun updatePrismServerConfigured(configured: Boolean) {
|
fun updatePrismServerConfigured(configured: Boolean) {
|
||||||
mainUiState = mainUiState.copy(prismServerConfigured = configured)
|
mainUiState = mainUiState.copy(prismServerConfigured = configured)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -32,19 +32,25 @@ import app.lonecloud.prism.R
|
||||||
@Composable
|
@Composable
|
||||||
fun AddAppScreen(
|
fun AddAppScreen(
|
||||||
selectedApp: InstalledApp?,
|
selectedApp: InstalledApp?,
|
||||||
|
prefilledName: String? = null,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onNavigateToAppPicker: () -> Unit,
|
onNavigateToAppPicker: () -> Unit,
|
||||||
onConfirm: (name: String, packageName: String, description: String?) -> Unit
|
onConfirm: (name: String, packageName: String, description: String?) -> Unit
|
||||||
) {
|
) {
|
||||||
var name by remember { mutableStateOf("") }
|
var name by remember { mutableStateOf(prefilledName ?: "") }
|
||||||
|
|
||||||
// Auto-fill name when app is selected
|
|
||||||
androidx.compose.runtime.LaunchedEffect(selectedApp) {
|
androidx.compose.runtime.LaunchedEffect(selectedApp) {
|
||||||
if (name.isBlank() && selectedApp != null) {
|
if (name.isBlank() && selectedApp != null) {
|
||||||
name = selectedApp.appName
|
name = selectedApp.appName
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
androidx.compose.runtime.LaunchedEffect(prefilledName) {
|
||||||
|
if (prefilledName != null && name != prefilledName) {
|
||||||
|
name = prefilledName
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Column(
|
Column(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.fillMaxSize()
|
.fillMaxSize()
|
||||||
|
|
|
||||||
|
|
@ -27,20 +27,61 @@ import androidx.compose.ui.unit.dp
|
||||||
import androidx.core.graphics.drawable.toBitmap
|
import androidx.core.graphics.drawable.toBitmap
|
||||||
import app.lonecloud.prism.R
|
import app.lonecloud.prism.R
|
||||||
|
|
||||||
|
data class PrismServerApp(val name: String, val matchedInstalledApp: InstalledApp? = null)
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun AppPickerScreen(
|
fun AppPickerScreen(
|
||||||
apps: List<InstalledApp>,
|
apps: List<InstalledApp>,
|
||||||
onNavigateBack: () -> Unit,
|
onNavigateBack: () -> Unit,
|
||||||
onSelect: (InstalledApp) -> Unit
|
onSelect: (InstalledApp) -> Unit,
|
||||||
|
onSelectPrismApp: ((String) -> Unit)? = null
|
||||||
) {
|
) {
|
||||||
var searchQuery by remember { mutableStateOf("") }
|
var searchQuery by remember { mutableStateOf("") }
|
||||||
|
var prismServerApps by remember { mutableStateOf<List<PrismServerApp>>(emptyList()) }
|
||||||
|
var prismAppsLoaded by remember { mutableStateOf(false) }
|
||||||
|
var showContent by remember { mutableStateOf(false) }
|
||||||
|
val context = androidx.compose.ui.platform.LocalContext.current
|
||||||
|
|
||||||
|
androidx.compose.runtime.LaunchedEffect(Unit) {
|
||||||
|
kotlinx.coroutines.delay(100)
|
||||||
|
showContent = true
|
||||||
|
}
|
||||||
|
|
||||||
val recommendedPackages = listOf(
|
val recommendedPackages = listOf(
|
||||||
"ch.protonmail.android",
|
"ch.protonmail.android",
|
||||||
"io.homeassistant.companion.android"
|
"io.homeassistant.companion.android"
|
||||||
)
|
)
|
||||||
|
|
||||||
val (recommendedApps, otherApps) = remember(apps, searchQuery) {
|
androidx.compose.runtime.LaunchedEffect(onSelectPrismApp) {
|
||||||
|
if (onSelectPrismApp != null) {
|
||||||
|
app.lonecloud.prism.PrismServerClient.fetchRegisteredApps(
|
||||||
|
context,
|
||||||
|
onSuccess = { serverAppNames ->
|
||||||
|
val db = app.lonecloud.prism.DatabaseFactory.getDb(context)
|
||||||
|
val localAppNames = db.listApps()
|
||||||
|
.filter { it.description?.startsWith("target:") == true }
|
||||||
|
.mapNotNull { it.title }
|
||||||
|
.toSet()
|
||||||
|
prismServerApps = serverAppNames
|
||||||
|
.filterNot { it in localAppNames }
|
||||||
|
.map { serverAppName ->
|
||||||
|
val matchedApp = apps.find {
|
||||||
|
it.appName.equals(serverAppName, ignoreCase = true)
|
||||||
|
}
|
||||||
|
PrismServerApp(serverAppName, matchedApp)
|
||||||
|
}
|
||||||
|
prismAppsLoaded = true
|
||||||
|
},
|
||||||
|
onError = {
|
||||||
|
prismAppsLoaded = true
|
||||||
|
}
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
prismAppsLoaded = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
val (recommendedApps, otherApps) = remember(apps, searchQuery, prismServerApps) {
|
||||||
val filtered = if (searchQuery.isBlank()) {
|
val filtered = if (searchQuery.isBlank()) {
|
||||||
apps
|
apps
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -50,12 +91,14 @@ fun AppPickerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
val prismAppNames = prismServerApps.map { it.name }
|
||||||
val recommended = filtered.filter { app ->
|
val recommended = filtered.filter { app ->
|
||||||
recommendedPackages.any { pkg -> app.packageName.startsWith(pkg) }
|
recommendedPackages.any { pkg -> app.packageName.startsWith(pkg) } &&
|
||||||
}
|
!prismAppNames.contains(app.appName)
|
||||||
|
}.sortedBy { it.appName.lowercase() }
|
||||||
val others = filtered.filterNot { app ->
|
val others = filtered.filterNot { app ->
|
||||||
recommendedPackages.any { pkg -> app.packageName.startsWith(pkg) }
|
recommendedPackages.any { pkg -> app.packageName.startsWith(pkg) }
|
||||||
}
|
}.sortedBy { it.appName.lowercase() }
|
||||||
|
|
||||||
recommended to others
|
recommended to others
|
||||||
}
|
}
|
||||||
|
|
@ -79,7 +122,31 @@ fun AppPickerScreen(
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp)
|
verticalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
if (recommendedApps.isNotEmpty()) {
|
if (showContent && prismAppsLoaded && prismServerApps.isNotEmpty() && onSelectPrismApp != null && searchQuery.isBlank()) {
|
||||||
|
item {
|
||||||
|
Text(
|
||||||
|
text = stringResource(R.string.from_your_server),
|
||||||
|
style = MaterialTheme.typography.titleSmall,
|
||||||
|
color = MaterialTheme.colorScheme.primary,
|
||||||
|
modifier = Modifier.padding(vertical = 8.dp, horizontal = 12.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
items(prismServerApps) { prismApp ->
|
||||||
|
PrismAppListItem(
|
||||||
|
prismApp = prismApp,
|
||||||
|
onClick = {
|
||||||
|
if (prismApp.matchedInstalledApp != null) {
|
||||||
|
onSelect(prismApp.matchedInstalledApp)
|
||||||
|
onNavigateBack()
|
||||||
|
} else {
|
||||||
|
searchQuery = prismApp.name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (showContent && recommendedApps.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.recommended_apps),
|
text = stringResource(R.string.recommended_apps),
|
||||||
|
|
@ -100,8 +167,8 @@ fun AppPickerScreen(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otherApps.isNotEmpty()) {
|
if (showContent && otherApps.isNotEmpty()) {
|
||||||
if (recommendedApps.isNotEmpty()) {
|
if (recommendedApps.isNotEmpty() || prismServerApps.isNotEmpty()) {
|
||||||
item {
|
item {
|
||||||
Text(
|
Text(
|
||||||
text = stringResource(R.string.all_apps),
|
text = stringResource(R.string.all_apps),
|
||||||
|
|
@ -162,3 +229,36 @@ private fun AppListItem(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
private fun PrismAppListItem(prismApp: PrismServerApp, onClick: () -> Unit) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.clickable(onClick = onClick)
|
||||||
|
.padding(12.dp),
|
||||||
|
horizontalArrangement = Arrangement.spacedBy(12.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically
|
||||||
|
) {
|
||||||
|
val icon = prismApp.matchedInstalledApp?.icon
|
||||||
|
if (icon != null) {
|
||||||
|
val bitmap = icon.toBitmap(48, 48)
|
||||||
|
Image(
|
||||||
|
bitmap = bitmap.asImageBitmap(),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
Image(
|
||||||
|
painter = androidx.compose.ui.res.painterResource(R.drawable.app_logo),
|
||||||
|
contentDescription = null,
|
||||||
|
modifier = Modifier.size(48.dp)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Text(
|
||||||
|
text = prismApp.name,
|
||||||
|
style = MaterialTheme.typography.bodyLarge,
|
||||||
|
color = MaterialTheme.colorScheme.primary
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -104,9 +104,7 @@ fun App(
|
||||||
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = {
|
topBar = {
|
||||||
if (currentScreen == AppScreen.Intro) {
|
if (currentScreen != AppScreen.Intro) {
|
||||||
null
|
|
||||||
} else {
|
|
||||||
when (currentScreen) {
|
when (currentScreen) {
|
||||||
AppScreen.Main -> {
|
AppScreen.Main -> {
|
||||||
MainAppBarOrSelection(
|
MainAppBarOrSelection(
|
||||||
|
|
@ -125,8 +123,11 @@ fun App(
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
floatingActionButton = {
|
floatingActionButton = {
|
||||||
val prefs = PrismPreferences(context)
|
val serverPrefs = PrismPreferences(context)
|
||||||
if (currentScreen == AppScreen.Main && !prefs.prismServerUrl.isNullOrBlank() && !prefs.prismApiKey.isNullOrBlank()) {
|
if (currentScreen == AppScreen.Main &&
|
||||||
|
!serverPrefs.prismServerUrl.isNullOrBlank() &&
|
||||||
|
!serverPrefs.prismApiKey.isNullOrBlank()
|
||||||
|
) {
|
||||||
FloatingActionButton(
|
FloatingActionButton(
|
||||||
onClick = {
|
onClick = {
|
||||||
mainViewModel.clearSelectedApp()
|
mainViewModel.clearSelectedApp()
|
||||||
|
|
@ -253,12 +254,14 @@ fun App(
|
||||||
) {
|
) {
|
||||||
AddAppScreen(
|
AddAppScreen(
|
||||||
selectedApp = mainViewModel.selectedApp,
|
selectedApp = mainViewModel.selectedApp,
|
||||||
|
prefilledName = mainViewModel.prefilledName,
|
||||||
onNavigateBack = { navController.navigateUp() },
|
onNavigateBack = { navController.navigateUp() },
|
||||||
onNavigateToAppPicker = {
|
onNavigateToAppPicker = {
|
||||||
navController.navigate(AppScreen.AppPicker.name)
|
navController.navigate(AppScreen.AppPicker.name)
|
||||||
},
|
},
|
||||||
onConfirm = { name, packageName, description ->
|
onConfirm = { name, packageName, description ->
|
||||||
mainViewModel.addManualApp(name, packageName, description)
|
mainViewModel.addManualApp(name, packageName, description)
|
||||||
|
mainViewModel.prefilledName = null
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -273,6 +276,9 @@ fun App(
|
||||||
onNavigateBack = { navController.navigateUp() },
|
onNavigateBack = { navController.navigateUp() },
|
||||||
onSelect = { app ->
|
onSelect = { app ->
|
||||||
mainViewModel.selectApp(app)
|
mainViewModel.selectApp(app)
|
||||||
|
},
|
||||||
|
onSelectPrismApp = { appName ->
|
||||||
|
mainViewModel.prefilledName = appName
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -25,7 +25,6 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
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.LocalUriHandler
|
import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.painterResource
|
import androidx.compose.ui.res.painterResource
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
|
|
@ -42,7 +41,6 @@ fun IntroScreen(onComplete: (url: String, apiKey: String) -> Unit, onSkip: () ->
|
||||||
var apiKey by remember { mutableStateOf("") }
|
var apiKey by remember { mutableStateOf("") }
|
||||||
var isTesting by remember { mutableStateOf(false) }
|
var isTesting by remember { mutableStateOf(false) }
|
||||||
var testResult by remember { mutableStateOf<String?>(null) }
|
var testResult by remember { mutableStateOf<String?>(null) }
|
||||||
val context = LocalContext.current
|
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val successMessage = stringResource(R.string.connection_successful)
|
val successMessage = stringResource(R.string.connection_successful)
|
||||||
val failedMessageTemplate = stringResource(R.string.connection_failed)
|
val failedMessageTemplate = stringResource(R.string.connection_failed)
|
||||||
|
|
|
||||||
|
|
@ -160,7 +160,6 @@ fun ServerConfigScreen(
|
||||||
}
|
}
|
||||||
|
|
||||||
val normalizedUrl = normalizeUrl(url)
|
val normalizedUrl = normalizeUrl(url)
|
||||||
val finalApiKey = apiKey.ifBlank { initialApiKey }
|
|
||||||
val isServerChanging = initialUrl.isNotBlank() && normalizedUrl != initialUrl
|
val isServerChanging = initialUrl.isNotBlank() && normalizedUrl != initialUrl
|
||||||
|
|
||||||
if (isServerChanging) {
|
if (isServerChanging) {
|
||||||
|
|
|
||||||
|
|
@ -34,8 +34,8 @@
|
||||||
<!-- Prism-specific strings -->
|
<!-- Prism-specific strings -->
|
||||||
<string name="app_name">Prism</string>
|
<string name="app_name">Prism</string>
|
||||||
<string name="settings">Settings</string>
|
<string name="settings">Settings</string>
|
||||||
<string name="add_custom_app_title">Add App</string>
|
<string name="add_custom_app_title">Add New App</string>
|
||||||
<string name="add_app_description">Register an app to receive notifications from your Prism server. Optionally choose a target app to deliver notifications to.</string>
|
<string name="add_app_description">Add an app to receive notifications from your Prism server. Optionally choose a target app to deliver notifications to.</string>
|
||||||
<string name="app_name_label">App Name</string>
|
<string name="app_name_label">App Name</string>
|
||||||
<string name="app_name_placeholder">Enter app name</string>
|
<string name="app_name_placeholder">Enter app name</string>
|
||||||
<string name="target_app_label">Target App (Optional)</string>
|
<string name="target_app_label">Target App (Optional)</string>
|
||||||
|
|
@ -44,13 +44,14 @@
|
||||||
<string name="cancel_button">Cancel</string>
|
<string name="cancel_button">Cancel</string>
|
||||||
<string name="select_target_app_title">Select Target App</string>
|
<string name="select_target_app_title">Select Target App</string>
|
||||||
<string name="recommended_apps">Recommended</string>
|
<string name="recommended_apps">Recommended</string>
|
||||||
|
<string name="from_your_server">From Your Prism Server</string>
|
||||||
<string name="all_apps">All Apps</string>
|
<string name="all_apps">All Apps</string>
|
||||||
<string name="search_apps_label">Search</string>
|
<string name="search_apps_label">Search</string>
|
||||||
<string name="search_apps_placeholder">Search for apps</string>
|
<string name="search_apps_placeholder">Search for apps</string>
|
||||||
<string name="add_manual_app_content_description">Add manual app</string>
|
<string name="add_manual_app_content_description">Add manual app</string>
|
||||||
<string name="registered_apps_heading">Registered Apps</string>
|
<string name="registered_apps_heading">Registered Apps</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_info">Prism server enables manual app registrations and must be self-hosted.</string>
|
<string name="prism_server_info">Prism server enables manual app registrations and must be self-hosted.</string>
|
||||||
<string name="prism_server_learn_more">Learn more about 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>
|
||||||
|
|
@ -70,7 +71,7 @@
|
||||||
<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="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>
|
<string name="dynamic_colors_description">Use colors from your wallpaper</string>
|
||||||
|
|
||||||
<!-- Intro screen strings -->
|
<!-- Intro screen strings -->
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue