feat(PluginsScreen): improve layout internals

This commit is contained in:
rushiiMachine 2025-04-11 03:49:22 -07:00
parent 7384b562d5
commit 1433108ec2
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
10 changed files with 258 additions and 174 deletions

View file

@ -1,11 +1,13 @@
package com.aliucord.manager.ui.components
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.*
import androidx.compose.foundation.layout.size
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
import com.aliucord.manager.ui.util.mirrorVertically
@ -15,13 +17,20 @@ fun ResetToDefaultButton(
onClick: () -> Unit,
modifier: Modifier = Modifier,
) {
AnimatedVisibility(visible = enabled, modifier = modifier) {
AnimatedVisibility(
visible = enabled,
enter = fadeIn() + slideInHorizontally(),
exit = fadeOut() + slideOutHorizontally(),
modifier = modifier,
) {
IconButton(onClick = onClick) {
Icon(
painter = painterResource(R.drawable.ic_refresh),
tint = MaterialTheme.colorScheme.secondary,
contentDescription = stringResource(R.string.action_reset_default),
modifier = Modifier.mirrorVertically(),
modifier = Modifier
.mirrorVertically()
.size(26.dp),
)
}
}

View file

@ -92,7 +92,7 @@ class PluginsModel(
val deleteSuccess = try {
File(pluginPath).delete()
} catch (t: Throwable) {
t.printStackTrace()
Log.e(BuildConfig.TAG, "Failed to delete plugin", t)
false
}

View file

@ -9,25 +9,21 @@ import android.os.Parcelable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleResumeEffect
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import com.aliucord.manager.R
import com.aliucord.manager.ui.components.BackButton
import com.aliucord.manager.ui.screens.plugins.components.Changelog
import com.aliucord.manager.ui.screens.plugins.components.PluginCard
import com.aliucord.manager.ui.screens.plugins.components.*
import com.aliucord.manager.ui.screens.plugins.components.dialogs.UninstallPluginDialog
import com.aliucord.manager.ui.screens.plugins.model.PluginItem
import com.aliucord.manager.ui.util.paddings.*
import kotlinx.collections.immutable.ImmutableList
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@ -39,7 +35,6 @@ class PluginsScreen : Screen, Parcelable {
@Composable
override fun Content() {
val model = getScreenModel<PluginsModel>()
val plugins = model.filteredPlugins.collectAsState()
// Refresh plugins list on activity resume or when this initially opens
LifecycleResumeEffect(Unit) {
@ -63,156 +58,81 @@ class PluginsScreen : Screen, Parcelable {
)
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.plugins_title)) },
navigationIcon = { BackButton() },
)
}
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
PluginSearch(
currentFilter = model.searchText.collectAsState().value,
onFilterChange = model::setSearchText,
modifier = Modifier.fillMaxWidth(),
)
PluginsScreenContent(
searchText = model.searchText.collectAsState(),
setSearchText = model::setSearchText,
isError = model.error,
plugins = model.filteredPlugins.collectAsState().value,
onPluginUninstall = model::showUninstallDialog,
onPluginChangelog = model::showChangelogDialog,
onPluginToggle = model::setPluginEnabled,
)
}
}
if (model.error) {
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
painter = painterResource(R.drawable.ic_warning),
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
)
Text(
text = stringResource(R.string.plugins_error),
color = MaterialTheme.colorScheme.error,
)
}
@Composable
fun PluginsScreenContent(
searchText: State<String>,
setSearchText: (String) -> Unit,
isError: Boolean,
plugins: ImmutableList<PluginItem>,
onPluginUninstall: (PluginItem) -> Unit,
onPluginChangelog: (PluginItem) -> Unit,
onPluginToggle: (name: String, enabled: Boolean) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.plugins_title)) },
navigationIcon = { BackButton() },
)
}
) { paddingValues ->
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxSize()
.padding(paddingValues.exclude(PaddingValuesSides.Bottom))
.padding(horizontal = 20.dp)
) {
PluginSearch(
currentFilter = searchText,
onFilterChange = setSearchText,
modifier = Modifier.fillMaxWidth()
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = paddingValues
.exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top)
.add(PaddingValues(vertical = 12.dp)),
modifier = Modifier.fillMaxSize(),
) {
when {
isError -> item(key = "ERROR") {
PluginsError(modifier = Modifier.fillParentMaxSize())
}
} else if (plugins.value.isNotEmpty()) {
LazyColumn(
contentPadding = PaddingValues(bottom = 15.dp, top = 6.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
items(items = plugins.value) { plugin ->
plugins.isNotEmpty() -> {
items(
items = plugins,
contentType = { "PLUGIN" },
key = { it.path },
) { plugin ->
PluginCard(
plugin = plugin,
onClickDelete = { model.showUninstallDialog(plugin) },
onClickShowChangelog = { model.showChangelogDialog(plugin) },
onSetEnabled = { model.setPluginEnabled(pluginName = plugin.manifest.name, it) }
onClickDelete = { onPluginUninstall(plugin) },
onClickShowChangelog = { onPluginChangelog(plugin) },
onSetEnabled = { onPluginToggle(plugin.manifest.name, it) },
)
}
}
} else {
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
painter = painterResource(R.drawable.ic_extension_off),
contentDescription = null
)
Text(stringResource(R.string.plugins_none_installed))
}
else -> item(key = "PLUGINS_NONE") {
PluginsNone(modifier = Modifier.fillParentMaxSize())
}
}
}
}
}
}
@Composable
private fun PluginSearch(
currentFilter: String,
onFilterChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
OutlinedTextField(
value = currentFilter,
onValueChange = onFilterChange,
singleLine = true,
shape = RoundedCornerShape(12.dp),
label = { Text(stringResource(R.string.action_search)) },
trailingIcon = {
val isFilterBlank by remember { derivedStateOf { currentFilter.isEmpty() } }
if (!isFilterBlank) {
IconButton(onClick = { onFilterChange("") }) {
Icon(
painter = painterResource(R.drawable.ic_close),
contentDescription = stringResource(R.string.action_clear)
)
}
} else {
Icon(
painter = painterResource(R.drawable.ic_search),
contentDescription = stringResource(R.string.plugins_search)
)
}
},
keyboardOptions = KeyboardOptions(autoCorrectEnabled = false, imeAction = ImeAction.Search),
keyboardActions = KeyboardActions { focusManager.clearFocus() },
modifier = modifier,
)
}
@Composable
private fun UninstallPluginDialog(
pluginName: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
painter = painterResource(R.drawable.ic_delete_forever),
contentDescription = stringResource(R.string.action_uninstall)
)
},
title = {
Text("${stringResource(R.string.action_uninstall)} $pluginName")
},
text = {
Text(stringResource(R.string.plugins_delete_plugin_body))
},
confirmButton = {
Button(
onClick = onConfirm,
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
Button(
onClick = onDismiss,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Text(stringResource(android.R.string.cancel))
}
}
)
}

View file

@ -36,10 +36,10 @@ fun PluginCard(
ElevatedCard {
// Header
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable { onSetEnabled(!plugin.enabled) }
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 14.dp),
verticalAlignment = Alignment.CenterVertically
) {
Column {
// Name
@ -107,21 +107,19 @@ fun PluginCard(
) {
// Description
Text(
text = plugin.manifest.description,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.heightIn(max = 150.dp, min = 40.dp)
.padding(bottom = 20.dp),
text = plugin.manifest.description,
style = MaterialTheme.typography.bodyMedium
)
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
IconButton(
onClick = { uriHandler.openUri(plugin.manifest.repositoryUrl) },
modifier = Modifier.size(25.dp),
onClick = {
uriHandler.openUri(plugin.manifest.repositoryUrl)
}
) {
Icon(
modifier = Modifier.fillMaxSize(),
@ -131,10 +129,13 @@ fun PluginCard(
}
if (plugin.manifest.changelog != null) {
IconButton(modifier = Modifier.size(25.dp), onClick = onClickShowChangelog) {
IconButton(
onClick = onClickShowChangelog,
modifier = Modifier.size(25.dp),
) {
Icon(
painter = painterResource(R.drawable.ic_history),
contentDescription = stringResource(R.string.plugins_view_changelog, plugin.manifest.name),
contentDescription = stringResource(R.string.plugins_view_changelog),
modifier = Modifier.fillMaxSize(),
)
}
@ -142,7 +143,10 @@ fun PluginCard(
Spacer(Modifier.weight(1f, true))
IconButton(modifier = Modifier.size(25.dp), onClick = onClickDelete) {
IconButton(
onClick = onClickDelete,
modifier = Modifier.size(25.dp),
) {
Icon(
modifier = Modifier.fillMaxSize(),
painter = painterResource(R.drawable.ic_delete_forever),

View file

@ -0,0 +1,46 @@
package com.aliucord.manager.ui.screens.plugins.components
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
import com.aliucord.manager.ui.components.ResetToDefaultButton
@Composable
fun PluginSearch(
currentFilter: State<String>,
onFilterChange: (String) -> Unit,
modifier: Modifier = Modifier.Companion,
) {
val focusManager = LocalFocusManager.current
OutlinedTextField(
value = currentFilter.value,
onValueChange = onFilterChange,
singleLine = true,
shape = MaterialTheme.shapes.medium,
label = { Text(stringResource(R.string.action_search)) },
trailingIcon = {
val isFilterBlank by remember { derivedStateOf { currentFilter.value.isEmpty() } }
ResetToDefaultButton(
enabled = !isFilterBlank,
onClick = { onFilterChange("") },
modifier = Modifier.padding(end = 4.dp),
)
},
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
imeAction = ImeAction.Companion.Search
),
keyboardActions = KeyboardActions { focusManager.clearFocus() },
modifier = modifier,
)
}

View file

@ -0,0 +1,35 @@
package com.aliucord.manager.ui.screens.plugins.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
@Composable
fun PluginsError(modifier: Modifier = Modifier) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier,
) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
painter = painterResource(R.drawable.ic_warning),
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
)
Text(
text = stringResource(R.string.plugins_error),
color = MaterialTheme.colorScheme.error,
)
}
}
}

View file

@ -0,0 +1,31 @@
package com.aliucord.manager.ui.screens.plugins.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.aliucord.manager.R
@Composable
fun PluginsNone(modifier: Modifier = Modifier) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier,
) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
painter = painterResource(R.drawable.ic_extension_off),
contentDescription = null
)
Text(stringResource(R.string.plugins_none_installed))
}
}
}

View file

@ -0,0 +1,48 @@
package com.aliucord.manager.ui.screens.plugins.components.dialogs
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.aliucord.manager.R
@Composable
fun UninstallPluginDialog(
pluginName: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
painter = painterResource(R.drawable.ic_delete_forever),
contentDescription = null,
)
},
title = {
Text(stringResource(R.string.plugins_delete_plugin, pluginName))
},
text = {
Text(stringResource(R.string.plugins_delete_plugin_body))
},
confirmButton = {
Button(
onClick = onConfirm,
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
Button(
onClick = onDismiss,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Text(stringResource(android.R.string.cancel))
}
}
)
}

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960">
<path
android:fillColor="#e8eaed"
android:pathData="M380,640q-109,0 -184.5,-75.5T120,380q0,-109 75.5,-184.5T380,120q109,0 184.5,75.5T640,380q0,44 -14,83t-38,69l224,224q11,11 11,28t-11,28q-11,11 -28,11t-28,-11L532,588q-30,24 -69,38t-83,14ZM380,560q75,0 127.5,-52.5T560,380q0,-75 -52.5,-127.5T380,200q-75,0 -127.5,52.5T200,380q0,75 52.5,127.5T380,560Z" />
</vector>

View file

@ -60,9 +60,9 @@
<string name="theme_light">Light</string>
<string name="plugins_title">Plugins</string>
<string name="plugins_view_changelog">View changelog for %1$s</string>
<string name="plugins_view_changelog">View changelog</string>
<string name="plugins_delete_plugin">Uninstall %s</string>
<string name="plugins_delete_plugin_body">Are you sure you want to uninstall this plugin?</string>
<string name="plugins_search">Search Plugins</string>
<string name="plugins_none_installed">No plugins installed!</string>
<string name="plugins_changelog_media">Changelog Media</string>
<string name="plugins_plugin_title">§plugin§%1$s§ v%2$s</string>