From 137a739705a14fac4dcc74a484f3723885fead3f Mon Sep 17 00:00:00 2001 From: Vendicated Date: Wed, 10 Nov 2021 21:26:12 +0100 Subject: [PATCH] Add plugins page --- app/build.gradle.kts | 14 +- app/src/main/AndroidManifest.xml | 1 + .../com/aliucord/manager/ui/MainActivity.kt | 10 ++ .../java/com/aliucord/manager/ui/Screen.kt | 10 +- .../manager/ui/components/Changelog.kt | 99 ++++++++++ .../ui/components/ExposedDropdownMenu.kt | 5 +- .../aliucord/manager/ui/screens/HomeScreen.kt | 39 +++- .../manager/ui/screens/PluginsScreen.kt | 169 ++++++++++++++++++ .../manager/ui/screens/SettingsScreen.kt | 4 +- .../com/aliucord/manager/ui/theme/Color.kt | 1 + .../com/aliucord/manager/ui/theme/Theme.kt | 27 ++- .../com/aliucord/manager/utils/Constants.kt | 11 ++ .../java/com/aliucord/manager/utils/Github.kt | 9 +- .../java/com/aliucord/manager/utils/Plugin.kt | 59 ++++++ .../res/drawable/ic_history_white_24dp.xml | 3 + app/src/main/res/drawable/ic_plugin_24dp.xml | 9 + app/src/main/res/layout/plugin.xml | 25 +++ app/src/main/res/values/strings.xml | 9 + gradle.properties | 20 +-- 19 files changed, 490 insertions(+), 34 deletions(-) create mode 100644 app/src/main/java/com/aliucord/manager/ui/components/Changelog.kt create mode 100644 app/src/main/java/com/aliucord/manager/ui/screens/PluginsScreen.kt create mode 100644 app/src/main/java/com/aliucord/manager/utils/Constants.kt create mode 100644 app/src/main/java/com/aliucord/manager/utils/Plugin.kt create mode 100644 app/src/main/res/drawable/ic_history_white_24dp.xml create mode 100644 app/src/main/res/drawable/ic_plugin_24dp.xml create mode 100644 app/src/main/res/layout/plugin.xml diff --git a/app/build.gradle.kts b/app/build.gradle.kts index af23557..b25b0cf 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -12,12 +12,12 @@ plugins { } android { - compileSdk = 30 + compileSdk = 31 defaultConfig { applicationId = "com.aliucord.manager" minSdk = 24 - targetSdk = 30 + targetSdk = 29 versionCode = 1 versionName = "0.0.1" @@ -51,17 +51,17 @@ android { dependencies { // core dependencies - implementation("androidx.appcompat:appcompat:1.3.0") - implementation("androidx.core:core-ktx:1.6.0") + implementation("androidx.appcompat:appcompat:1.3.1") + implementation("androidx.core:core-ktx:1.7.0") implementation("com.google.android.material:material:1.4.0") // compose dependencies - implementation("androidx.activity:activity-compose:1.3.0-rc02") + implementation("androidx.activity:activity-compose:1.4.0") implementation("androidx.compose.ui:ui:${composeVersion}") implementation("androidx.compose.material:material:${composeVersion}") implementation("androidx.compose.ui:ui-tooling:${composeVersion}") - implementation("androidx.constraintlayout:constraintlayout-compose:1.0.0-alpha08") - implementation("androidx.navigation:navigation-compose:2.4.0-alpha04") + implementation("androidx.constraintlayout:constraintlayout-compose:1.0.0-rc01") + implementation("androidx.navigation:navigation-compose:2.4.0-beta02") // accompanist dependencies implementation("com.google.accompanist:accompanist-systemuicontroller:${accompanistVersion}") diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f73aa81..1bb1282 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -10,6 +10,7 @@ ) { + var idx = 0 + with (hyperLinkPattern.matcher(content)) { + while (find()) { + val start = start() + val end = end() + val title = group(1)!! + val url = group(2)!! + + append(content.substring(idx, start)) + withStyle(style = SpanStyle(color = MaterialTheme.colors.primary, textDecoration = TextDecoration.Underline)) { + append(title) + } + addStringAnnotation(title, url, start, end) + idx = end + 1 + outTags.add(title) + } + } + if (idx < content.length) append(content.substring(idx)) +} + +const val bulletPoint = '•' +@Composable +fun Changelog(changelog: String) { + val scrollState = rememberScrollState() + Column(modifier = Modifier + .fillMaxHeight(0.7f) + .verticalScroll(scrollState) + ) { + changelog.lines().forEach { + var line = it.trim() + if (line.isNotEmpty()) { + when (line[0]) { + '#' -> { + do { + line = line.substring(1); + } while (line.startsWith("#")) + Text(line.trimStart(), style = MaterialTheme.typography.h6) + } + '*' -> { + val tags = ArrayList() + LinkText( + buildAnnotatedString { + withStyle(style = SpanStyle(color = MaterialTheme.colors.primary)) { + append(bulletPoint) + } + hyperlink(line.substring(1), tags) + } + ) + } + else -> { + val tags = ArrayList() + LinkText( + buildAnnotatedString { + hyperlink(line, tags) + } + ) + } + } + } + } + } +} + +@Composable +private fun LinkText(annotatedString: AnnotatedString) { + val urlHandler = LocalUriHandler.current + ClickableText( + text = annotatedString, + style = TextStyle(color = LocalContentColor.current), + onClick = { offset -> + annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let { + urlHandler.openUri(it.item) + } + } + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/components/ExposedDropdownMenu.kt b/app/src/main/java/com/aliucord/manager/ui/components/ExposedDropdownMenu.kt index 11da15e..71065b5 100644 --- a/app/src/main/java/com/aliucord/manager/ui/components/ExposedDropdownMenu.kt +++ b/app/src/main/java/com/aliucord/manager/ui/components/ExposedDropdownMenu.kt @@ -53,7 +53,10 @@ fun ExposedDropdownMenu( ) { items.forEachIndexed { i, item -> DropdownMenuItem( - onClick = { selectItem(i) } + onClick = { + expanded = false + selectItem(i) + } ) { Text(item) } diff --git a/app/src/main/java/com/aliucord/manager/ui/screens/HomeScreen.kt b/app/src/main/java/com/aliucord/manager/ui/screens/HomeScreen.kt index ba41008..010283e 100644 --- a/app/src/main/java/com/aliucord/manager/ui/screens/HomeScreen.kt +++ b/app/src/main/java/com/aliucord/manager/ui/screens/HomeScreen.kt @@ -5,15 +5,14 @@ package com.aliucord.manager.ui.screens +import android.util.Log import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.padding import androidx.compose.material.Card import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ListItem import androidx.compose.material.Text -import androidx.compose.runtime.Composable -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Modifier import androidx.compose.ui.text.SpanStyle import androidx.compose.ui.text.buildAnnotatedString @@ -21,21 +20,47 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp +import com.aliucord.manager.BuildConfig import com.aliucord.manager.ui.components.CommitsList +import com.aliucord.manager.utils.Github +import com.aliucord.manager.utils.Versions +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch @OptIn(ExperimentalMaterialApi::class) @Composable @Preview fun HomeScreen() { + val coroutineScope = rememberCoroutineScope { Dispatchers.IO } val selectedCommit = remember { mutableStateOf(null) } + val versions = remember { mutableStateOf(null) } + + if (versions.value == null) { + LaunchedEffect(null) { + coroutineScope.launch { + try { + versions.value = Github.versions + } catch (th: Throwable) { + Log.e(BuildConfig.TAG, "Failed to get versions") + } + } + } + } + Column(modifier = Modifier.padding(8.dp)) { Card(modifier = Modifier.padding(bottom = 8.dp)) { ListItem( text = { Text("Aliucord") }, - secondaryText = { Text(buildAnnotatedString { - append("Supported version: ") - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { append(selectedCommit.value ?: "blah") } - }) }, + secondaryText = { + Text(buildAnnotatedString { + append("Supported version: ") + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append( + versions.value?.versionName ?: "?" + ) + } + }) + }, modifier = Modifier.padding(vertical = 4.dp) ) } diff --git a/app/src/main/java/com/aliucord/manager/ui/screens/PluginsScreen.kt b/app/src/main/java/com/aliucord/manager/ui/screens/PluginsScreen.kt new file mode 100644 index 0000000..bff4da9 --- /dev/null +++ b/app/src/main/java/com/aliucord/manager/ui/screens/PluginsScreen.kt @@ -0,0 +1,169 @@ +package com.aliucord.manager.ui.screens + +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll +import androidx.compose.material.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete +import androidx.compose.runtime.* +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.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import com.aliucord.manager.R +import com.aliucord.manager.ui.components.Changelog +import com.aliucord.manager.utils.Plugin + +@OptIn(ExperimentalMaterialApi::class) +@Composable +@Preview +fun PluginsScreen() { + Column( + modifier = Modifier + .fillMaxSize() + .padding(12.dp) + ) { + var plugins by remember { mutableStateOf(Plugin.loadAll()) } + var search by remember { mutableStateOf("") } + + TextField( + value = search, + placeholder = { Text(stringResource(R.string.search)) }, + onValueChange = { search = it }, + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) + ) + + LazyColumn { + items(plugins.filter { + it.manifest.run { + name.contains(search, true) + || description.contains(search, true) + || authors.any { a -> a.name.contains(search, true) } + } + }) { p -> + // TODO - make this get pluginEnabled from Aliucord preferences somehow + val isChecked = remember { mutableStateOf(true) } + var showDeleteDialog by remember { mutableStateOf(false) } + var showChangelogDialog by remember { mutableStateOf(false) } + + if (showDeleteDialog) { + AlertDialog( + onDismissRequest = { showDeleteDialog = false }, + title = { Text(stringResource(R.string.delete_plugin, p.manifest.name)) }, + text = { Text(stringResource(R.string.delete_plugin_body)) }, + confirmButton = { + Button( + onClick = { + showDeleteDialog = false + p.file.delete() + plugins = plugins.filter { it != p } + }, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.error, contentColor = MaterialTheme.colors.onError) + ) { + Text(stringResource(android.R.string.ok).uppercase()) + } + }, + dismissButton = { + Button( + onClick = { + showDeleteDialog = false + }, + colors = ButtonDefaults.buttonColors(backgroundColor = MaterialTheme.colors.secondary) + ) { + Text(stringResource(android.R.string.cancel).uppercase()) + } + } + ) + } + + if (showChangelogDialog) { + AlertDialog( + onDismissRequest = { showChangelogDialog = false }, + title = { Text(stringResource(R.string.changelog, p.manifest.name), style = MaterialTheme.typography.h5) }, + text = { + Changelog(p.manifest.changelog!!) + }, + confirmButton = {}, + dismissButton = { + Button( + onClick = { + showChangelogDialog = false + }, + ) { + Text(stringResource(android.R.string.cancel).uppercase()) + } + } + ) + } + + Card( + modifier = Modifier + .fillMaxWidth() + .padding(16.dp), + elevation = 1.dp + ) { + Column { + ListItem( + text = { + Text( + stringResource( + R.string.plugin_title, + p.manifest.name, + p.manifest.version, + p.manifest.authors.joinToString( + " ", + transform = { it.name }) + ), + ) + }, + trailing = { + Switch( + checked = isChecked.value, + onCheckedChange = { isChecked.value = it }, + ) + }, + ) + Divider(color = MaterialTheme.colors.primary) + Text( + p.manifest.description, + modifier = Modifier + .heightIn(min = 50.dp) + .padding(start = 16.dp, top = 16.dp, end = 16.dp, bottom = 0.dp) + ) + + Row(modifier = Modifier.align(Alignment.End)) { + if (p.manifest.changelog != null) { + IconButton(onClick = { showChangelogDialog = true }) { + Icon( + painter = painterResource(R.drawable.ic_history_white_24dp), + contentDescription = stringResource( + R.string.view_plugin_changelog, + p.manifest.name + ) + ) + } + } + IconButton(onClick = { showDeleteDialog = true }) { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource( + R.string.delete_plugin, + p.manifest.name + ), + tint = MaterialTheme.colors.error + ) + } + } + } + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/screens/SettingsScreen.kt b/app/src/main/java/com/aliucord/manager/ui/screens/SettingsScreen.kt index 8e19c47..0943e36 100644 --- a/app/src/main/java/com/aliucord/manager/ui/screens/SettingsScreen.kt +++ b/app/src/main/java/com/aliucord/manager/ui/screens/SettingsScreen.kt @@ -11,6 +11,8 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.material.* import androidx.compose.runtime.Composable +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import androidx.compose.ui.res.stringResource @@ -23,7 +25,7 @@ import com.aliucord.manager.preferences.themePreference import com.aliucord.manager.preferences.useDexFromStoragePreference import com.aliucord.manager.ui.components.ExposedDropdownMenu -private val themes = arrayOf("System", "Light", "Dark") +private val themes = arrayOf("System", "Light", "Dark", "Black") @OptIn(ExperimentalMaterialApi::class) @Composable diff --git a/app/src/main/java/com/aliucord/manager/ui/theme/Color.kt b/app/src/main/java/com/aliucord/manager/ui/theme/Color.kt index ef7d96c..d8e4c22 100644 --- a/app/src/main/java/com/aliucord/manager/ui/theme/Color.kt +++ b/app/src/main/java/com/aliucord/manager/ui/theme/Color.kt @@ -12,3 +12,4 @@ val darkBackground = Color(0xff2e2e2e) val primaryColor = Color(0xff00c853) val primaryColorLight = Color(0xff5efc82) val primaryColorDark = Color(0xff009624) +val errorColor = Color(0xffed4245) \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/theme/Theme.kt b/app/src/main/java/com/aliucord/manager/ui/theme/Theme.kt index 805a8e7..58d1867 100644 --- a/app/src/main/java/com/aliucord/manager/ui/theme/Theme.kt +++ b/app/src/main/java/com/aliucord/manager/ui/theme/Theme.kt @@ -13,20 +13,33 @@ import androidx.compose.runtime.Composable import androidx.compose.ui.graphics.Color import com.aliucord.manager.preferences.themePreference +private val LightColorPalette = lightColors( + primary = primaryColor, + primaryVariant = primaryColorDark, + secondary = primaryColorLight, + error = errorColor, +) + private val DarkColorPalette = darkColors( primary = primaryColor, primaryVariant = primaryColorDark, secondary = primaryColorLight, + error = errorColor, onPrimary = Color.White, background = darkBackground, - surface = Color(0xff424242) + surface = Color(0xff424242), ) -private val LightColorPalette = lightColors( +private val BlackColorPalette = darkColors( primary = primaryColor, primaryVariant = primaryColorDark, - secondary = primaryColorLight + secondary = primaryColorLight, + error = errorColor, + + onPrimary = Color.White, + background = Color(0xff000000), + surface = Color(0xff121212) ) @Composable @@ -36,9 +49,15 @@ fun isDark() = when (themePreference.value.value) { else -> true } +@Composable +fun getTheme() = if (!isDark()) LightColorPalette else when (themePreference.value.value) { + 3 -> BlackColorPalette + else -> DarkColorPalette +} + @Composable fun AliucordManagerTheme(content: @Composable () -> Unit) { - val colors = if (isDark()) DarkColorPalette else LightColorPalette + val colors = getTheme() MaterialTheme( colors, diff --git a/app/src/main/java/com/aliucord/manager/utils/Constants.kt b/app/src/main/java/com/aliucord/manager/utils/Constants.kt new file mode 100644 index 0000000..b0c0d31 --- /dev/null +++ b/app/src/main/java/com/aliucord/manager/utils/Constants.kt @@ -0,0 +1,11 @@ +package com.aliucord.manager.utils + +import android.content.Context +import android.os.Environment +import com.google.gson.Gson +import java.io.File + +val aliucordDir = File(Environment.getExternalStorageDirectory(), "Aliucord") +val pluginsDir = File(aliucordDir, "plugins") + +val gson = Gson() \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/utils/Github.kt b/app/src/main/java/com/aliucord/manager/utils/Github.kt index 4c290d4..6486990 100644 --- a/app/src/main/java/com/aliucord/manager/utils/Github.kt +++ b/app/src/main/java/com/aliucord/manager/utils/Github.kt @@ -11,16 +11,23 @@ import com.google.gson.reflect.TypeToken import java.io.InputStreamReader import java.net.URL +data class Versions(val versionCode: String, val versionName: String, val aliucordHash: String) + object Github { private const val org = "Aliucord" private const val repo = "Aliucord" - private const val commitsUrl = "https://api.github.com/repos/${org}/${repo}/commits" + private const val commitsUrl = "https://api.github.com/repos/$org/$repo/commits" + private const val dataUrl = "https://raw.githubusercontent.com/$org/$repo/builds/data.json" fun checkForUpdates() { // TODO } + val versions: Versions by lazy { + gson.fromJson(InputStreamReader(URL(dataUrl).openStream()), Versions::class.java) + } + private val commitsCache = HashMap>() private val commitsType = object : TypeToken>() {}.type private val gson = Gson() diff --git a/app/src/main/java/com/aliucord/manager/utils/Plugin.kt b/app/src/main/java/com/aliucord/manager/utils/Plugin.kt new file mode 100644 index 0000000..128f80d --- /dev/null +++ b/app/src/main/java/com/aliucord/manager/utils/Plugin.kt @@ -0,0 +1,59 @@ +package com.aliucord.manager.utils + +import android.util.Log +import com.aliucord.manager.BuildConfig +import java.io.File +import java.io.InputStreamReader +import java.util.zip.ZipException +import java.util.zip.ZipFile + +data class Manifest( + val name: String, + val authors: Array, + val description: String, + val version: String, + val changelog: String?, + val changelogMedia: String? +) { + data class Author(val name: String, val id: Long) +} + +class Plugin(val file: File) { + val manifest: Manifest + + init { + ZipFile(file).use { + val entry = it.getEntry("manifest.json") + ?: throw ZipException("Plugin ${file.nameWithoutExtension} has no manifest.") + it.getInputStream(entry).use { zis -> + manifest = gson.fromJson(InputStreamReader(zis), Manifest::class.java) + } + } + } + + companion object { + fun loadAll(): List { + if (!pluginsDir.exists() && !pluginsDir.mkdirs()) { + Log.e(BuildConfig.TAG, "Failed to create plugins dir. Missing Permissions?") + return emptyList() + } + + val files = pluginsDir.listFiles() ?: return emptyList() + val plugins = ArrayList(files.size) + + files.forEach { + if (!it.name.endsWith(".zip")) { + Log.w(BuildConfig.TAG, "Found non zip ${it.name} in plugins folder.") + } else { + try { + plugins.add(Plugin(it)) + } catch (th: Throwable) { + Log.e(BuildConfig.TAG, "Failed to load plugin ${it.nameWithoutExtension}", th) + } + } + } + + return plugins.apply { sortBy { it.manifest.name }} + } + } +} \ No newline at end of file diff --git a/app/src/main/res/drawable/ic_history_white_24dp.xml b/app/src/main/res/drawable/ic_history_white_24dp.xml new file mode 100644 index 0000000..94396f7 --- /dev/null +++ b/app/src/main/res/drawable/ic_history_white_24dp.xml @@ -0,0 +1,3 @@ + + + diff --git a/app/src/main/res/drawable/ic_plugin_24dp.xml b/app/src/main/res/drawable/ic_plugin_24dp.xml new file mode 100644 index 0000000..34f0422 --- /dev/null +++ b/app/src/main/res/drawable/ic_plugin_24dp.xml @@ -0,0 +1,9 @@ + + + \ No newline at end of file diff --git a/app/src/main/res/layout/plugin.xml b/app/src/main/res/layout/plugin.xml new file mode 100644 index 0000000..5f9fe04 --- /dev/null +++ b/app/src/main/res/layout/plugin.xml @@ -0,0 +1,25 @@ + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 476c564..83cc979 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -11,4 +11,13 @@ Show menu Theme Use Aliucord.dex from storage + Plugins + Launch Plugins Page + %1$s v%2$s by %3$s + Delete %1$s + Search + View changelog for %1$s + Are you sure you want to delete this plugin? This action cannot be undone. + Delete + %1$s Changelog diff --git a/gradle.properties b/gradle.properties index 98bed16..2c94133 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,21 +1,17 @@ -# Project-wide Gradle settings. -# IDE (e.g. Android Studio) users: -# Gradle settings configured through the IDE *will override* -# any settings specified in this file. -# For more details on how to configure your build environment visit +## For more details on how to configure your build environment visit # http://www.gradle.org/docs/current/userguide/build_environment.html +# # Specifies the JVM arguments used for the daemon process. # The setting is particularly useful for tweaking memory settings. -org.gradle.jvmargs=-Xmx2048m -Dfile.encoding=UTF-8 +# Default value: -Xmx1024m -XX:MaxPermSize=256m +# org.gradle.jvmargs=-Xmx2048m -XX:MaxPermSize=512m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 +# # When configured, Gradle will run in incubating parallel mode. # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true -# AndroidX package structure to make it clearer which packages are bundled with the -# Android operating system, and which are packaged with your app"s APK -# https://developer.android.com/topic/libraries/support-library/androidx-rn +#Wed Nov 10 16:11:30 CET 2021 +kotlin.code.style=official +org.gradle.jvmargs=-Xmx1536M -Dkotlin.daemon.jvm.options\="-Xmx2048M" -Dfile.encoding\=UTF-8 android.useAndroidX=true -# Automatically convert third-party libraries to use AndroidX android.enableJetifier=true -# Kotlin code style for this project: "official" or "obsolete": -kotlin.code.style=official \ No newline at end of file