`` feat: Bump app version to 1.1.1 `` - Fix: Use correct version name

- Remove: Unused code, plugins screen, and contributor model - Refactor:
Use new `Developer` model - Feat: Update about screen to display
developer list - Feat: Add github icon to plugin card - Chore: Fix
installer icon
This commit is contained in:
kmmiio99o 2025-11-21 22:27:54 +01:00
parent 97cf2a9b6c
commit 47737d1c19
33 changed files with 263 additions and 1283 deletions

View file

@ -26,7 +26,7 @@ android {
minSdk = 28
targetSdk = 36
versionCode = 10_11_00
versionName = "1.1.0"
versionName = "1.1.1"
vectorDrawables {
useSupportLibrary = true
}

View file

@ -17,7 +17,7 @@ import com.aliucord.manager.ui.screens.log.LogScreenModel
import com.aliucord.manager.ui.screens.logs.LogsListScreenModel
import com.aliucord.manager.ui.screens.patching.PatchingScreenModel
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsModel
import com.aliucord.manager.ui.screens.plugins.PluginsModel
import com.aliucord.manager.ui.screens.settings.SettingsModel
import com.aliucord.manager.ui.widgets.updater.UpdaterViewModel
import com.aliucord.manager.installers.root.RootInstaller
@ -57,7 +57,7 @@ class ManagerApplication : Application() {
// UI Models
modules(module {
factory { HomeModel(get<Application>(), get<AliucordGithubService>(), get<AliucordMavenService>(), get<ShiggyGithubService>(), get<RNATrackerService>(), get<kotlinx.serialization.json.Json>(), get<com.aliucord.manager.manager.PreferencesManager>()) }
factoryOf(::PluginsModel)
factoryOf(::AboutModel)
factoryOf(::PatchingScreenModel)
factory { SettingsModel(get<Application>(), get<PathManager>(), get<PreferencesManager>(), get<InstallerManager>()) }

View file

@ -11,28 +11,12 @@ import java.io.File
class PathManager(
private val context: Context,
) {
/**
* The Aliucord folder in which plugins/settings/themes are stored.
* Standard path: `~/Aliucord`
*/
val aliucordDir = Environment.getExternalStorageDirectory().resolve("Aliucord")
/**
* The Shiggy folder where shared Shiggy files are stored.
* Standard path: `~/Shiggy`
*/
val shiggyDir = Environment.getExternalStorageDirectory().resolve("Shiggy")
/**
* The directory in external storage in which plugins are stored by Aliucord.
*/
val pluginsDir = aliucordDir.resolve("plugins")
/**
* The settings file in which Aliucord's core uses.
*/
val coreSettingsFile = aliucordDir.resolve("settings/Aliucord.json")
/**
* Global keystore used for signing APKs.
*/

View file

@ -1,23 +0,0 @@
package com.aliucord.manager.network.models
import androidx.compose.runtime.Immutable
import com.aliucord.manager.util.serialization.ImmutableListSerializer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.serialization.Serializable
@Immutable
@Serializable
data class Contributor(
val username: String,
val avatarUrl: String,
val commits: Int,
@Serializable(with = ImmutableListSerializer::class)
val repositories: ImmutableList<Repository>,
) {
@Immutable
@Serializable
data class Repository(
val name: String,
val commits: Int,
)
}

View file

@ -0,0 +1,18 @@
package com.aliucord.manager.network.models
import kotlinx.serialization.Serializable
@Serializable
data class Developer(
val username: String,
val avatarUrl: String,
val commits: Int,
val repositories: List<Repository>,
val role: String? = null
) {
@Serializable
data class Repository(
val name: String,
val commits: Int
)
}

View file

@ -17,13 +17,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import coil3.compose.SubcomposeAsyncImage
import com.aliucord.manager.network.models.Contributor
import com.aliucord.manager.network.models.Developer
import com.valentinilk.shimmer.shimmer
import dev.shiggy.manager.R
@Composable
fun ContributorCommitsItem(
user: Contributor,
user: Developer,
modifier: Modifier = Modifier,
) {
val uriHandler = LocalUriHandler.current

View file

@ -1,20 +0,0 @@
package com.aliucord.manager.ui.previews.dialogs
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.aliucord.manager.ui.screens.plugins.components.dialogs.UninstallPluginDialog
import com.aliucord.manager.ui.theme.ManagerTheme
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun UninstallPluginDialogPreview() {
ManagerTheme {
UninstallPluginDialog(
pluginName = "FakeNitro",
onConfirm = {},
onDismiss = {},
)
}
}

View file

@ -1,52 +0,0 @@
package com.aliucord.manager.ui.previews.screens.about
import android.content.res.Configuration
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.*
import com.aliucord.manager.network.models.Contributor
import com.aliucord.manager.ui.screens.about.AboutScreenContent
import com.aliucord.manager.ui.screens.about.AboutScreenState
import com.aliucord.manager.ui.theme.ManagerTheme
import com.aliucord.manager.ui.util.emptyImmutableList
import com.aliucord.manager.util.serialization.ImmutableListSerializer
import kotlinx.collections.immutable.*
import kotlinx.serialization.json.Json
// This preview has scrollable content that cannot be properly viewed from an IDE preview
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun AboutScreenLoadedPreview(
@PreviewParameter(ContributorsProvider::class) contributors: ImmutableList<Contributor>,
) {
ManagerTheme {
AboutScreenContent(
state = remember { mutableStateOf(AboutScreenState.Loaded(contributors)) },
)
}
}
private class ContributorsProvider : PreviewParameterProvider<ImmutableList<Contributor>> {
private val realDataRaw =
"[{\"username\":\"maisymoe\",\"avatarUrl\":\"https://avatars.githubusercontent.com/u/61095277?v=4\",\"repositories\":]"
private val realData =
Json.decodeFromString(ImmutableListSerializer(Contributor.serializer()), realDataRaw)
override val values =
sequenceOf(
emptyImmutableList<Contributor>(),
persistentListOf(
Contributor(
username =
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
avatarUrl = "UNUSED",
commits = Int.MAX_VALUE,
repositories =
(realData[0].repositories + realData[0].repositories)
.toImmutableList(),
)
),
realData,
)
}

View file

@ -34,7 +34,6 @@ private fun HomeScreenLoadedPreview(
onUpdate = {},
onOpenApp = {},
onOpenAppInfo = {},
onOpenPlugins = {},
onUninstall = {},
)
}

View file

@ -1,29 +0,0 @@
package com.aliucord.manager.ui.previews.screens.plugins
import android.content.res.Configuration
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.Preview
import com.aliucord.manager.ui.screens.plugins.PluginsScreenContent
import com.aliucord.manager.ui.theme.ManagerTheme
import com.aliucord.manager.ui.util.emptyImmutableList
// This preview has interactable content that cannot be tested from an IDE preview
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun PluginsScreenFailedPreview() {
val filterState = remember { mutableStateOf("") }
ManagerTheme {
PluginsScreenContent(
searchText = filterState,
setSearchText = filterState::value::set,
isError = true,
plugins = emptyImmutableList(),
onPluginUninstall = {},
onPluginChangelog = {},
onPluginToggle = { name, enabled -> },
)
}
}

View file

@ -1,92 +0,0 @@
package com.aliucord.manager.ui.previews.screens.plugins
import android.content.res.Configuration
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.Preview
import com.aliucord.manager.ui.screens.plugins.PluginsScreenContent
import com.aliucord.manager.ui.screens.plugins.model.PluginItem
import com.aliucord.manager.ui.screens.plugins.model.PluginManifest
import com.aliucord.manager.ui.theme.ManagerTheme
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.util.UUID
// This preview has scrollable/interactable content that cannot be tested from an IDE preview
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun PluginsScreenLoadedPreview() {
val filterState = remember { mutableStateOf("test") }
ManagerTheme {
PluginsScreenContent(
searchText = filterState,
setSearchText = filterState::value::set,
isError = false,
plugins = plugins,
onPluginUninstall = {},
onPluginChangelog = {},
onPluginToggle = { name, enabled -> },
)
}
}
private val plugins: ImmutableList<PluginItem> = persistentListOf(
PluginItem(
path = UUID.randomUUID().toString(),
manifest = PluginManifest(
name = "CloseDMs",
authors = persistentListOf(
PluginManifest.Author(name = "Diamond", id = 0L),
),
description = "Shortcut to close DMs in the DM context menu.",
version = "1.0.0",
updateUrl = "",
changelog = "",
changelogMedia = null,
)
),
PluginItem(
path = UUID.randomUUID().toString(),
manifest = PluginManifest(
name = "ConfigurableStickerSizes",
authors = persistentListOf(
PluginManifest.Author(name = "rushii", id = 0L, hyperlink = false),
),
description = "Makes sticker sizes configurable.",
version = "1.1.5",
updateUrl = "",
changelog = "",
changelogMedia = null,
)
),
PluginItem(
path = UUID.randomUUID().toString(),
manifest = PluginManifest(
name = "AudioPlayer",
authors = persistentListOf(
PluginManifest.Author(name = "rushii", id = 0L, hyperlink = false),
),
description = "Makes audio files playable.",
version = "0.0.1",
updateUrl = "",
changelog = "",
changelogMedia = null,
)
),
PluginItem(
path = UUID.randomUUID().toString(),
manifest = PluginManifest(
name = "TypingIndicators",
authors = persistentListOf(
PluginManifest.Author(name = "rushii", id = 0L, hyperlink = false),
),
description = "Adds typing indicators to channels that people are currently typing in.",
version = "1.1.0",
updateUrl = "",
changelog = "",
changelogMedia = null,
)
),
)

View file

@ -1,29 +0,0 @@
package com.aliucord.manager.ui.previews.screens.plugins
import android.content.res.Configuration
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.Preview
import com.aliucord.manager.ui.screens.plugins.PluginsScreenContent
import com.aliucord.manager.ui.theme.ManagerTheme
import com.aliucord.manager.ui.util.emptyImmutableList
// This preview has interactable content that cannot be tested from an IDE preview
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun PluginsScreenNonePreview() {
val filterState = remember { mutableStateOf("") }
ManagerTheme {
PluginsScreenContent(
searchText = filterState,
setSearchText = filterState::value::set,
isError = false,
plugins = emptyImmutableList(),
onPluginUninstall = {},
onPluginChangelog = {},
onPluginToggle = { name, enabled -> },
)
}
}

View file

@ -2,56 +2,60 @@ package com.aliucord.manager.ui.screens.about
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.network.models.Contributor
import com.aliucord.manager.network.models.Developer
import com.aliucord.manager.network.services.HttpService
import com.aliucord.manager.network.utils.fold
import com.aliucord.manager.ui.util.toUnsafeImmutable
import com.aliucord.manager.util.launchIO
import io.ktor.client.request.url
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.collections.immutable.persistentListOf
class AboutModel(
private val http: HttpService,
) : StateScreenModel<AboutScreenState>(AboutScreenState.Loading) {
init {
fetchContributors()
fetchDevelopers()
}
fun fetchContributors() = screenModelScope.launchIO {
fun fetchDevelopers() = screenModelScope.launchIO {
mutableState.value = AboutScreenState.Loading
// Use a curated static list of GitHub usernames (profiles + static avatar URLs).
// This avoids pulling from a specific repo and lets us include people
// who are not contributors to any particular repository.
val staticContributors = persistentListOf(
Contributor(
username = "rushiiMachine",
avatarUrl = "https://github.com/rushiiMachine.png",
commits = 0,
repositories = persistentListOf<com.aliucord.manager.network.models.Contributor.Repository>()
),
Contributor(
username = "maisymoe",
avatarUrl = "https://github.com/maisymoe.png",
commits = 0,
repositories = persistentListOf<com.aliucord.manager.network.models.Contributor.Repository>()
),
Contributor(
val allDevelopers = persistentListOf(
Developer(
username = "kmmiio99o",
avatarUrl = "https://github.com/kmmiio99o.png",
commits = 0,
repositories = persistentListOf<com.aliucord.manager.network.models.Contributor.Repository>()
repositories = emptyList(),
role = "ShiggyCord - Creator"
),
Contributor(
Developer(
username = "C0C0B01",
avatarUrl = "https://github.com/C0C0B01.png",
commits = 0,
repositories = emptyList(),
role = "Kettu - Creator"
),
Developer(
username = "pylixonly",
avatarUrl = "https://github.com/pylixonly.png",
commits = 0,
repositories = persistentListOf<com.aliucord.manager.network.models.Contributor.Repository>()
repositories = emptyList(),
role = "Bunny - Creator"
),
Developer(
username = "maisymoe",
avatarUrl = "https://github.com/maisymoe.png",
commits = 0,
repositories = emptyList(),
role = "Creator - Vendetta"
),
Developer(
username = "rushiiMachine",
avatarUrl = "https://github.com/rushiiMachine.png",
commits = 0,
repositories = emptyList(),
role = "Manager - Creator"
)
)
mutableState.value = AboutScreenState.Loaded(staticContributors.toUnsafeImmutable())
mutableState.value = AboutScreenState.Loaded(allDevelopers.toUnsafeImmutable())
}
}

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -20,7 +21,7 @@ import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel
import com.aliucord.manager.ui.components.*
import com.aliucord.manager.ui.screens.about.components.LeadContributor
import com.aliucord.manager.ui.screens.about.components.ContributorListItem
import com.aliucord.manager.ui.util.paddings.*
import dev.shiggy.manager.R
import kotlinx.parcelize.IgnoredOnParcel
@ -41,107 +42,90 @@ class AboutScreen : Screen, Parcelable {
@Composable
fun AboutScreenContent(state: State<AboutScreenState>) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.navigation_about_manager)) },
navigationIcon = { BackButton() },
)
}
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.navigation_about_manager)) },
navigationIcon = { BackButton() },
)
}
) { paddingValues ->
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
horizontalAlignment = Alignment.CenterHorizontally,
contentPadding =
paddingValues
.exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top)
.add(PaddingValues(vertical = 16.dp)),
modifier =
Modifier.padding(paddingValues.exclude(PaddingValuesSides.Bottom))
.padding(horizontal = 14.dp),
verticalArrangement = Arrangement.spacedBy(2.dp),
horizontalAlignment = Alignment.CenterHorizontally,
contentPadding = paddingValues
.exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top)
.add(PaddingValues(vertical = 16.dp)),
modifier = Modifier
.padding(paddingValues.exclude(PaddingValuesSides.Bottom))
) {
item(key = "MANAGER_CREDIT_DISCLAIMER_BANNER") {
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier.padding(bottom = 24.dp)
.border(
width = 2.dp,
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.medium
)
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 14.dp)
) {
Text(
text =
stringResource(
R.string.about_manager_credit_disclaimer_notice
),
style = MaterialTheme.typography.labelMedium,
contentAlignment = Alignment.Center,
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth()
.border(
width = 1.dp,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
shape = MaterialTheme.shapes.large
)
.clip(MaterialTheme.shapes.large)
.background(MaterialTheme.colorScheme.surfaceContainerLow)
) {
Text(
text = stringResource(R.string.about_manager_credit_disclaimer_notice),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp)
)
}
}
item(key = "PROJECT_HEADER") {
Box(modifier = Modifier.padding(horizontal = 16.dp)) {
ProjectHeader()
}
}
item(key = "TEAM_SPACER") {
Spacer(modifier = Modifier.height(32.dp))
}
when (val currentState = state.value) {
AboutScreenState.Loading -> {
item(key = "LOADING") {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 48.dp)
) {
CircularProgressIndicator()
}
}
}
}
item(key = "PROJECT_HEADER") { ProjectHeader() }
item(key = "HEADER_DIVIDER") {
TextDivider(
text = stringResource(R.string.contributors_lead),
modifier = Modifier.padding(top = 18.dp, bottom = 20.dp),
)
}
item(key = "MAIN_CONTRIBUTORS") {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
LeadContributor("C0C0B01", stringResource(R.string.lead_role_kettu))
LeadContributor("kmmiio99o", stringResource(R.string.lead_role_shiggycord))
LeadContributor("pylixonly", stringResource(R.string.lead_role_bunny))
}
}
item(key = "CONTRIBUTORS_DIVIDER") {
TextDivider(
text = stringResource(R.string.contributors),
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
)
}
when (val state = state.value) {
AboutScreenState.Loading ->
item(key = "CONTRIBUTIONS_LOADING") {
Box(
contentAlignment = Alignment.Center,
content = { CircularProgressIndicator() },
modifier = Modifier.fillMaxWidth().padding(vertical = 38.dp),
)
}
AboutScreenState.Failure ->
item(key = "LOAD_FAILURE") {
LoadFailure(modifier = Modifier.fillMaxSize().padding(vertical = 38.dp))
}
is AboutScreenState.Loaded -> {
// Render the two requested contributors as LeadContributor entries
item(key = "SELECTED_CONTRIBUTORS") {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
AboutScreenState.Failure -> {
item(key = "LOAD_FAILURE") {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 48.dp)
) {
LeadContributor("maisymoe", "Creator - Vendetta")
LeadContributor("rushiiMachine", "Manager - Creator")
LoadFailure()
}
}
}
is AboutScreenState.Loaded -> {
itemsIndexed(
items = currentState.contributors,
key = { index, developer -> developer.username }
) { index, developer ->
ContributorListItem(
contributor = developer,
isLast = index == currentState.contributors.lastIndex
)
}
}
}
}
}

View file

@ -1,10 +1,10 @@
package com.aliucord.manager.ui.screens.about
import com.aliucord.manager.network.models.Contributor
import com.aliucord.manager.network.models.Developer
import kotlinx.collections.immutable.ImmutableList
sealed interface AboutScreenState {
data object Loading : AboutScreenState
data object Failure : AboutScreenState
data class Loaded(val contributors: ImmutableList<Contributor>) : AboutScreenState
data class Loaded(val contributors: ImmutableList<Developer>) : AboutScreenState
}

View file

@ -0,0 +1,96 @@
package com.aliucord.manager.ui.screens.about.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.SubcomposeAsyncImage
import com.aliucord.manager.network.models.Developer
import com.valentinilk.shimmer.shimmer
@Composable
fun ContributorListItem(
contributor: Developer,
isLast: Boolean = false,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
Column(modifier = modifier.fillMaxWidth()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { uriHandler.openUri("https://github.com/${contributor.username}") }
)
.padding(horizontal = 24.dp, vertical = 16.dp)
) {
SubcomposeAsyncImage(
model = contributor.avatarUrl,
contentDescription = contributor.username,
contentScale = ContentScale.Crop,
loading = {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.shimmer()
) {}
},
error = {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
) {}
},
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(20.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = contributor.username,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
contributor.role?.let { role ->
Text(
text = role,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
if (!isLast) {
Divider(
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.1f),
thickness = 1.dp,
modifier = Modifier.padding(start = 100.dp, end = 24.dp)
)
}
}
}

View file

@ -30,7 +30,6 @@ import com.aliucord.manager.ui.components.LoadFailure
import com.aliucord.manager.ui.components.ProjectHeader
import com.aliucord.manager.ui.screens.home.components.*
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsScreen
import com.aliucord.manager.ui.screens.plugins.PluginsScreen
import com.aliucord.manager.ui.util.paddings.PaddingValuesSides
import com.aliucord.manager.ui.util.paddings.exclude
import com.aliucord.manager.util.*
@ -78,7 +77,6 @@ class HomeScreen : Screen, Parcelable {
},
onOpenApp = model::openApp,
onOpenAppInfo = model::openAppInfo,
onOpenPlugins = { navigator.push(PluginsScreen()) }, // TODO: install-specific plugins
onUninstall = model::uninstallApp,
)
@ -129,7 +127,6 @@ fun HomeScreenLoadedContent(
onUpdate: (packageName: String) -> Unit,
onOpenApp: (packageName: String) -> Unit,
onOpenAppInfo: (packageName: String) -> Unit,
onOpenPlugins: (packageName: String) -> Unit,
onUninstall: (packageName: String) -> Unit,
) {
LazyColumn(
@ -163,7 +160,6 @@ fun HomeScreenLoadedContent(
onUpdate = { onUpdate(item.packageName) },
onOpenApp = { onOpenApp(item.packageName) },
onOpenInfo = { onOpenAppInfo(item.packageName) },
onOpenPlugins = { onOpenPlugins(item.packageName) },
onUninstall = { onUninstall(item.packageName) },
modifier = Modifier.fillMaxWidth()
)

View file

@ -26,7 +26,6 @@ fun InstalledItemCard(
onUpdate: () -> Unit,
onOpenApp: () -> Unit,
onOpenInfo: () -> Unit,
onOpenPlugins: () -> Unit,
onUninstall: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -89,13 +88,6 @@ fun InstalledItemCard(
horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.clip(MaterialTheme.shapes.large),
) {
// // Plugins cannot be managed externally yet in Shiggy
// SegmentedButton(
// icon = painterResource(R.drawable.ic_extension),
// text = stringResource(R.string.plugins_title),
// onClick = onOpenPlugins,
// )
// If the up-to-date status cannot be determined, assume it is up-to-date
if (data.isUpToDate != false)
SegmentedButton(
@ -126,7 +118,7 @@ fun InstalledItemCard(
)
SegmentedButton(
icon = painterResource(R.drawable.ic_delete_forever),
text = stringResource(R.string.action_uninstall), // Assuming R.string.action_uninstall exists or will be added
text = stringResource(R.string.action_uninstall),
iconColor = MaterialTheme.colorScheme.error,
textColor = MaterialTheme.colorScheme.error,
onClick = onUninstall,

View file

@ -1,203 +0,0 @@
package com.aliucord.manager.ui.screens.plugins
import android.app.Application
import android.util.Log
import androidx.compose.runtime.*
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.manager.PathManager
import com.aliucord.manager.ui.screens.plugins.model.PluginItem
import com.aliucord.manager.ui.screens.plugins.model.PluginManifest
import com.aliucord.manager.ui.util.emptyImmutableList
import com.aliucord.manager.ui.util.toUnsafeImmutable
import com.aliucord.manager.util.*
import com.github.diamondminer88.zip.ZipReader
import dev.shiggy.manager.BuildConfig
import dev.shiggy.manager.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.*
import java.io.File
import kotlin.time.Duration
class PluginsModel(
private val context: Application,
private val paths: PathManager,
private val json: Json,
) : ScreenModel {
private val plugins = MutableStateFlow<ImmutableList<PluginItem>>(emptyImmutableList())
private val aliucordJsonMutex = Mutex()
var error by mutableStateOf(false)
private set
var showChangelogDialog by mutableStateOf<PluginItem?>(null)
private set
var showUninstallDialog by mutableStateOf<PluginItem?>(null)
private set
val searchText: StateFlow<String>
private field = MutableStateFlow("")
val filteredPlugins: StateFlow<ImmutableList<PluginItem>> = searchText
.combine(plugins) { searchText, plugins ->
if (searchText.isBlank()) {
plugins
} else {
plugins.filter { plugin ->
plugin.manifest.name.contains(searchText, ignoreCase = true)
|| plugin.manifest.description.contains(searchText, ignoreCase = true)
|| plugin.manifest.authors.any { (name) -> name.contains(searchText, ignoreCase = true) }
}.toUnsafeImmutable()
}
}.stateIn(
scope = screenModelScope + Dispatchers.Default,
started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO),
initialValue = plugins.value,
)
fun setSearchText(search: String) {
searchText.value = search
}
fun showChangelogDialog(plugin: PluginItem) {
showChangelogDialog = plugin
}
fun hideChangelogDialog() {
showChangelogDialog = null
}
fun showUninstallDialog(plugin: PluginItem) {
showUninstallDialog = plugin
}
fun hideUninstallDialog() {
showUninstallDialog = null
}
fun uninstallPlugin(plugin: PluginItem) = screenModelScope.launchIO {
if (!plugins.value.any { it.path == plugin.path }) {
mainThread { hideUninstallDialog() }
return@launchIO
}
val deleteSuccess = try {
File(plugin.path).delete()
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to delete plugin", t)
false
}
if (!deleteSuccess) {
mainThread {
hideUninstallDialog()
context.showToast(R.string.plugins_error)
}
return@launchIO
}
plugins.update { (it - plugin).toUnsafeImmutable() }
mainThread { hideUninstallDialog() }
}
fun setPluginEnabled(pluginName: String, enabled: Boolean) = screenModelScope.launchIO {
aliucordJsonMutex.withLock {
@OptIn(ExperimentalSerializationApi::class)
try {
val settings = paths.coreSettingsFile.inputStream()
.use { Json.decodeFromStream<Map<String, JsonElement>>(it) }
.toMutableMap()
settings["AC_PM_$pluginName"] = JsonPrimitive(enabled)
paths.coreSettingsFile.outputStream()
.use { json.encodeToStream(settings, it) }
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to write Aliucord.json", t)
mainThread { error = true }
}
}
mainThread {
plugins.value.forEach {
if (it.manifest.name == pluginName)
it.enabled = enabled
}
}
}
// Called by screen to load initial data
fun refreshData() = screenModelScope.launchIO {
loadPlugins()
loadPluginsEnabled()
}
private suspend fun loadPluginsEnabled() {
if (!paths.coreSettingsFile.exists()) return
aliucordJsonMutex.withLock {
@OptIn(ExperimentalSerializationApi::class)
val pluginToggles = try {
paths.coreSettingsFile.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", t)
mainThread { error = true }
emptyMap()
}
mainThread {
plugins.value.forEach {
if (!pluginToggles.getOrDefault(it.manifest.name, true))
it.enabled = false
}
}
}
}
private suspend fun loadPlugins() {
try {
if (!paths.pluginsDir.exists() && !paths.pluginsDir.mkdirs())
throw IllegalStateException("Failed to create plugins directory")
val pluginFiles = paths.pluginsDir.listFiles { file -> file.extension == "zip" }
?: throw IllegalStateException("Failed to read plugins directory")
val pluginItems = pluginFiles.map {
PluginItem(
manifest = loadPluginManifest(it),
path = it.absolutePath,
)
}
plugins.value = pluginItems.toUnsafeImmutable()
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to load plugins", t)
mainThread { error = true }
}
}
private fun loadPluginManifest(pluginFile: File): PluginManifest {
return ZipReader(pluginFile).use {
val manifest = it.openEntry("manifest.json")
?: throw Exception("Plugin ${pluginFile.name} has no manifest")
try {
@OptIn(ExperimentalSerializationApi::class)
json.decodeFromStream(manifest.read().inputStream())
} catch (t: Throwable) {
throw Exception("Failed to parse plugin manifest for ${pluginFile.name}", t)
}
}
}
}

View file

@ -1,138 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screens.plugins
import android.os.Parcelable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleResumeEffect
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel
import com.aliucord.manager.ui.components.BackButton
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 dev.shiggy.manager.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
class PluginsScreen : Screen, Parcelable {
@IgnoredOnParcel
override val key = "Plugins"
@Composable
override fun Content() {
val model = koinScreenModel<PluginsModel>()
// Refresh plugins list on activity resume or when this initially opens
LifecycleResumeEffect(Unit) {
model.refreshData()
onPauseOrDispose {}
}
model.showUninstallDialog?.let { plugin ->
UninstallPluginDialog(
pluginName = plugin.manifest.name,
onConfirm = { model.uninstallPlugin(plugin) },
onDismiss = model::hideUninstallDialog
)
}
model.showChangelogDialog?.let { plugin ->
Changelog(
plugin = plugin,
onDismiss = model::hideChangelogDialog
)
}
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,
)
}
}
@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())
}
plugins.isNotEmpty() -> {
items(
items = plugins,
contentType = { "PLUGIN" },
key = { it.path },
) { plugin ->
PluginCard(
plugin = plugin,
onClickDelete = { onPluginUninstall(plugin) },
onClickShowChangelog = { onPluginChangelog(plugin) },
onSetEnabled = { onPluginToggle(plugin.manifest.name, it) },
)
}
}
else -> item(key = "PLUGINS_NONE") {
PluginsNone(modifier = Modifier.fillParentMaxSize())
}
}
}
}
}
}

View file

@ -1,159 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screens.plugins.components
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.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import coil3.compose.AsyncImage
import com.aliucord.manager.ui.screens.plugins.model.PluginItem
import dev.shiggy.manager.R
private val hyperLinkPattern = Regex("\\[(.+?)]\\((.+?\\))")
@Suppress("RegExpRedundantEscape") // It is very much not redundant and causes a crash lol
private val headerStylePattern = Regex("\\{(improved|added|fixed)( marginTop)?\\}")
@Composable
private fun AnnotatedString.Builder.MarkdownHyperlink(content: String) {
var idx = 0
with(hyperLinkPattern.toPattern().matcher(content)) {
while (find()) {
val start = start()
val end = end()
val title = group(1)!!
val url = group(2)!!
append(content.substring(idx, start))
// @formatter:off
pushLink(LinkAnnotation.Url(
url,
TextLinkStyles(SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline
))
))
append(title)
pop()
// @formatter:on
idx = end
}
}
if (idx < content.length) append(content.substring(idx))
}
@Composable
fun Changelog(
plugin: PluginItem,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
painter = painterResource(R.drawable.ic_history),
contentDescription = stringResource(R.string.plugins_view_changelog, plugin.manifest.name)
)
},
title = { Text(plugin.manifest.name) },
text = {
Column {
plugin.manifest.changelogMedia?.let { mediaUrl ->
AsyncImage(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 90.dp)
.clip(RoundedCornerShape(14.dp)),
model = mediaUrl,
contentDescription = stringResource(R.string.plugins_changelog_media)
)
}
LazyColumn {
items(plugin.manifest.changelog!!.lines()) {
var line = it.trim()
if (line.isNotEmpty()) {
when (line[0]) {
'#' -> {
do {
line = line.substring(1)
} while (line.startsWith("#"))
Text(
text = line.trimStart(),
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
)
}
'*' -> {
Text(
modifier = Modifier.padding(bottom = 2.dp),
text = buildAnnotatedString {
withStyle(
SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
) {
append("● ")
}
MarkdownHyperlink(line.substring(1))
}
)
}
else -> {
when {
line.endsWith("marginTop}") -> {
val color = MaterialTheme.colorScheme.onSurface
Text(
text = line,
fontWeight = FontWeight.Bold,
color = color,
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
)
}
line.all { c -> c == '=' } -> {} // Discord ignores =======
else -> {
Text(buildAnnotatedString {
MarkdownHyperlink(line)
})
}
}
}
}
}
}
}
}
},
confirmButton = {
Button(onClick = onDismiss) {
Text(stringResource(R.string.action_close))
}
}
)
}

View file

@ -1,153 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screens.plugins.components
import androidx.compose.foundation.clickable
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.draw.alpha
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import com.aliucord.manager.ui.screens.plugins.model.PluginItem
import dev.shiggy.manager.R
@Composable
fun PluginCard(
plugin: PluginItem,
onClickDelete: () -> Unit,
onClickShowChangelog: () -> Unit,
onSetEnabled: (Boolean) -> Unit,
) {
val uriHandler = LocalUriHandler.current
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),
) {
Column {
// Name
Text(
text = buildAnnotatedString {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(plugin.manifest.name)
}
append(" v")
append(plugin.manifest.version)
}
)
// Authors
val authors = buildAnnotatedString {
withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) {
for ((idx, author) in plugin.manifest.authors.withIndex()) {
if (idx > 0) append(", ")
if (author.hyperlink) pushLink(
LinkAnnotation.Url(
url = author.discordUrl,
styles = TextLinkStyles(
SpanStyle(
textDecoration = TextDecoration.Underline,
)
),
)
)
append(author.name)
if (author.hyperlink) pop()
}
}
}
Text(
text = authors,
style = MaterialTheme.typography.labelLarge,
)
}
Spacer(Modifier.weight(1f, true))
// Toggle Switch
Switch(
checked = plugin.enabled,
onCheckedChange = { onSetEnabled(!plugin.enabled) }
)
}
HorizontalDivider(
modifier = Modifier
.alpha(0.3f)
.padding(horizontal = 16.dp)
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// Description
Text(
text = plugin.manifest.description,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.heightIn(max = 150.dp, min = 40.dp)
.padding(bottom = 20.dp),
)
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
IconButton(
onClick = { uriHandler.openUri(plugin.manifest.repositoryUrl) },
modifier = Modifier.size(25.dp),
) {
Icon(
modifier = Modifier.fillMaxSize(),
painter = painterResource(R.drawable.ic_account_github_white_24dp),
contentDescription = stringResource(R.string.github)
)
}
if (plugin.manifest.changelog != null) {
IconButton(
onClick = onClickShowChangelog,
modifier = Modifier.size(25.dp),
) {
Icon(
painter = painterResource(R.drawable.ic_history),
contentDescription = stringResource(R.string.plugins_view_changelog),
modifier = Modifier.fillMaxSize(),
)
}
}
Spacer(Modifier.weight(1f, true))
IconButton(
onClick = onClickDelete,
modifier = Modifier.size(25.dp),
) {
Icon(
modifier = Modifier.fillMaxSize(),
painter = painterResource(R.drawable.ic_delete_forever),
contentDescription = stringResource(R.string.action_uninstall),
tint = MaterialTheme.colorScheme.error
)
}
}
}
}
}

View file

@ -1,46 +0,0 @@
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.ui.components.ResetToDefaultButton
import dev.shiggy.manager.R
@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

@ -1,35 +0,0 @@
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 dev.shiggy.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

@ -1,31 +0,0 @@
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 dev.shiggy.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

@ -1,56 +0,0 @@
package com.aliucord.manager.ui.screens.plugins.components.dialogs
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.text.style.TextAlign
import androidx.compose.ui.unit.dp
import dev.shiggy.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,
modifier = Modifier.size(32.dp),
)
},
title = {
Text(stringResource(R.string.plugins_delete_plugin, pluginName))
},
text = {
Text(
text = stringResource(R.string.plugins_delete_plugin_body, pluginName),
textAlign = TextAlign.Center,
)
},
confirmButton = {
Button(
onClick = onConfirm,
) {
Text(stringResource(R.string.action_confirm))
}
},
dismissButton = {
Button(
onClick = onDismiss,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Text(stringResource(R.string.action_cancel))
}
}
)
}

View file

@ -1,12 +0,0 @@
package com.aliucord.manager.ui.screens.plugins.model
import androidx.compose.runtime.*
@Stable
data class PluginItem(
val manifest: PluginManifest,
val path: String,
) {
// Plugins are enabled by default unless disabled
var enabled by mutableStateOf(true)
}

View file

@ -1,35 +0,0 @@
package com.aliucord.manager.ui.screens.plugins.model
import androidx.compose.runtime.Immutable
import com.aliucord.manager.util.serialization.ImmutableListSerializer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.serialization.Serializable
@Immutable
@Serializable
data class PluginManifest(
val name: String,
@Serializable(with = ImmutableListSerializer::class)
val authors: ImmutableList<Author>,
val description: String,
val version: String,
val updateUrl: String,
val changelog: String?,
val changelogMedia: String?,
) {
val repositoryUrl: String
get() = updateUrl
.replace("raw.githubusercontent.com", "github.com")
.replaceFirst("/builds.*".toRegex(), "")
@Immutable
@Serializable
data class Author(
val name: String,
val id: Long,
val hyperlink: Boolean = true,
) {
val discordUrl: String
get() = "https://discord.com/users/$id"
}
}

View file

@ -159,8 +159,8 @@ class SettingsScreen : Screen, Parcelable {
painter = painterResource(
when (preferences.installer) {
InstallerSetting.PM -> R.drawable.ic_android
InstallerSetting.ROOT -> R.drawable.ic_code
InstallerSetting.Shizuku -> R.drawable.ic_tools
InstallerSetting.ROOT -> R.drawable.ic_hashtag
InstallerSetting.Shizuku -> R.drawable.ic_shizuku
}
),
contentDescription = null,

View file

@ -56,8 +56,8 @@ fun InstallerDialog(
painter = painterResource(
when (installer) {
InstallerSetting.PM -> R.drawable.ic_android
InstallerSetting.ROOT -> R.drawable.ic_code
InstallerSetting.Shizuku -> R.drawable.ic_tools
InstallerSetting.ROOT -> R.drawable.ic_hashtag
InstallerSetting.Shizuku -> R.drawable.ic_shizuku
}
),
contentDescription = null,

View file

@ -0,0 +1,12 @@
<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="#aaaab1"
android:pathData="m360,640 l-33,131q-3,13 -13,21t-24,8q-19,0 -31,-15t-7,-33l28,-112L171,640q-20,0 -32,-15.5t-7,-34.5q3,-14 14,-22t25,-8h129l40,-160L231,400q-20,0 -32,-15.5t-7,-34.5q3,-14 14,-22t25,-8h129l33,-131q3,-13 13,-21t24,-8q19,0 31,15t7,33l-28,112h160l33,-131q3,-13 13,-21t24,-8q19,0 31,15t7,33l-28,112h109q20,0 32,15.5t7,34.5q-3,14 -14,22t-25,8L660,400l-40,160h109q20,0 32,15.5t7,34.5q-3,14 -14,22t-25,8L600,640l-33,131q-3,13 -13,21t-24,8q-19,0 -31,-15t-7,-33l28,-112L360,640ZM380,560h160l40,-160L420,400l-40,160Z"
/>
</vector>

View file

@ -0,0 +1,17 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
>
<path
android:fillAlpha="0.45"
android:fillColor="#FF000000"
android:pathData="M85.8,15.3H59.3L46,38.2l13.2,22.9h2.7c-0.9,-0.6 -1,-1.8 -0.4,-2.7c0.6,-0.9 1.8,-1 2.7,-0.4c0.9,0.6 1,1.8 0.4,2.7c-0.1,0.1 -0.3,0.3 -0.4,0.4h21.5L99,38.2L85.8,15.3z"
/>
<path
android:fillColor="#FF000000"
android:pathData="M85.5,42.7c-1.7,-0.5 -3.4,-1 -5.2,-1.3c-3.9,-0.5 -7.9,-0.3 -11.7,0.6c-3.4,-3 -7.4,-5.2 -11.7,-6.5c-2.1,-0.6 -4.3,-1 -6.5,-1.3c-2.7,-5.6 -7,-10.4 -12.3,-13.6c-4.1,3.6 -7.1,8.3 -8.4,13.6c-5.6,2.1 -10.5,5.7 -14.3,10.4c-2.3,2.9 -6.4,8 -6.5,14.3c-0.3,13 16.6,22.3 23.3,25.9C40,89 48.3,91.6 57,92.7c4,0.4 8.2,-0.5 11.7,-2.6c5.7,-3.4 7.6,-8.9 9.1,-13c1.9,-5.8 2.5,-12.1 1.3,-18.2c2.2,-3.2 4,-6.6 5.2,-10.4C84.7,46.6 85.1,44.7 85.5,42.7zM39.1,50.3c-1,0 -1.9,-0.9 -1.9,-1.9s0.9,-1.9 1.9,-1.9s1.9,0.9 1.9,1.9S40.3,50.3 39.1,50.3L39.1,50.3zM63.1,61.5c-1,0 -1.9,-0.9 -1.9,-1.9c0,-1 0.9,-1.9 1.9,-1.9c1,0 1.9,0.9 1.9,1.9C65,60.6 64.2,61.5 63.1,61.5z"
/>
</vector>

View file

@ -73,28 +73,19 @@
<string name="theme_dark">Dark</string>
<string name="theme_light">Light</string>
<string name="plugins_title">Plugins</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 the %s plugin?</string>
<string name="plugins_none_installed">No plugins installed!</string>
<string name="plugins_changelog_media">Changelog Media</string>
<string name="plugins_error">An error occurred while processing plugins!</string>
<string name="navigation_back">Back</string>
<string name="navigation_about">About</string>
<string name="navigation_about_manager">About Manager</string>
<string name="navigation_logs">Open logs</string>
<string name="navigation_settings">Settings</string>
<string name="contributors_lead">Lead Developers</string>
<string name="contributors_contributions">%d contributions</string>
<string name="contributors">Contributors</string>
<string name="lead_role_kettu">Kettu - Creator</string>
<string name="lead_role_shiggycord">ShiggyCord - Creator</string>
<string name="lead_role_bunny">Bunny - Creator</string>
<string name="role_creator_vendetta">Creator of Vendetta</string>
<string name="role_original_manager_creator">Original Manager Creator</string>
<string name="contributors">Other Developers</string>
<string name="contributors_contributions">%s contributions</string>
<string name="network_load_fail">Failed to load</string>
<string name="network_warning_title">Metered network</string>