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