Add plugins page

This commit is contained in:
Vendicated 2021-11-10 21:26:12 +01:00
parent 7881279196
commit 137a739705
No known key found for this signature in database
GPG key ID: EC781ADFB93EFFA3
19 changed files with 490 additions and 34 deletions

View file

@ -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}")

View file

@ -10,6 +10,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<application
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"

View file

@ -17,6 +17,7 @@ import androidx.compose.foundation.layout.*
import androidx.compose.material.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material.icons.filled.Build
import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -98,6 +99,15 @@ fun MainActivityLayout() {
tint = MaterialTheme.colors.onPrimary
)
}
IconButton(onClick = { navController.navigate(Screen.Plugins.route) {
popUpTo(Screen.Home.route) { saveState = true }
} }) {
Icon(
painter = painterResource(R.drawable.ic_plugin_24dp),
contentDescription = stringResource(id = R.string.launch_plugins),
tint = MaterialTheme.colors.onPrimary
)
}
IconButton(onClick = { isMenuExpanded = !isMenuExpanded }) {
Icon(
imageVector = Icons.Default.MoreVert,

View file

@ -6,9 +6,11 @@
package com.aliucord.manager.ui
import androidx.annotation.StringRes
import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.runtime.Composable
import com.aliucord.manager.R
import com.aliucord.manager.ui.screens.HomeScreen
import com.aliucord.manager.ui.screens.PluginsScreen
import com.aliucord.manager.ui.screens.SettingsScreen
sealed class Screen(
@ -18,7 +20,7 @@ sealed class Screen(
) {
companion object {
// NOTE: without lazy it doesn't work, kotlin moment https://youtrack.jetbrains.com/issue/KT-8970
val SCREENS by lazy { listOf(Home, Settings) }
val SCREENS by lazy { listOf(Home, Settings, Plugins) }
}
object Home : Screen(
@ -32,4 +34,10 @@ sealed class Screen(
R.string.settings,
{ SettingsScreen() }
)
object Plugins : Screen(
"plugins",
R.string.plugins,
{ PluginsScreen() }
)
}

View file

@ -0,0 +1,99 @@
package com.aliucord.manager.ui.components
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.text.ClickableText
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.LocalContentColor
import androidx.compose.material.MaterialTheme
import androidx.compose.material.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.*
import androidx.compose.ui.text.style.TextDecoration
import java.util.regex.Pattern
private val hyperLinkPattern = Pattern.compile("\\[(.+?)]\\((.+?\\))")
@SuppressLint("ComposableNaming") // Can't use MaterialTheme without Composable, but this is not a component
@Composable
private fun AnnotatedString.Builder.hyperlink(content: String, outTags: ArrayList<String>) {
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<String>()
LinkText(
buildAnnotatedString {
withStyle(style = SpanStyle(color = MaterialTheme.colors.primary)) {
append(bulletPoint)
}
hyperlink(line.substring(1), tags)
}
)
}
else -> {
val tags = ArrayList<String>()
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)
}
}
)
}

View file

@ -53,7 +53,10 @@ fun ExposedDropdownMenu(
) {
items.forEachIndexed { i, item ->
DropdownMenuItem(
onClick = { selectItem(i) }
onClick = {
expanded = false
selectItem(i)
}
) {
Text(item)
}

View file

@ -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<String?>(null) }
val versions = remember { mutableStateOf<Versions?>(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)
)
}

View file

@ -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
)
}
}
}
}
}
}
}
}

View file

@ -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

View file

@ -12,3 +12,4 @@ val darkBackground = Color(0xff2e2e2e)
val primaryColor = Color(0xff00c853)
val primaryColorLight = Color(0xff5efc82)
val primaryColorDark = Color(0xff009624)
val errorColor = Color(0xffed4245)

View file

@ -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,

View file

@ -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()

View file

@ -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<String, Array<Commit>>()
private val commitsType = object : TypeToken<Array<Commit>>() {}.type
private val gson = Gson()

View file

@ -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<Author>,
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<Plugin> {
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<Plugin>(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 }}
}
}
}

View file

@ -0,0 +1,3 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#ffffff" android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z"/>
</vector>

View file

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M10.5,4.5c0.28,0 0.5,0.22 0.5,0.5v2h6v6h2c0.28,0 0.5,0.22 0.5,0.5s-0.22,0.5 -0.5,0.5h-2v6h-2.12c-0.68,-1.75 -2.39,-3 -4.38,-3s-3.7,1.25 -4.38,3H4v-2.12c1.75,-0.68 3,-2.39 3,-4.38 0,-1.99 -1.24,-3.7 -2.99,-4.38L4,7h6V5c0,-0.28 0.22,-0.5 0.5,-0.5m0,-2C9.12,2.5 8,3.62 8,5H4c-1.1,0 -1.99,0.9 -1.99,2v3.8h0.29c1.49,0 2.7,1.21 2.7,2.7s-1.21,2.7 -2.7,2.7H2V20c0,1.1 0.9,2 2,2h3.8v-0.3c0,-1.49 1.21,-2.7 2.7,-2.7s2.7,1.21 2.7,2.7v0.3H17c1.1,0 2,-0.9 2,-2v-4c1.38,0 2.5,-1.12 2.5,-2.5S20.38,11 19,11V7c0,-1.1 -0.9,-2 -2,-2h-4c0,-1.38 -1.12,-2.5 -2.5,-2.5z" />
</vector>

View file

@ -0,0 +1,25 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<Switch
android:id="@+id/plugin_header"
android:layout_width="match_parent"
android:layout_height="48dp"
android:background="@color/design_default_color_secondary_variant"
android:padding="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
android:id="@+id/plugin_description"
android:layout_width="match_parent"
android:layout_height="100dp"
android:background="@color/design_default_color_primary"
android:padding="16dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/plugin_header" />
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -11,4 +11,13 @@
<string name="show_menu">Show menu</string>
<string name="theme">Theme</string>
<string name="use_dex_from_storage">Use Aliucord.dex from storage</string>
<string name="plugins">Plugins</string>
<string name="launch_plugins">Launch Plugins Page</string>
<string name="plugin_title">%1$s v%2$s by %3$s</string>
<string name="delete_plugin">Delete %1$s</string>
<string name="search">Search</string>
<string name="view_plugin_changelog">View changelog for %1$s</string>
<string name="delete_plugin_body">Are you sure you want to delete this plugin? This action cannot be undone.</string>
<string name="delete">Delete</string>
<string name="changelog">%1$s Changelog</string>
</resources>

View file

@ -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