From fd9535e0974b2b619e55795e3960f5d27b325351 Mon Sep 17 00:00:00 2001 From: Diamond <33725716+DiamondMiner88@users.noreply.github.com> Date: Sat, 7 Jan 2023 18:28:26 -0800 Subject: [PATCH] feat(plugins): toggling + error handling --- .../ui/component/plugins/PluginCard.kt | 22 ++-- .../manager/ui/screen/PluginsScreen.kt | 84 +++++++++------ .../manager/ui/viewmodel/PluginsViewModel.kt | 102 +++++++++++++----- app/src/main/res/values/strings.xml | 3 +- 4 files changed, 139 insertions(+), 72 deletions(-) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/PluginCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/PluginCard.kt index a01b371..3f9a36d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/PluginCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/PluginCard.kt @@ -12,8 +12,7 @@ import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.History import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.runtime.saveable.rememberSaveable +import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha @@ -33,17 +32,18 @@ import kotlinx.collections.immutable.persistentListOf @Composable fun PluginCard( plugin: Plugin, + enabled: Boolean, onClickDelete: () -> Unit, - onClickShowChangelog: () -> Unit + onClickShowChangelog: () -> Unit, + onSetEnabled: (Boolean) -> Unit, ) { val uriHandler = LocalUriHandler.current - var isEnabled by rememberSaveable { mutableStateOf(true) } ElevatedCard { // Header Row( modifier = Modifier - .clickable { isEnabled = !isEnabled } + .clickable { onSetEnabled(!enabled) } .padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 14.dp), verticalAlignment = Alignment.CenterVertically ) { @@ -103,14 +103,16 @@ fun PluginCard( // Toggle Switch Switch( - checked = isEnabled, - onCheckedChange = { isEnabled = it } + checked = enabled, + onCheckedChange = { onSetEnabled(!enabled) } ) } - Divider(modifier = Modifier - .alpha(0.3f) - .padding(horizontal = 16.dp)) + Divider( + modifier = Modifier + .alpha(0.3f) + .padding(horizontal = 16.dp) + ) Column( modifier = Modifier diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screen/PluginsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screen/PluginsScreen.kt index 2254f13..5b2c195 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screen/PluginsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screen/PluginsScreen.kt @@ -86,38 +86,56 @@ fun PluginsScreen( .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(14.dp) ) { - val search = viewModel.search + val focusManager = LocalFocusManager.current - if (viewModel.plugins.isNotEmpty()) { - val focusManager = LocalFocusManager.current - - OutlinedTextField( - modifier = Modifier.fillMaxWidth(), - maxLines = 1, - value = viewModel.search, - onValueChange = viewModel::search, - singleLine = true, - shape = RoundedCornerShape(12.dp), - label = { Text(stringResource(R.string.action_search)) }, - trailingIcon = { - if (viewModel.search.isNotBlank()) { - IconButton(onClick = viewModel::clearSearch) { - Icon( - imageVector = Icons.Default.Close, - contentDescription = stringResource(R.string.action_clear) - ) - } - } else { + OutlinedTextField( + modifier = Modifier.fillMaxWidth(), + maxLines = 1, + value = viewModel.search, + onValueChange = viewModel::search, + singleLine = true, + shape = RoundedCornerShape(12.dp), + label = { Text(stringResource(R.string.action_search)) }, + trailingIcon = { + if (viewModel.search.isNotBlank()) { + IconButton(onClick = viewModel::clearSearch) { Icon( - imageVector = Icons.Default.Search, - contentDescription = stringResource(R.string.plugins_search) + imageVector = Icons.Default.Close, + contentDescription = stringResource(R.string.action_clear) ) } - }, - keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Search), - keyboardActions = KeyboardActions { focusManager.clearFocus() } - ) + } else { + Icon( + imageVector = Icons.Default.Search, + contentDescription = stringResource(R.string.plugins_search) + ) + } + }, + keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Search), + keyboardActions = KeyboardActions { focusManager.clearFocus() } + ) + if (viewModel.error) { + Box( + modifier = Modifier.fillMaxSize() + ) { + Column( + modifier = Modifier.align(Alignment.Center), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(8.dp), + ) { + Icon( + imageVector = Icons.Default.Warning, + contentDescription = null, + tint = MaterialTheme.colorScheme.error, + ) + Text( + text = stringResource(R.string.plugins_error), + color = MaterialTheme.colorScheme.error, + ) + } + } + } else if (viewModel.plugins.isNotEmpty()) { LazyColumn( contentPadding = PaddingValues(bottom = 15.dp, top = 6.dp), verticalArrangement = Arrangement.spacedBy(14.dp) @@ -125,16 +143,18 @@ fun PluginsScreen( items( viewModel.plugins.filter { plugin -> plugin.manifest.run { - name.contains(search, true) - || description.contains(search, true) - || authors.any { (name) -> name.contains(search, true) } + name.contains(viewModel.search, true) + || description.contains(viewModel.search, true) + || authors.any { (name) -> name.contains(viewModel.search, true) } } } ) { plugin -> PluginCard( plugin = plugin, + enabled = viewModel.enabled[plugin.manifest.name] ?: true, onClickDelete = { viewModel.showUninstallDialog(plugin) }, - onClickShowChangelog = { viewModel.showChangelogDialog(plugin) } + onClickShowChangelog = { viewModel.showChangelogDialog(plugin) }, + onSetEnabled = { viewModel.setPluginEnabled(plugin.manifest.name, it) } ) } } @@ -147,7 +167,7 @@ fun PluginsScreen( horizontalAlignment = Alignment.CenterHorizontally ) { Icon( - imageVector = Icons.Default.Extension, + imageVector = Icons.Default.ExtensionOff, contentDescription = null ) Text(stringResource(R.string.plugins_none_installed)) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/PluginsViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/PluginsViewModel.kt index a385565..4240938 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/PluginsViewModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/PluginsViewModel.kt @@ -1,19 +1,17 @@ package com.aliucord.manager.ui.viewmodel import android.util.Log -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.setValue +import androidx.compose.runtime.* import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import com.aliucord.manager.BuildConfig import com.aliucord.manager.aliucordDir import com.aliucord.manager.domain.model.Plugin -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import kotlinx.coroutines.withContext +import kotlinx.coroutines.* +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.* +@OptIn(ExperimentalSerializationApi::class) class PluginsViewModel : ViewModel() { var showChangelogDialog by mutableStateOf(false) private set @@ -27,13 +25,11 @@ class PluginsViewModel : ViewModel() { var selectedPlugin by mutableStateOf(null) private set - val plugins = mutableStateListOf() + var error by mutableStateOf(false) + private set - init { - viewModelScope.launch { - loadPlugins() - } - } + val plugins = mutableStateListOf() + val enabled = mutableStateMapOf() fun search(search: String) { this.search = search @@ -66,26 +62,74 @@ class PluginsViewModel : ViewModel() { plugins.remove(plugin) } - private suspend fun loadPlugins(): Unit = withContext(Dispatchers.IO) { - val pluginsDir = aliucordDir.resolve("plugins") + private val settingsFile = aliucordDir.resolve("settings/Aliucord.json") - if (!pluginsDir.exists() && !pluginsDir.mkdirs()) { - Log.e(BuildConfig.TAG, "Failed to create plugins dir. Missing Permissions?") - return@withContext + fun setPluginEnabled(plugin: String, enable: Boolean) { + viewModelScope.launch(Dispatchers.IO) { + withContext(Dispatchers.Main) { + enabled.clear() + enabled.putAll(loadEnabled()) + enabled[plugin] = enable + } + + try { + val settings = settingsFile.inputStream() + .use { Json.decodeFromStream>(it) } + .toMutableMap() + + settings["AC_PM_$plugin"] = JsonPrimitive(enable) + + settingsFile.outputStream() + .use { Json.encodeToStream(settings, it) } + } catch (t: Throwable) { + Log.e(BuildConfig.TAG, "Failed to write Aliucord.json: ${Log.getStackTraceString(t)}") + withContext(Dispatchers.Main) { error = true } + } + } + } + + private suspend fun loadEnabled(): Map = withContext(Dispatchers.IO) { + if (!settingsFile.exists()) return@withContext emptyMap() + + try { + settingsFile.inputStream() + .use { Json.decodeFromStream>(it) } + .filterKeys { it.startsWith("AC_PM_") } + .mapKeys { it.key.substring("AC_PM_".length) } + .mapValues { it.value.jsonPrimitive.boolean } + } catch (t: Throwable) { + Log.e(BuildConfig.TAG, "Failed to load Aliucord.json ${Log.getStackTraceString(t)}") + withContext(Dispatchers.Main) { error = true } + emptyMap() + } + } + + init { + viewModelScope.launch { + enabled.putAll(loadEnabled()) } - val files = pluginsDir.listFiles { file -> file.extension == "zip" } ?: return@withContext - val loadedPlugins = files.mapNotNull { file -> - try { - Plugin(file) - } catch (e: Exception) { - Log.e(BuildConfig.TAG, "Failed to load plugin ${file.nameWithoutExtension}", e) - null - } - }.sortedBy { it.manifest.name } + viewModelScope.launch(Dispatchers.IO) { + val pluginsDir = aliucordDir.resolve("plugins") - withContext(Dispatchers.Main) { - plugins.addAll(loadedPlugins) + if (!pluginsDir.exists() && !pluginsDir.mkdirs()) { + Log.e(BuildConfig.TAG, "Failed to create plugins dir. Missing Permissions?") + return@launch + } + + val files = pluginsDir.listFiles { file -> file.extension == "zip" } ?: return@launch + val loadedPlugins = files.mapNotNull { file -> + try { + Plugin(file) + } catch (e: Exception) { + Log.e(BuildConfig.TAG, "Failed to load plugin ${file.name}", e) + null + } + }.sortedBy { it.manifest.name } + + withContext(Dispatchers.Main) { + plugins.addAll(loadedPlugins) + } } } } diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index cde20bc..e3edbd0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -32,7 +32,7 @@ Cleared cache! Grant Permissions - In order for Aliucord Manager to work properly, you need to grant file permissions. Since shared data in stored in ~/Aliucord, permissions are required in order for manager to function. + In order for Aliucord Manager to function, file permissions are required. Since shared data is stored in ~/Aliucord, permissions are required in order to access it. Failed to launch Aliucord @@ -65,6 +65,7 @@ No plugins installed! Changelog Media §plugin§%1$s§ v%2$s + An error occurred while processing plugins! Back Home