feat(plugins): toggling + error handling

This commit is contained in:
Diamond 2023-01-07 18:28:26 -08:00
parent 3116e38e4d
commit fd9535e097
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
4 changed files with 139 additions and 72 deletions

View file

@ -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

View file

@ -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))

View file

@ -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<Plugin?>(null)
private set
val plugins = mutableStateListOf<Plugin>()
var error by mutableStateOf(false)
private set
init {
viewModelScope.launch {
loadPlugins()
}
}
val plugins = mutableStateListOf<Plugin>()
val enabled = mutableStateMapOf<String, Boolean>()
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<Map<String, JsonElement>>(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<String, Boolean> = withContext(Dispatchers.IO) {
if (!settingsFile.exists()) return@withContext emptyMap()
try {
settingsFile.inputStream()
.use { Json.decodeFromStream<Map<String, JsonElement>>(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)
}
}
}
}

View file

@ -32,7 +32,7 @@
<string name="action_cleared_cache">Cleared cache!</string>
<string name="permissions_grant_title">Grant Permissions</string>
<string name="permissions_grant_body">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.</string>
<string name="permissions_grant_body">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.</string>
<string name="launch_aliucord_fail">Failed to launch Aliucord</string>
@ -65,6 +65,7 @@
<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>
<string name="plugins_error">An error occurred while processing plugins!</string>
<string name="navigation_back">Back</string>
<string name="navigation_home">Home</string>