feat(plugins): toggling + error handling
This commit is contained in:
parent
3116e38e4d
commit
fd9535e097
4 changed files with 139 additions and 72 deletions
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue