feat(PluginsScreen): improve layout internals
This commit is contained in:
parent
7384b562d5
commit
1433108ec2
10 changed files with 258 additions and 174 deletions
|
|
@ -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),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
|
|
@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue