diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 0e5dfcd..fdb70d2 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -53,14 +53,14 @@ dependencies { // core dependencies implementation("androidx.appcompat:appcompat:1.4.1") implementation("androidx.core:core-ktx:1.7.0") - implementation("com.google.android.material:material:1.5.0") + implementation("com.github.aliucord:libzip:1.0.1") // compose dependencies implementation("androidx.activity:activity-compose:1.4.0") implementation("androidx.compose.ui:ui:${composeVersion}") - implementation("androidx.compose.material3:material3:1.0.0-alpha05") + implementation("androidx.compose.material3:material3:1.0.0-alpha06") implementation("androidx.compose.material:material:${composeVersion}") - implementation("androidx.compose.ui:ui-tooling-preview:1.1.0") + implementation("androidx.compose.ui:ui-tooling-preview:${composeVersion}") implementation("androidx.navigation:navigation-compose:2.4.1") // accompanist dependencies @@ -70,10 +70,11 @@ dependencies { // other dependencies implementation("com.google.code.gson:gson:2.8.9") implementation("io.coil-kt:coil-compose:1.4.0") - implementation("androidx.datastore:datastore-preferences:1.0.0") // ktor implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("io.ktor:ktor-client-android:$ktorVersion") + implementation("io.ktor:ktor-client-gson:$ktorVersion") + + implementation("org.bouncycastle:bcpkix-jdk15on:1.65") } \ No newline at end of file diff --git a/app/proguard-rules.pro b/app/proguard-rules.pro index 9f38a9b..80f29cd 100644 --- a/app/proguard-rules.pro +++ b/app/proguard-rules.pro @@ -15,5 +15,6 @@ # Uncomment this to preserve the line number information for # debugging stack traces. -keepattributes SourceFile,LineNumberTable +-renamesourcefileattribute SourceFile --keep class com.aliucord.manager.models.** { private final *; } +-keep class com.aliucord.manager.models.** { private final *; } \ No newline at end of file diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index e4d7799..497e75a 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -3,16 +3,25 @@ xmlns:tools="http://schemas.android.com/tools" package="com.aliucord.manager"> + - - + + + + + + + diff --git a/app/src/main/assets/icon1.png b/app/src/main/assets/icon1.png new file mode 100644 index 0000000..592fc38 Binary files /dev/null and b/app/src/main/assets/icon1.png differ diff --git a/app/src/main/assets/icon2.png b/app/src/main/assets/icon2.png new file mode 100644 index 0000000..7d8a5dd Binary files /dev/null and b/app/src/main/assets/icon2.png differ diff --git a/app/src/main/assets/kotlin/LICENSE.txt b/app/src/main/assets/kotlin/LICENSE.txt new file mode 100644 index 0000000..9c308d9 --- /dev/null +++ b/app/src/main/assets/kotlin/LICENSE.txt @@ -0,0 +1,202 @@ + + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2000-2020 JetBrains s.r.o. and Kotlin Programming Language contributors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/app/src/main/assets/kotlin/classes.dex b/app/src/main/assets/kotlin/classes.dex new file mode 100644 index 0000000..add9e1b Binary files /dev/null and b/app/src/main/assets/kotlin/classes.dex differ diff --git a/app/src/main/assets/pine/LICENSE b/app/src/main/assets/pine/LICENSE new file mode 100644 index 0000000..4780a49 --- /dev/null +++ b/app/src/main/assets/pine/LICENSE @@ -0,0 +1,46 @@ +Copyright (c) canyie + +"Anti 996" License Version 1.0 (Draft) + +Permission is hereby granted to any individual or legal entity +obtaining a copy of this licensed work (including the source code, +documentation and/or related items, hereinafter collectively referred +to as the "licensed work"), free of charge, to deal with the licensed +work for any purpose, including without limitation, the rights to use, +reproduce, modify, prepare derivative works of, distribute, publish +and sublicense the licensed work, subject to the following conditions: + +1. The individual or the legal entity must conspicuously display, +without modification, this License and the notice on each redistributed +or derivative copy of the Licensed Work. + +2. The individual or the legal entity must strictly comply with all +applicable laws, regulations, rules and standards of the jurisdiction +relating to labor and employment where the individual is physically +located or where the individual was born or naturalized; or where the +legal entity is registered or is operating (whichever is stricter). In +case that the jurisdiction has no such laws, regulations, rules and +standards or its laws, regulations, rules and standards are +unenforceable, the individual or the legal entity are required to +comply with Core International Labor Standards. + +3. The individual or the legal entity shall not induce, suggest or force +its employee(s), whether full-time or part-time, or its independent +contractor(s), in any methods, to agree in oral or written form, to +directly or indirectly restrict, weaken or relinquish his or her +rights or remedies under such laws, regulations, rules and standards +relating to labor and employment as mentioned above, no matter whether +such written or oral agreements are enforceable under the laws of the +said jurisdiction, nor shall such individual or the legal entity +limit, in any methods, the rights of its employee(s) or independent +contractor(s) from reporting or complaining to the copyright holder or +relevant authorities monitoring the compliance of the license about +its violation(s) of the said license. + +THE LICENSED WORK IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, +DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR +OTHERWISE, ARISING FROM, OUT OF OR IN ANY WAY CONNECTION WITH THE +LICENSED WORK OR THE USE OR OTHER DEALINGS IN THE LICENSED WORK. diff --git a/app/src/main/assets/pine/README.md b/app/src/main/assets/pine/README.md new file mode 100644 index 0000000..6877295 --- /dev/null +++ b/app/src/main/assets/pine/README.md @@ -0,0 +1,8 @@ +# Pine + +https://github.com/canyie/pine + +classes.dex is pine-core 0.2.5 and pine-xposed 0.0.8 merged + +### Building from source +Use [the script](make_artifacts.sh) or read it and do the steps manually diff --git a/app/src/main/assets/pine/arm64-v8a/libpine.so b/app/src/main/assets/pine/arm64-v8a/libpine.so new file mode 100644 index 0000000..dde49ae Binary files /dev/null and b/app/src/main/assets/pine/arm64-v8a/libpine.so differ diff --git a/app/src/main/assets/pine/armeabi-v7a/libpine.so b/app/src/main/assets/pine/armeabi-v7a/libpine.so new file mode 100644 index 0000000..664e367 Binary files /dev/null and b/app/src/main/assets/pine/armeabi-v7a/libpine.so differ diff --git a/app/src/main/assets/pine/classes.dex b/app/src/main/assets/pine/classes.dex new file mode 100644 index 0000000..7ac364f Binary files /dev/null and b/app/src/main/assets/pine/classes.dex differ diff --git a/app/src/main/assets/pine/make_artifacts.sh b/app/src/main/assets/pine/make_artifacts.sh new file mode 100644 index 0000000..0576a71 --- /dev/null +++ b/app/src/main/assets/pine/make_artifacts.sh @@ -0,0 +1,100 @@ +#!/bin/sh +# Simple script to compile all the required files for pine +# - Prompts you for Aliucord & pine dir +# - Compiles pine core and xposed +# - Merges core and xposed classes into one jar +# - Compiles the jar to DEX bytecode via d8 +# - Copies classes.jar to Aliucord/.assets +# - Copies libpine.so builds and the compiled classes.dex to the Aliucord/installer/.../assets + +set -e + +RED="\033[0;31m" +YELLOW="\033[0;33m" +RESET="\033[0m" + +# Usage: message... +die() { + printf "$RED%s$RESET\n" "$*" + exit 1 +} >&2 + +# Usage: message... +log() { + echo + printf "$YELLOW%s$RESET" "$*" + echo + sleep .3 +} + +# Usage: OutVariableName FolderName expectedChildren... +prompt_folder() { + varname="$1" + shift + log "Please specify the $1 directory. You may use a relative path or even . " + printf "> " + read -r response + case "$response" in + ~*) response="$HOME/$(printf "%s" "$response" | cut -c 2-)" ;; + esac + dir="$(realpath "$response")" + shift + for file in "$@"; do + [ -e "$dir/$file" ] || die "$dir/$file doesn't exist. Are you sure you specified the correct directory?" + done + eval "$varname=$dir" +} + +ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}" +[ -d "$ANDROID_HOME" ] || die "Android SDK not found. Please set ANDROID_HOME and try again." +BUILD_TOOLS="$(find "$ANDROID_HOME/build-tools" -maxdepth 1 | sort | tail -n 1)" +[ -x "$BUILD_TOOLS/d8" ] || die "Couldn't find your buildtools. Make sure ANDROID_HOME is correct" + +for dep in zip unzip; do + command -v "$dep" >/dev/null || die "$dep not found. Please install it then try again" +done + +prompt_folder PINE Pine core xposed gradlew +echo +prompt_folder ALIUCORD Aliucord installer .assets + +( + +cd "$PINE" +chmod +x gradlew + +log "Assembling Pine Core" +./gradlew core:assemble +log "Assembling Pine Xposed" +./gradlew xposed:assemble + +CORE="$(mktemp -d)" +XPOSED="$(mktemp -d)" +CLASSES="$(mktemp -d)" + +log "Extracting Pine Core & Xposed builds" +unzip core/build/outputs/aar/core-release.aar -d "$CORE" +unzip xposed/build/outputs/aar/xposed-release.aar -d "$XPOSED" + +log "Merging Pine Core & Xposed classes" +cd "$CLASSES" +unzip "$CORE/classes.jar" +unzip "$XPOSED/classes.jar" +zip -r classes.jar -- * + +log "Compiling to DEX bytecode" +"$BUILD_TOOLS/d8" classes.jar + +log "Copying files to Aliucord assets dir" +PINE_ASSETS="$ALIUCORD/installer/android/app/src/main/assets/pine" +cp classes.dex "$PINE_ASSETS" +cp classes.jar "$ALIUCORD/.assets/pine.jar" +for dir in "$CORE/jni/"*; do + outdir="$PINE_ASSETS/$(basename "$dir")" + mkdir -p "$outdir" + cp "$dir/libpine.so" "$outdir" +done + +log "Done! Successfully built and copied all pine artifacts to the installer assets." + +) diff --git a/app/src/main/java/com/aliucord/manager/data/DataStore.kt b/app/src/main/java/com/aliucord/manager/data/DataStore.kt deleted file mode 100644 index cac5725..0000000 --- a/app/src/main/java/com/aliucord/manager/data/DataStore.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.aliucord.manager.data - -import android.content.Context -import androidx.datastore.core.DataStore -import androidx.datastore.preferences.core.Preferences -import androidx.datastore.preferences.core.intPreferencesKey -import androidx.datastore.preferences.preferencesDataStore - -class StorePreferences(private val context: Context) { - private val THEME = intPreferencesKey("theme") - - - companion object { - private val Context.dataStore: DataStore by preferencesDataStore("settings") - } - - init { - - } -} \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/components/plugins/Changelog.kt b/app/src/main/java/com/aliucord/manager/ui/components/plugins/Changelog.kt deleted file mode 100644 index b6349a8..0000000 --- a/app/src/main/java/com/aliucord/manager/ui/components/plugins/Changelog.kt +++ /dev/null @@ -1,196 +0,0 @@ -package com.aliucord.manager.ui.components.plugins - -import android.annotation.SuppressLint -import android.graphics.BitmapFactory -import android.util.Log -import androidx.compose.foundation.Image -import androidx.compose.foundation.background -import androidx.compose.foundation.layout.* -import androidx.compose.foundation.lazy.LazyColumn -import androidx.compose.foundation.lazy.items -import androidx.compose.foundation.text.ClickableText -import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.ui.Alignment -import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.ImageBitmap -import androidx.compose.ui.graphics.asImageBitmap -import androidx.compose.ui.platform.LocalUriHandler -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.BuildConfig -import com.aliucord.manager.R -import com.aliucord.manager.utils.Plugin -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.launch -import java.net.URL -import java.util.regex.Pattern - -private val hyperLinkPattern = Pattern.compile("\\[(.+?)]\\((.+?\\))") - -@Suppress("RegExpRedundantEscape") // It is very much not redundant and causes a crash lol -private val headerStylePattern = Pattern.compile("\\{(improved|added|fixed)( marginTop)?\\}") - -@SuppressLint("ComposableNaming") // Can't use MaterialTheme without Composable, but this is not a component -@Composable -private fun AnnotatedString.Builder.hyperlink(content: 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.colorScheme.primary, - textDecoration = TextDecoration.Underline - ) - ) { - pushStringAnnotation(title, url) - append(title) - pop() - } - idx = end - } - } - - if (idx < content.length) append(content.substring(idx)) -} - -@Composable -fun Changelog(plugin: Plugin, onDismiss: () -> Unit) { - val imageBitmap = remember { - mutableStateOf(null) - } - - if (imageBitmap.value == null && plugin.manifest.changelogMedia != null) { - LaunchedEffect(null) { - launch(Dispatchers.IO) { - try { - @Suppress("BlockingMethodInNonBlockingContext") - imageBitmap.value = URL(plugin.manifest.changelogMedia).openStream().use { stream -> - BitmapFactory.decodeStream(stream) - }.asImageBitmap() - } catch (err: Throwable) { - Log.e( - BuildConfig.TAG, - "Failed to load changelogMedia ${plugin.manifest.changelogMedia}", - err - ) - } - } - } - } - - Column( - modifier = Modifier - .background(MaterialTheme.colorScheme.surface) - .fillMaxHeight(0.9f) - .padding(16.dp), - verticalArrangement = Arrangement.SpaceBetween - ) { - Text(plugin.manifest.name, style = MaterialTheme.typography.headlineLarge) - Divider(color = MaterialTheme.colorScheme.primary, modifier = Modifier.padding(vertical = 8.dp)) - - imageBitmap.value?.let { - Image(it, "", modifier = Modifier.fillMaxWidth()) - } - - LazyColumn( - modifier = Modifier - .padding(bottom = 16.dp) - .fillMaxHeight(0.92f) - ) { - items(plugin.manifest.changelog!!.lines()) { - var line = it.trim() - if (line.isNotEmpty()) { - when (line[0]) { - '#' -> { - do { - line = line.substring(1) - } while (line.startsWith("#")) - - Text( - line.trimStart(), - fontWeight = FontWeight.Bold, - modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) - ) - } - '*' -> { - LinkText( - buildAnnotatedString { - withStyle( - style = SpanStyle( - color = MaterialTheme.colorScheme.primary, - fontWeight = FontWeight.Bold - ) - ) { - append("● ") - } - - hyperlink(line.substring(1)) - }, - Modifier.padding(bottom = 2.dp) - ) - } - else -> { - when { - line.endsWith("marginTop}") -> { - val color = MaterialTheme.colorScheme.onSurface - Text( - line, - fontWeight = FontWeight.Bold, - color = color, - modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) - ) - } - line.all { c -> c == '=' } -> {} // Discord ignores ======= - else -> { - LinkText( - buildAnnotatedString { - hyperlink(line) - } - ) - } - } - } - } - } - } - } - - Button( - onClick = onDismiss, - modifier = Modifier.align(Alignment.End).height(IntrinsicSize.Min) - ) { - Text(stringResource(R.string.close)) - } - } - -} - -@Composable -private fun LinkText(annotatedString: AnnotatedString, modifier: Modifier = Modifier) { - val urlHandler = LocalUriHandler.current - - ClickableText( - text = annotatedString, - style = TextStyle(color = LocalContentColor.current), - onClick = { offset -> - annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let { - urlHandler.openUri(it.item) - } - }, - modifier = modifier - ) -} \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/screens/InstallScreen.kt b/app/src/main/java/com/aliucord/manager/ui/screens/InstallScreen.kt deleted file mode 100644 index 2f884e3..0000000 --- a/app/src/main/java/com/aliucord/manager/ui/screens/InstallScreen.kt +++ /dev/null @@ -1,60 +0,0 @@ -package com.aliucord.manager.ui.screens - -import androidx.compose.foundation.layout.* -import androidx.compose.material3.LinearProgressIndicator -import androidx.compose.material3.Text -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext -import androidx.compose.ui.unit.dp -import com.aliucord.manager.utils.DownloadUtils -import com.aliucord.manager.utils.Github -import com.aliucord.manager.utils.aliucordDir -import kotlinx.coroutines.launch -import java.io.File - -@Composable -fun InstallerScreen() { - var log by remember { mutableStateOf("") } - val context = LocalContext.current - - LaunchedEffect(Unit) { - launch { - val supportedVersion = Github.versions.versionCode - - log += "Checking for cached APK...\n" - - val apk = File(context.cacheDir, "discord-${supportedVersion}").let { file -> - val archiveInfo = context.packageManager.getPackageArchiveInfo(file.path, 0) - - if (file.exists() && archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@let file - - log += "Downloading APK...\n" - - DownloadUtils.downloadDiscordApk(context, supportedVersion) - - log += "Finished downloading APK\n" - } - - val manifest = File(aliucordDir, "AndroidManifest.xml").takeIf(File::exists) ?: run { - log += "Downloading manifest\n" - - DownloadUtils.download(context, Github.getDownloadUrl("builds", "AndroidManifest.xml")) - } - } - } - - Column( - modifier = Modifier.fillMaxWidth(), - verticalArrangement = Arrangement.spacedBy(8.dp) - ) { - Text(log) - - Spacer(Modifier.weight(1f, true)) - - LinearProgressIndicator( - modifier = Modifier.fillMaxWidth().height(8.dp), - progress = 0.5f - ) - } -} \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/screens/StoreScreen.kt b/app/src/main/java/com/aliucord/manager/ui/screens/StoreScreen.kt deleted file mode 100644 index 101cadc..0000000 --- a/app/src/main/java/com/aliucord/manager/ui/screens/StoreScreen.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.aliucord.manager.ui.screens - -import androidx.compose.foundation.layout.Column -import androidx.compose.runtime.Composable - -@Composable -fun StoreScreen() { - Column { - - } -} \ No newline at end of file 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 deleted file mode 100644 index 331bc6b..0000000 --- a/app/src/main/java/com/aliucord/manager/ui/theme/Color.kt +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Copyright (c) 2021 Juby210 - * Licensed under the Open Software License version 3.0 - */ - -package com.aliucord.manager.ui.theme - -import androidx.compose.ui.graphics.Color - -val darkBackground = Color(0xff1a1a1a) - -val primaryColor = Color(0xff00c853) -val primaryColorLight = Color(0xff5efc82) -val primaryColorDark = Color(0xff009624) - -val discordBrand = Color(0xff5865F2) -val discordGreen = Color(0xff57F287) -val discordRed = Color(0xffED4245) - -val errorColor = discordRed - -val Blue10 = Color(0xFF000F5E) -val Blue20 = Color(0xFF001E92) -val Blue30 = Color(0xFF002ECC) -val Blue40 = Color(0xFF1546F6) -val Blue80 = Color(0xFFB8C3FF) -val Blue90 = Color(0xFFDDE1FF) - -val DarkBlue10 = Color(0xFF00036B) -val DarkBlue20 = Color(0xFF000BA6) -val DarkBlue30 = Color(0xFF1026D3) -val DarkBlue40 = Color(0xFF3648EA) -val DarkBlue80 = Color(0xFFBBC2FF) -val DarkBlue90 = Color(0xFFDEE0FF) - -val Yellow10 = Color(0xFF261900) -val Yellow20 = Color(0xFF402D00) -val Yellow30 = Color(0xFF5C4200) -val Yellow40 = Color(0xFF7A5900) -val Yellow80 = Color(0xFFFABD1B) -val Yellow90 = Color(0xFFFFDE9C) - -val Red10 = Color(0xFF410001) -val Red20 = Color(0xFF680003) -val Red30 = Color(0xFF930006) -val Red40 = Color(0xFFBA1B1B) -val Red80 = Color(0xFFFFB4A9) -val Red90 = Color(0xFFFFDAD4) - -val Grey10 = Color(0xFF191C1D) -val Grey20 = Color(0xFF2D3132) -val Grey80 = Color(0xFFC4C7C7) -val Grey90 = Color(0xFFE0E3E3) -val Grey95 = Color(0xFFEFF1F1) -val Grey99 = Color(0xFFFBFDFD) - -val BlueGrey30 = Color(0xFF45464F) -val BlueGrey50 = Color(0xFF767680) -val BlueGrey60 = Color(0xFF90909A) -val BlueGrey80 = Color(0xFFC6C5D0) -val BlueGrey90 = Color(0xFFE2E1EC) \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/utils/DownloadUtils.kt b/app/src/main/java/com/aliucord/manager/utils/DownloadUtils.kt deleted file mode 100644 index dfdd773..0000000 --- a/app/src/main/java/com/aliucord/manager/utils/DownloadUtils.kt +++ /dev/null @@ -1,51 +0,0 @@ -package com.aliucord.manager.utils - -import android.app.DownloadManager -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import android.widget.Toast -import androidx.core.net.toFile -import androidx.core.net.toUri -import java.io.File -import kotlin.coroutines.resume -import kotlin.coroutines.suspendCoroutine - -object DownloadUtils { - private const val backendHost = "https://aliucord.com/" - - suspend fun downloadDiscordApk(ctx: Context, version: String) = download(ctx, "$backendHost/download/discord?v=$version") - - suspend fun download(ctx: Context, url: String): File { - val downloadManager = ctx.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager - - return suspendCoroutine { continuation -> - try { - val receiver = object : BroadcastReceiver() { - var downloadId = 0L - - override fun onReceive(context: Context, intent: Intent) { - val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) - - if (downloadId == id) { - Toast.makeText(ctx, "Finished downloading", Toast.LENGTH_SHORT).show() - ctx.unregisterReceiver(this) - continuation.resume(downloadManager.getUriForDownloadedFile(id).toFile()) - } - } - } - - ctx.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) - - receiver.downloadId = DownloadManager.Request(url.toUri()).run { - setTitle("Downloading APK") - setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) - downloadManager.enqueue(this) - } - } finally { - Toast.makeText(ctx, "Finished downloading", Toast.LENGTH_SHORT).show() - } - } - } -} \ 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 deleted file mode 100644 index 6500877..0000000 --- a/app/src/main/java/com/aliucord/manager/utils/Github.kt +++ /dev/null @@ -1,57 +0,0 @@ -/* - * Copyright (c) 2021 Juby210 - * Licensed under the Open Software License version 3.0 - */ - -package com.aliucord.manager.utils - -import com.aliucord.manager.models.Commit -import com.aliucord.manager.models.GithubUser -import com.google.gson.reflect.TypeToken -import java.io.InputStreamReader -import java.net.URL -import kotlin.collections.set - -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 contributorsUrl = "https://api.github.com/repos/$org/$repo/contributors" - 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 contributorsType = object : TypeToken>() {}.type - - fun getCommits(params: Map?): Array { - // return emptyArray() - val query = params?.map { it.key + "=" + it.value }?.joinToString("&", "?") ?: "" - if (commitsCache.containsKey(query)) return commitsCache[query] ?: emptyArray() - val res = InputStreamReader(URL(commitsUrl + query).openStream()).use { - gson.fromJson>(it, commitsType) - } - commitsCache[query] = res - return res - } - - val contributors: Array by lazy() { - InputStreamReader(URL(contributorsUrl).openStream()).use { stream -> - gson.fromJson>(stream, contributorsType).apply { - sortByDescending { it.contributions } - } - } - } - - fun getDownloadUrl(ref: String, file: String) = "https://raw.githubusercontent.com/${org}/${repo}/${ref}/${file}" -} \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/KeySet.kt b/app/src/main/kotlin/com/aliucord/manager/KeySet.kt new file mode 100644 index 0000000..5946444 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/KeySet.kt @@ -0,0 +1,6 @@ +package com.aliucord.manager + +import java.security.PrivateKey +import java.security.cert.X509Certificate + +class KeySet(val publicKey: X509Certificate, val privateKey: PrivateKey) \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/MainActivity.kt b/app/src/main/kotlin/com/aliucord/manager/MainActivity.kt similarity index 66% rename from app/src/main/java/com/aliucord/manager/ui/MainActivity.kt rename to app/src/main/kotlin/com/aliucord/manager/MainActivity.kt index 99a8cfd..2eb1ce6 100644 --- a/app/src/main/java/com/aliucord/manager/ui/MainActivity.kt +++ b/app/src/main/kotlin/com/aliucord/manager/MainActivity.kt @@ -3,14 +3,20 @@ * Licensed under the Open Software License version 3.0 */ -package com.aliucord.manager.ui +package com.aliucord.manager import android.Manifest +import android.annotation.SuppressLint import android.content.Context +import android.content.Intent +import android.os.Build import android.os.Bundle +import android.os.Environment +import android.provider.Settings import androidx.activity.ComponentActivity import androidx.activity.compose.setContent import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.padding import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material3.ExperimentalMaterial3Api @@ -18,15 +24,21 @@ import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.unit.dp +import androidx.core.net.toUri import androidx.navigation.compose.NavHost import androidx.navigation.compose.composable import androidx.navigation.compose.rememberNavController +import com.aliucord.manager.preferences.Preferences import com.aliucord.manager.preferences.sharedPreferences +import com.aliucord.manager.preferences.themePref +import com.aliucord.manager.ui.Screen import com.aliucord.manager.ui.components.AppBar import com.aliucord.manager.ui.components.GrantPermission import com.aliucord.manager.ui.screens.* import com.aliucord.manager.ui.theme.ManagerTheme +import com.aliucord.manager.ui.theme.Theme import com.aliucord.manager.utils.Github import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.PermissionRequired @@ -44,7 +56,14 @@ class MainActivity : ComponentActivity() { Github.checkForUpdates() setContent { - ManagerTheme { + ManagerTheme( + isBlack = Preferences.useBlack.get(), + isDarkTheme = run { + val theme = Theme.from(themePref.get()) + + theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK + } + ) { MainActivityLayout() } } @@ -57,11 +76,19 @@ class MainActivity : ComponentActivity() { @ExperimentalPermissionsApi @Composable private fun MainActivityLayout() { - val storagePermissionState = rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) + val storagePermissionState = rememberPermissionState(Manifest.permission.READ_EXTERNAL_STORAGE) val navController = rememberNavController() + val requestAllFilesAccess = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R + val context = LocalContext.current - if (!storagePermissionState.hasPermission) SideEffect { - storagePermissionState.launchPermissionRequest() + SideEffect { + @SuppressLint("NewApi") + if (requestAllFilesAccess && !Environment.isExternalStorageManager()) { + val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) + .setData("package:com.aliucord.manager".toUri()) + + context.startActivity(intent) + } else if (storagePermissionState.shouldShowRationale) storagePermissionState.launchPermissionRequest() } Scaffold( @@ -69,8 +96,8 @@ private fun MainActivityLayout() { ) { paddingValues -> PermissionRequired( permissionState = storagePermissionState, - permissionNotGrantedContent = { GrantPermission(storagePermissionState) }, - permissionNotAvailableContent = { GrantPermission(storagePermissionState) } + permissionNotAvailableContent = { GrantPermission(storagePermissionState) }, + permissionNotGrantedContent = { GrantPermission(storagePermissionState) } ) { NavHost( navController, @@ -82,19 +109,8 @@ private fun MainActivityLayout() { composable(Screen.About.route) { AboutScreen() } composable(Screen.Settings.route) { SettingsScreen() } composable(Screen.Store.route) { StoreScreen() } - composable( - route = Screen.Installer.route, -// arguments = listOf( -// navArgument("method") { type = NavType.EnumType( type = InstallMethod::class.java ) } -// )) - ) { - InstallerScreen() - } + composable(Screen.Installer.route) { InstallerScreen(navController) } } } } -} - -enum class InstallMethod { - DOWNLOAD, INSTALLED, STORAGE } \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/Signer.kt b/app/src/main/kotlin/com/aliucord/manager/Signer.kt new file mode 100644 index 0000000..228e924 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/Signer.kt @@ -0,0 +1,199 @@ +/* + * Copyright (c) 2021 Juby210 & Vendicated + * Licensed under the Open Software License version 3.0 + */ +package com.aliucord.manager + +import com.aliucord.libzip.Zip +import org.bouncycastle.asn1.x500.X500Name +import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo +import org.bouncycastle.cert.X509v3CertificateBuilder +import org.bouncycastle.cert.jcajce.JcaCertStore +import org.bouncycastle.cert.jcajce.JcaX509CertificateConverter +import org.bouncycastle.cms.CMSProcessableByteArray +import org.bouncycastle.cms.CMSSignedDataGenerator +import org.bouncycastle.cms.jcajce.JcaSignerInfoGeneratorBuilder +import org.bouncycastle.operator.jcajce.JcaContentSignerBuilder +import org.bouncycastle.operator.jcajce.JcaDigestCalculatorProviderBuilder +import org.bouncycastle.util.encoders.Base64 +import java.io.ByteArrayOutputStream +import java.io.File +import java.io.IOException +import java.math.BigInteger +import java.security.* +import java.security.cert.Certificate +import java.security.cert.X509Certificate +import java.util.* +import java.util.jar.Attributes +import java.util.jar.JarFile +import java.util.jar.Manifest + +object Signer { + private val stripPattern = Regex("^META-INF/(.*)[.](MF|SF|RSA|DSA)$") + + fun newKeystore(out: File?) { + val password = "password".toCharArray() + val key = createKey() + + with(KeyStore.getInstance("BKS", "BC")) { + load(null, password) + setKeyEntry("alias", key.privateKey, password, arrayOf(key.publicKey)) + store(out?.outputStream(), password) + } + } + + @Throws(Exception::class) + private fun createKey(): KeySet { + val pair = KeyPairGenerator.getInstance("RSA").run { + initialize(2048) + generateKeyPair() + } + var serialNumber: BigInteger + + do serialNumber = SecureRandom().nextInt().toBigInteger() + while (serialNumber < BigInteger.ZERO) + + val x500Name = X500Name("CN=Aliucord Installer") + val builder = X509v3CertificateBuilder( + x500Name, + serialNumber, + Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L), + Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L), + Locale.ENGLISH, + x500Name, + SubjectPublicKeyInfo.getInstance(pair.public.encoded) + ) + val signer = JcaContentSignerBuilder("SHA1withRSA").build(pair.private) + + return KeySet(JcaX509CertificateConverter().getCertificate(builder.build(signer)), pair.private) + } + + // based on https://gist.github.com/mmuszkow/10288441 + // and https://github.com/fornwall/apksigner/blob/master/src/main/java/net/fornwall/apksigner/ZipSigner.java + @Throws(Exception::class) + fun signApk(apkFile: File) { + val ks = File(apkFile.parent, "ks.keystore").also { keyStore -> + if (!keyStore.exists()) newKeystore(keyStore) + } + + val keyStore = KeyStore.getInstance("BKS", "BC") + ks.inputStream().use { keyStore.load(it, null) } + + val alias = keyStore.aliases().nextElement() + val password = "password".toCharArray() + val keySet = KeySet( + keyStore.getCertificate(alias) as X509Certificate, + keyStore.getKey(alias, password) as PrivateKey + ) + var zip = Zip(apkFile.absolutePath, 6, 'r') + val dig = MessageDigest.getInstance("SHA1") + val digests = LinkedHashMap() + val filesToRemove = ArrayList() + repeat(zip.totalEntries) { i -> + zip.openEntryByIndex(i) + val name = zip.entryName + + if (stripPattern.matches(name)) + filesToRemove.add(name) + else + digests[name] = dig.digest(zip.readEntry()).toBase64() + + zip.closeEntry() + } + zip.close() + zip = Zip(apkFile.absolutePath, 6, 'a') + + filesToRemove.forEach(zip::deleteEntry) + val sectionDigests = LinkedHashMap() + var manifest = Manifest() + var attrs = manifest.mainAttributes + attrs[Attributes.Name.MANIFEST_VERSION] = "1.0" + attrs[Attributes.Name("Created-By")] = "Aliucord Installer" + val digestAttr = Attributes.Name("SHA1-Digest") + + for ((name, value) in digests) { + val attributes = Attributes() + + attributes[digestAttr] = value + manifest.entries[name] = attributes + sectionDigests[name] = hashEntrySection(name, attributes, dig) + } + + ByteArrayOutputStream().use { baos -> + manifest.write(baos) + + zip.openEntry(JarFile.MANIFEST_NAME) + zip.writeEntry(baos.toByteArray(), baos.size().toLong()) + zip.closeEntry() + } + val manifestHash = getManifestHash(manifest, dig) + val tmpManifest = Manifest().apply { + mainAttributes.putAll(attrs) + } + val manifestMainHash = getManifestHash(tmpManifest, dig) + manifest = Manifest() + attrs = manifest.mainAttributes + attrs[Attributes.Name.SIGNATURE_VERSION] = "1.0" + attrs[Attributes.Name("Created-By")] = "Aliucord Installer" + attrs[Attributes.Name("SHA1-Digest-Manifest")] = manifestHash + attrs[Attributes.Name("SHA1-Digest-Manifest-Main-Attributes")] = manifestMainHash + sectionDigests.forEach { (key, value) -> + val attributes = Attributes() + attributes[digestAttr] = value + manifest.entries[key] = attributes + } + var sigBytes: ByteArray + ByteArrayOutputStream().use { sigStream -> + manifest.write(sigStream) + sigBytes = sigStream.toByteArray() + zip.openEntry("META-INF/CERT.SF") + zip.writeEntry(sigBytes, sigStream.size().toLong()) + zip.closeEntry() + } + val signature = signSigFile(keySet, sigBytes) + zip.openEntry("META-INF/CERT.RSA") + zip.writeEntry(signature, signature.size.toLong()) + zip.closeEntry() + zip.close() + } + + @Throws(IOException::class) + private fun hashEntrySection(name: String, attrs: Attributes, dig: MessageDigest): String { + val manifest = Manifest() + manifest.mainAttributes[Attributes.Name.MANIFEST_VERSION] = "1.0" + + ByteArrayOutputStream().use { baos -> + manifest.write(baos) + + val emptyLen = baos.toByteArray().size + manifest.entries[name] = attrs + baos.reset() + manifest.write(baos) + var ob = baos.toByteArray() + ob = ob.copyOfRange(emptyLen, ob.size) + return dig.digest(ob).toBase64() + } + } + + @Throws(IOException::class) + private fun getManifestHash(manifest: Manifest, dig: MessageDigest) = ByteArrayOutputStream().use { baos -> + manifest.write(baos) + dig.digest(baos.toByteArray()).toBase64() + } + + private fun signSigFile(keySet: KeySet, content: ByteArray): ByteArray { + val sha1Signer = JcaContentSignerBuilder("SHA1withRSA").build(keySet.privateKey) + val jcaDigestCalculatorProviderBuilder = JcaDigestCalculatorProviderBuilder() + val jcaSignerInfoGeneratorBuilder = + JcaSignerInfoGeneratorBuilder(jcaDigestCalculatorProviderBuilder.build()).apply { + setDirectSignature(true) + } + val sigData = CMSSignedDataGenerator().apply { + addSignerInfoGenerator(jcaSignerInfoGeneratorBuilder.build(sha1Signer, keySet.publicKey)) + addCertificates(JcaCertStore(listOf(keySet.publicKey))) + }.generate(CMSProcessableByteArray(content), false) + return sigData.toASN1Structure().getEncoded("DER") + } + + private fun ByteArray.toBase64() = String(Base64.encode(this)) +} \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/models/Commit.kt b/app/src/main/kotlin/com/aliucord/manager/models/Commit.kt similarity index 74% rename from app/src/main/java/com/aliucord/manager/models/Commit.kt rename to app/src/main/kotlin/com/aliucord/manager/models/Commit.kt index fef3c64..fbcd2e0 100644 --- a/app/src/main/java/com/aliucord/manager/models/Commit.kt +++ b/app/src/main/kotlin/com/aliucord/manager/models/Commit.kt @@ -13,6 +13,6 @@ data class Commit( val commit: Commit, val author: Author ) { - class Author(@SerializedName("login") val name: String) - class Commit(val message: String) -} + data class Author(@SerializedName("login") val name: String) + data class Commit(val message: String) +} \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/models/GithubUser.kt b/app/src/main/kotlin/com/aliucord/manager/models/GithubUser.kt similarity index 100% rename from app/src/main/java/com/aliucord/manager/models/GithubUser.kt rename to app/src/main/kotlin/com/aliucord/manager/models/GithubUser.kt diff --git a/app/src/main/kotlin/com/aliucord/manager/models/Manifest.kt b/app/src/main/kotlin/com/aliucord/manager/models/Manifest.kt new file mode 100644 index 0000000..ed571c4 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/models/Manifest.kt @@ -0,0 +1,13 @@ +package com.aliucord.manager.models + +data class Manifest( + val name: String, + val authors: ArrayList, + val description: String, + val version: String, + val updateUrl: String, + val changelog: String?, + val changelogMedia: String? +) { + data class Author(val name: String, val id: Long) +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/models/Version.kt b/app/src/main/kotlin/com/aliucord/manager/models/Version.kt new file mode 100644 index 0000000..181f06c --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/models/Version.kt @@ -0,0 +1,7 @@ +package com.aliucord.manager.models + +data class Version( + val versionCode: String, + val versionName: String, + val aliucordHash: String +) \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/preferences/Preferences.kt b/app/src/main/kotlin/com/aliucord/manager/preferences/Preferences.kt similarity index 100% rename from app/src/main/java/com/aliucord/manager/preferences/Preferences.kt rename to app/src/main/kotlin/com/aliucord/manager/preferences/Preferences.kt diff --git a/app/src/main/java/com/aliucord/manager/preferences/PreferencesHolder.kt b/app/src/main/kotlin/com/aliucord/manager/preferences/PreferencesHolder.kt similarity index 100% rename from app/src/main/java/com/aliucord/manager/preferences/PreferencesHolder.kt rename to app/src/main/kotlin/com/aliucord/manager/preferences/PreferencesHolder.kt diff --git a/app/src/main/java/com/aliucord/manager/ui/Screen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/Screen.kt similarity index 96% rename from app/src/main/java/com/aliucord/manager/ui/Screen.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/Screen.kt index c1b55c9..a985bb1 100644 --- a/app/src/main/java/com/aliucord/manager/ui/Screen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/Screen.kt @@ -12,7 +12,7 @@ import com.aliucord.manager.R @ExperimentalFoundationApi sealed class Screen(val route: String, @StringRes val displayName: Int) { companion object { - val screens by lazy { listOf(Home, About, Settings, Commits, Installer) } + val screens by lazy { listOf(Home, About, Settings, Commits, Installer, Store) } } object Home : Screen("home", R.string.app_name) diff --git a/app/src/main/java/com/aliucord/manager/ui/components/AppBar.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/AppBar.kt similarity index 96% rename from app/src/main/java/com/aliucord/manager/ui/components/AppBar.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/AppBar.kt index d8824a1..c0e78cb 100644 --- a/app/src/main/java/com/aliucord/manager/ui/components/AppBar.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/AppBar.kt @@ -44,7 +44,9 @@ fun AppBar(navController: NavController) { IconButton( onClick = { - navController.navigate(Screen.Settings.route) + navController.navigate(Screen.Store.route) { + popUpTo(Screen.Home.route) { saveState = true } + } }, ) { Icon( diff --git a/app/src/main/java/com/aliucord/manager/ui/components/GrantPermission.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/GrantPermission.kt similarity index 92% rename from app/src/main/java/com/aliucord/manager/ui/components/GrantPermission.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/GrantPermission.kt index e7d1d0a..e2341af 100644 --- a/app/src/main/java/com/aliucord/manager/ui/components/GrantPermission.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/GrantPermission.kt @@ -11,7 +11,6 @@ import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign @@ -55,8 +54,8 @@ fun GrantPermission(permissionState: PermissionState) = Column( ) }, colors = ButtonDefaults.buttonColors( - containerColor = Color.LightGray, - contentColor = Color.Black + containerColor = MaterialTheme.colorScheme.secondaryContainer, + contentColor = MaterialTheme.colorScheme.onSecondaryContainer ), ) { Text(stringResource(R.string.open_settings)) diff --git a/app/src/main/java/com/aliucord/manager/ui/components/PluginsList.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/PluginsList.kt similarity index 91% rename from app/src/main/java/com/aliucord/manager/ui/components/PluginsList.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/PluginsList.kt index e313295..00e1f10 100644 --- a/app/src/main/java/com/aliucord/manager/ui/components/PluginsList.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/PluginsList.kt @@ -12,6 +12,7 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.OutlinedTextField import androidx.compose.material.TextFieldDefaults import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Search import androidx.compose.material3.* import androidx.compose.runtime.* @@ -71,7 +72,13 @@ fun PluginsList() = Column( if (showDeleteDialog) AlertDialog( onDismissRequest = { showDeleteDialog = false }, - title = { Text(stringResource(R.string.delete_plugin, plugin.manifest.name)) }, + icon = { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.uninstall_plugin, plugin.manifest.name) + ) + }, + title = { Text(stringResource(R.string.uninstall_plugin, plugin.manifest.name)) }, text = { Text(stringResource(R.string.delete_plugin_body)) }, confirmButton = { Button( @@ -79,13 +86,9 @@ fun PluginsList() = Column( showDeleteDialog = false plugin.file.delete() plugins = plugins.filter { it != plugin } - }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.primaryContainer, - contentColor = MaterialTheme.colorScheme.onPrimaryContainer - ) + } ) { - Text(stringResource(android.R.string.ok).uppercase()) + Text(stringResource(android.R.string.ok)) } }, dismissButton = { @@ -98,7 +101,7 @@ fun PluginsList() = Column( contentColor = MaterialTheme.colorScheme.onSecondaryContainer ) ) { - Text(stringResource(android.R.string.cancel).uppercase()) + Text(stringResource(android.R.string.cancel)) } } ) diff --git a/app/src/main/java/com/aliucord/manager/ui/components/Switch.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/Switch.kt similarity index 84% rename from app/src/main/java/com/aliucord/manager/ui/components/Switch.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/Switch.kt index e93fd2d..11a1789 100644 --- a/app/src/main/java/com/aliucord/manager/ui/components/Switch.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/Switch.kt @@ -25,10 +25,10 @@ fun Switch( enabled = enabled, interactionSource = interactionSource, colors = SwitchDefaults.colors( - checkedTrackColor = colors.surfaceVariant, - uncheckedTrackColor = colors.secondaryContainer, + checkedTrackColor = colors.primary, + uncheckedTrackColor = colors.secondary, checkedThumbColor = colors.primary, - uncheckedThumbColor = colors.onSecondaryContainer + uncheckedThumbColor = colors.secondary ) ) } \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/components/about/ContributorEntry.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorEntry.kt similarity index 100% rename from app/src/main/java/com/aliucord/manager/ui/components/about/ContributorEntry.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorEntry.kt diff --git a/app/src/main/java/com/aliucord/manager/ui/components/about/ContributorsCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorsCard.kt similarity index 53% rename from app/src/main/java/com/aliucord/manager/ui/components/about/ContributorsCard.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorsCard.kt index 44b4ec7..7d56cf9 100644 --- a/app/src/main/java/com/aliucord/manager/ui/components/about/ContributorsCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/ContributorsCard.kt @@ -1,26 +1,24 @@ package com.aliucord.manager.ui.components.about -import android.util.Log import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.lazy.items -import androidx.compose.material3.ElevatedCard -import androidx.compose.material3.ExperimentalMaterial3Api -import androidx.compose.material3.MaterialTheme -import androidx.compose.material3.Text +import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.aliucord.manager.BuildConfig import com.aliucord.manager.R import com.aliucord.manager.models.GithubUser import com.aliucord.manager.utils.Github +import com.aliucord.manager.utils.httpClient +import io.ktor.client.request.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -28,9 +26,9 @@ import kotlinx.coroutines.launch @ExperimentalFoundationApi @Composable fun ContributorsCard() = ElevatedCard( - modifier = Modifier.padding(bottom = 8.dp).wrapContentHeight().fillMaxWidth() + modifier = Modifier.wrapContentHeight().fillMaxWidth() ) { - val contributors = remember { mutableStateOf?>(null) } + val contributors = remember { mutableStateListOf() } Column( modifier = Modifier.padding(16.dp).fillMaxWidth(), @@ -42,22 +40,24 @@ fun ContributorsCard() = ElevatedCard( color = MaterialTheme.colorScheme.primary ) - if (contributors.value != null) LazyVerticalGrid( - cells = GridCells.Adaptive(48.dp), - verticalArrangement = Arrangement.spacedBy(4.dp), - horizontalArrangement = Arrangement.spacedBy(4.dp) - ) { - items(contributors.value!!) { user -> ContributorEntry(user.name) } + if (contributors.isNotEmpty()) { + LazyVerticalGrid( + cells = GridCells.Adaptive(48.dp), + verticalArrangement = Arrangement.spacedBy(4.dp), + horizontalArrangement = Arrangement.spacedBy(4.dp) + ) { + items(contributors) { user -> ContributorEntry(user.name) } + } } else { - Text(stringResource(R.string.loading)) + Box( + modifier = Modifier.fillMaxWidth().padding(12.dp), + contentAlignment = Alignment.Center, + content = { CircularProgressIndicator() } + ) - LaunchedEffect(null) { + LaunchedEffect(Unit) { launch(Dispatchers.IO) { - try { - contributors.value = Github.contributors - } catch (err: Throwable) { - Log.e(BuildConfig.TAG, "Failed to load contributors", err) - } + contributors.addAll(httpClient.get>(Github.contributorsUrl).sortedByDescending { it.contributions }) } } } diff --git a/app/src/main/java/com/aliucord/manager/ui/components/about/TeamCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/TeamCard.kt similarity index 92% rename from app/src/main/java/com/aliucord/manager/ui/components/about/TeamCard.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/about/TeamCard.kt index 0c1189b..3daaa5c 100644 --- a/app/src/main/java/com/aliucord/manager/ui/components/about/TeamCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/TeamCard.kt @@ -14,7 +14,7 @@ import com.aliucord.manager.R @OptIn(ExperimentalMaterial3Api::class) @Composable fun TeamCard() = ElevatedCard( - modifier = Modifier.padding(bottom = 8.dp).wrapContentHeight().fillMaxWidth() + modifier = Modifier.wrapContentHeight().fillMaxWidth() ) { Column( modifier = Modifier.padding(16.dp).fillMaxWidth(), diff --git a/app/src/main/java/com/aliucord/manager/ui/components/about/UserEntry.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/about/UserEntry.kt similarity index 100% rename from app/src/main/java/com/aliucord/manager/ui/components/about/UserEntry.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/about/UserEntry.kt diff --git a/app/src/main/java/com/aliucord/manager/ui/components/installer/InstallerDialog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallerDialog.kt similarity index 100% rename from app/src/main/java/com/aliucord/manager/ui/components/installer/InstallerDialog.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/installer/InstallerDialog.kt diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/Changelog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/Changelog.kt new file mode 100644 index 0000000..2069f48 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/Changelog.kt @@ -0,0 +1,176 @@ +package com.aliucord.manager.ui.components.plugins + +import android.annotation.SuppressLint +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.foundation.text.ClickableText +import androidx.compose.material3.* +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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 coil.compose.rememberImagePainter +import coil.transform.RoundedCornersTransformation +import com.aliucord.manager.R +import com.aliucord.manager.utils.Plugin +import java.util.regex.Pattern + +private val hyperLinkPattern = Pattern.compile("\\[(.+?)]\\((.+?\\))") + +@Suppress("RegExpRedundantEscape") // It is very much not redundant and causes a crash lol +private val headerStylePattern = Pattern.compile("\\{(improved|added|fixed)( marginTop)?\\}") + +@SuppressLint("ComposableNaming") // Can't use MaterialTheme without Composable, but this is not a component +@Composable +private fun AnnotatedString.Builder.hyperlink(content: 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.colorScheme.primary, + textDecoration = TextDecoration.Underline + ) + ) { + pushStringAnnotation(title, url) + append(title) + pop() + } + + idx = end + } + } + + if (idx < content.length) append(content.substring(idx)) +} + +@Composable +fun Changelog( + plugin: Plugin, + onDismiss: () -> Unit +) { + AlertDialog( + onDismissRequest = onDismiss, + icon = { + Icon( + painter = painterResource(R.drawable.ic_history_white_24dp), + contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name), + tint = MaterialTheme.colorScheme.onSurface + ) + }, + title = { Text(plugin.manifest.name) }, + text = { + Column { + plugin.manifest.changelogMedia?.let { mediaUrl -> + Image( + modifier = Modifier.fillMaxWidth().heightIn(max = 90.dp), + painter = rememberImagePainter(mediaUrl) { + transformations(RoundedCornersTransformation(14.dp.value)) + }, + contentDescription = "Changelog Media" + ) + } + + LazyColumn( + modifier = Modifier.wrapContentHeight() + ) { + items(plugin.manifest.changelog!!.lines()) { + var line = it.trim() + + if (line.isNotEmpty()) { + when (line[0]) { + '#' -> { + do { + line = line.substring(1) + } while (line.startsWith("#")) + + Text( + line.trimStart(), + fontWeight = FontWeight.Bold, + modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) + ) + } + '*' -> { + LinkText( + buildAnnotatedString { + withStyle( + style = SpanStyle( + color = MaterialTheme.colorScheme.primary, + fontWeight = FontWeight.Bold + ) + ) { + append("● ") + } + + hyperlink(line.substring(1)) + }, + Modifier.padding(bottom = 2.dp) + ) + } + else -> { + when { + line.endsWith("marginTop}") -> { + val color = MaterialTheme.colorScheme.onSurface + Text( + line, + fontWeight = FontWeight.Bold, + color = color, + modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) + ) + } + line.all { c -> c == '=' } -> {} // Discord ignores ======= + else -> { + LinkText( + buildAnnotatedString { + hyperlink(line) + } + ) + } + } + } + } + } + } + } + } + }, + confirmButton = { + Button( + onClick = onDismiss + ) { + Text(stringResource(R.string.close)) + } + } + ) +} + +@Composable +private fun LinkText(annotatedString: AnnotatedString, modifier: Modifier = Modifier) { + val urlHandler = LocalUriHandler.current + + ClickableText( + text = annotatedString, + style = TextStyle(color = LocalContentColor.current), + onClick = { offset -> + annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let { + urlHandler.openUri(it.item) + } + }, + modifier = modifier + ) +} \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/components/plugins/PluginCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/PluginCard.kt similarity index 81% rename from app/src/main/java/com/aliucord/manager/ui/components/plugins/PluginCard.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/PluginCard.kt index 0203414..3bd82a5 100644 --- a/app/src/main/java/com/aliucord/manager/ui/components/plugins/PluginCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/plugins/PluginCard.kt @@ -1,6 +1,7 @@ package com.aliucord.manager.ui.components.plugins import androidx.compose.foundation.layout.* +import androidx.compose.foundation.selection.toggleable import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete import androidx.compose.material3.* @@ -25,16 +26,21 @@ fun PluginCard( onDelete: () -> Unit, onShowChangelog: () -> Unit ) { - var isEnabled by remember { mutableStateOf(true) } - ElevatedCard( - modifier = Modifier.fillMaxWidth() + modifier = Modifier.fillMaxWidth(), + elevation = CardDefaults.cardElevation(defaultElevation = 2.dp) ) { Column( modifier = Modifier.padding(12.dp) ) { + var isEnabled by remember { mutableStateOf(true) } + // Header - Row { + Row( + modifier = Modifier.toggleable(value = isEnabled) { + isEnabled = it + } + ) { Column { // Name Text( @@ -83,7 +89,10 @@ fun PluginCard( ) } ) { - Icon(painterResource(R.drawable.ic_account_github_white_24dp), "GitHub") + Icon( + painter = painterResource(R.drawable.ic_account_github_white_24dp), + contentDescription = "GitHub", + tint = MaterialTheme.colorScheme.onSurface) } if (plugin.manifest.changelog != null) { @@ -92,7 +101,8 @@ fun PluginCard( ) { Icon( painter = painterResource(R.drawable.ic_history_white_24dp), - contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name) + contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name), + tint = MaterialTheme.colorScheme.onSurface ) } } @@ -102,7 +112,7 @@ fun PluginCard( IconButton(onClick = onDelete) { Icon( imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.delete_plugin, plugin.manifest.name), + contentDescription = stringResource(R.string.uninstall_plugin, plugin.manifest.name), tint = MaterialTheme.colorScheme.error ) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/Setting.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/Setting.kt new file mode 100644 index 0000000..0fc3c1f --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/Setting.kt @@ -0,0 +1,28 @@ +package com.aliucord.manager.ui.components.settings + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier + +@Composable +fun ListItem( + trailing: @Composable () -> Unit +) { + Row( + modifier = Modifier.fillMaxWidth() + ) { + Column { + Text("Title", style = MaterialTheme.typography.labelMedium) + Text("Subtext", style = MaterialTheme.typography.labelSmall) + } + + Spacer(Modifier.weight(1f, true)) + + trailing() + } +} \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/components/settings/SettingSwitch.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/SettingSwitch.kt similarity index 100% rename from app/src/main/java/com/aliucord/manager/ui/components/settings/SettingSwitch.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/settings/SettingSwitch.kt diff --git a/app/src/main/java/com/aliucord/manager/ui/components/settings/ThemeSetting.kt b/app/src/main/kotlin/com/aliucord/manager/ui/components/settings/ThemeSetting.kt similarity index 100% rename from app/src/main/java/com/aliucord/manager/ui/components/settings/ThemeSetting.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/components/settings/ThemeSetting.kt diff --git a/app/src/main/java/com/aliucord/manager/ui/screens/AboutScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/AboutScreen.kt similarity index 78% rename from app/src/main/java/com/aliucord/manager/ui/screens/AboutScreen.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/screens/AboutScreen.kt index 6074550..e51f170 100644 --- a/app/src/main/java/com/aliucord/manager/ui/screens/AboutScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/AboutScreen.kt @@ -1,10 +1,7 @@ package com.aliucord.manager.ui.screens import androidx.compose.foundation.ExperimentalFoundationApi -import androidx.compose.foundation.layout.Column -import androidx.compose.foundation.layout.Spacer -import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.* import androidx.compose.material3.Button import androidx.compose.material3.Icon import androidx.compose.material3.Text @@ -23,7 +20,9 @@ import com.aliucord.manager.ui.components.about.TeamCard fun AboutScreen() { val uriHandler = LocalUriHandler.current - Column { + Column( + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { TeamCard() ContributorsCard() @@ -35,9 +34,9 @@ fun AboutScreen() { ) { Icon( painter = painterResource(R.drawable.ic_account_github_white_24dp), - contentDescription = "GitHub", - modifier = Modifier.padding(8.dp) + contentDescription = "GitHub" ) + Spacer(Modifier.padding(4.dp)) Text(stringResource(R.string.source_code)) } } diff --git a/app/src/main/java/com/aliucord/manager/ui/screens/CommitsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/CommitsScreen.kt similarity index 80% rename from app/src/main/java/com/aliucord/manager/ui/screens/CommitsScreen.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/screens/CommitsScreen.kt index bfe6e7e..df76c11 100644 --- a/app/src/main/java/com/aliucord/manager/ui/screens/CommitsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/CommitsScreen.kt @@ -1,6 +1,7 @@ package com.aliucord.manager.ui.screens import android.util.Log +import androidx.annotation.Keep import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn @@ -10,7 +11,10 @@ import androidx.compose.material.ListItem import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color @@ -22,7 +26,8 @@ import com.aliucord.manager.utils.Github import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch -data class CommitData( +@Keep +private data class CommitData( val commit: Commit, val buildSha: String? ) @@ -31,38 +36,35 @@ data class CommitData( @ExperimentalMaterialApi @Composable fun CommitsScreen() { - var isLoading by remember { mutableStateOf(true) } val commits = remember { mutableStateListOf() } Column( modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - if (isLoading) { + if (commits.isEmpty()) { Box( modifier = Modifier.fillMaxSize().padding(12.dp), contentAlignment = Alignment.Center, content = { CircularProgressIndicator() } ) - LaunchedEffect(null) { + LaunchedEffect(Unit) { launch(Dispatchers.IO) { try { val buildCommits = Github.getCommits( - mapOf( - "sha" to "builds", - "path" to "Aliucord.dex", - "per_page" to "50" - ) + "sha" to "builds", + "path" to "Aliucord.dex", + "per_page" to "50" ) - val commits2 = Github.getCommits(mapOf("per_page" to "50")) + val commits2 = Github.getCommits("per_page" to "50") + commits.addAll(commits2.map { c -> CommitData( - c, - buildCommits.find { bc -> bc.commit.message.substring(6) == c.sha }?.sha + commit = c, + buildSha = buildCommits.find { bc -> bc.commit.message.substring(6) == c.sha }?.sha ) }) - isLoading = false } catch (e: Throwable) { Log.e(BuildConfig.TAG, "Failed to get commits", e) } diff --git a/app/src/main/java/com/aliucord/manager/ui/screens/HomeScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/HomeScreen.kt similarity index 74% rename from app/src/main/java/com/aliucord/manager/ui/screens/HomeScreen.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/screens/HomeScreen.kt index 8c2c170..d615b4f 100644 --- a/app/src/main/java/com/aliucord/manager/ui/screens/HomeScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/HomeScreen.kt @@ -5,18 +5,15 @@ package com.aliucord.manager.ui.screens -import android.Manifest -import android.util.Log +import android.content.Intent +import android.net.Uri +import android.widget.Toast import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.* import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete -import androidx.compose.material.icons.filled.ExitToApp import androidx.compose.material3.* -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember +import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalContext @@ -28,16 +25,17 @@ import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.withStyle import androidx.compose.ui.unit.dp import androidx.navigation.NavController -import com.aliucord.manager.BuildConfig import com.aliucord.manager.R +import com.aliucord.manager.models.Version import com.aliucord.manager.preferences.devModePreference import com.aliucord.manager.ui.Screen import com.aliucord.manager.ui.components.PluginsList import com.aliucord.manager.ui.components.installer.InstallerDialog import com.aliucord.manager.utils.Github -import com.aliucord.manager.utils.Versions +import com.aliucord.manager.utils.gson +import com.aliucord.manager.utils.httpClient import com.google.accompanist.permissions.ExperimentalPermissionsApi -import com.google.accompanist.permissions.rememberPermissionState +import io.ktor.client.request.* import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch @@ -46,10 +44,8 @@ import kotlinx.coroutines.launch @ExperimentalFoundationApi @Composable fun HomeScreen(navController: NavController) { - val storagePermissionState = rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) - val packageManager = LocalContext.current.packageManager - val versions = remember { mutableStateOf(null) } + var supportedVersion by remember { mutableStateOf(null) } val installedVersion = remember { try { packageManager.getPackageInfo("com.aliucord", 0).versionName @@ -60,12 +56,15 @@ fun HomeScreen(navController: NavController) { val showOptionsDialog = remember { mutableStateOf(false) } - if (versions.value == null) LaunchedEffect(Unit) { + if (supportedVersion == null) LaunchedEffect(Unit) { launch(Dispatchers.IO) { - try { - versions.value = Github.versions - } catch (th: Throwable) { - Log.e(BuildConfig.TAG, "Failed to get versions", th) + val version = gson.fromJson(httpClient.get(Github.dataUrl), Version::class.java) + + supportedVersion = "${version.versionName} - " + when (version.versionCode[3].toString()) { + "0" -> "Stable" + "1" -> "Beta" + "2" -> "Alpha" + else -> throw NoWhenBranchMatchedException() } } } @@ -93,7 +92,7 @@ fun HomeScreen(navController: NavController) { append("Supported version: ") withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(versions.value?.versionName ?: "?") + append(supportedVersion ?: "?") } append("\nInstalled version: ") @@ -104,11 +103,9 @@ fun HomeScreen(navController: NavController) { }) } - val (drawable, description) = when { - installedVersion == "-" -> R.drawable.ic_download_24dp to R.string.install - installedVersion.startsWith( - versions.value?.versionName ?: "uwu" - ) -> R.drawable.ic_reinstall_24dp to R.string.reinstall + val (drawable, description) = when (installedVersion) { + "-" -> R.drawable.ic_download_24dp to R.string.install + supportedVersion -> R.drawable.ic_reinstall_24dp to R.string.reinstall else -> R.drawable.ic_update_24dp to R.string.update } @@ -116,29 +113,6 @@ fun HomeScreen(navController: NavController) { modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp) ) { -// Row( -// modifier = Modifier.fillMaxWidth(), -// horizontalArrangement = Arrangement.spacedBy(12.dp) -// ) { -// -// -// Button( -// onClick = { -// navController.navigate(Screen.Commits.route) { -// popUpTo(Screen.Home.route) { saveState = true } -// } -// }, -// colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer) -// ) { -// Icon( -// painter = painterResource(R.drawable.ic_update_24dp), -// contentDescription = "Commits", -// tint = MaterialTheme.colorScheme.onSecondaryContainer, -// modifier = Modifier.padding(8.dp) -// ) -// } -// } - Button( modifier = Modifier.fillMaxWidth(), onClick = { @@ -166,12 +140,18 @@ fun HomeScreen(navController: NavController) { modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { -// if (installedVersion != "-") { - if (true) { + if (installedVersion != "-") { + val context = LocalContext.current + Button( modifier = Modifier.weight(1f), colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer), - onClick = { } + onClick = { + val packageURI = Uri.parse("package:com.aliucord") + val uninstallIntent = Intent(Intent.ACTION_DELETE, packageURI) + + context.startActivity(uninstallIntent) + } ) { Icon( modifier = Modifier.padding(8.dp), @@ -183,12 +163,17 @@ fun HomeScreen(navController: NavController) { Button( modifier = Modifier.weight(1f), - onClick = { }, + onClick = { + packageManager.getLaunchIntentForPackage("com.aliucord")?.let { + context.startActivity(it) + } ?: Toast.makeText(context, "Failed to launch Aliucord", Toast.LENGTH_LONG) + .show() + }, colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer) ) { Icon( modifier = Modifier.padding(8.dp), - imageVector = Icons.Default.ExitToApp, + painter = painterResource(R.drawable.ic_launch_24dp), contentDescription = "Launch", tint = MaterialTheme.colorScheme.onSecondaryContainer ) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt new file mode 100644 index 0000000..0438fea --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/InstallScreen.kt @@ -0,0 +1,226 @@ +package com.aliucord.manager.ui.screens + +import android.content.Intent +import android.os.Environment +import androidx.compose.foundation.ExperimentalFoundationApi +import androidx.compose.foundation.layout.* +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.unit.dp +import androidx.core.content.FileProvider +import androidx.navigation.NavController +import com.aliucord.libzip.Zip +import com.aliucord.manager.BuildConfig +import com.aliucord.manager.Signer +import com.aliucord.manager.preferences.replaceBg +import com.aliucord.manager.ui.Screen +import com.aliucord.manager.utils.DownloadUtils +import com.aliucord.manager.utils.Github +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch +import java.io.File +import java.io.FileNotFoundException +import java.io.InputStream + +@OptIn(ExperimentalFoundationApi::class) +@Composable +fun InstallerScreen(navController: NavController) { + var log by remember { mutableStateOf("") } + val context = LocalContext.current + + LaunchedEffect(Unit) { + launch(Dispatchers.IO) { + val supportedVersion = Github.version.versionCode + + log += "Checking for cached APK...\n" + + val discordApk = File(context.externalCacheDir, "discord-${supportedVersion}.apk").also { file -> + if (file.exists()) return@also + + val archiveInfo = context.packageManager.getPackageArchiveInfo(file.path, 0) + + if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also + + log += "Downloading APK...\n" + + DownloadUtils.downloadDiscordApk(context, supportedVersion) + + log += "Finished downloading APK\n" + } + + val manifest = File(context.externalCacheDir, "AndroidManifest.xml").also { file -> + if (file.exists()) return@also + + log += "Downloading patched AndroidManifest.xml...\n" + + DownloadUtils.downloadManifest(context) + + log += "Finished downloading manifest\n" + } + + val injector = File(context.externalCacheDir, "Injector.dex").also { file -> + if (file.exists()) return@also + + log += "Downloading injector...\n" + + DownloadUtils.downloadInjector(context) + + log += "Finished downloading injector\n" + } + + log += "Copying original APK ()\n" + + + val outputDir = File(Environment.getExternalStorageDirectory(), "Aliucord").also { + if (!it.exists() && !it.mkdirs()) throw FileNotFoundException() + } + + val outputApk = File(outputDir, "Aliucord.apk") + discordApk.copyTo(outputApk, true) + + log += "Repacking APK\n" + + var patched = false + + with(Zip(outputApk.absolutePath, 6, 'r')) { + repeat(totalEntries) { index -> + openEntryByIndex(index) + val name = entryName + if (name == "classes5.dex") patched = true + closeEntry() + } + + val cacheDir = context.cacheDir.absolutePath + + if (!patched) (1..3).forEach { i -> + openEntry("classes" + (if (i == 1) "" else i) + ".dex") + extractEntry(cacheDir + "/classes" + (i + 1) + ".dex") + closeEntry() + } + + close() + } + + val assetManager = context.assets + + with(Zip(outputApk.absolutePath, 6, 'a')) { + + if (!patched) { + (1..3).forEach { i -> deleteEntry("classes${if (i == 1) "" else i}.dex") } + } else { + deleteEntry("classes.dex") + deleteEntry("classes5.dex") + deleteEntry("classes6.dex") + } + + deleteEntry("AndroidManifest.xml") + deleteEntry("lib/arm64-v8a/libpine.so") + deleteEntry("lib/armeabi-v7a/libpine.so") + + if (!patched) for (i in 2..4) { + val name = "classes$i.dex" + val cacheFile = File(context.cacheDir, name) + + openEntry(name) + compressFile(cacheFile.absolutePath) + closeEntry() + cacheFile.delete() + } + + openEntry("classes.dex") + compressFile(injector.absolutePath) + closeEntry() + + writeEntry("classes5.dex", assetManager.open("pine/classes.dex")) + + openEntry("AndroidManifest.xml") + compressFile(manifest.absolutePath) + closeEntry() + + writeEntry("lib/arm64-v8a/libpine.so", assetManager.open("pine/arm64-v8a/libpine.so")) + writeEntry("lib/armeabi-v7a/libpine.so", assetManager.open("pine/armeabi-v7a/libpine.so")) + + writeEntry("classes6.dex", assetManager.open("kotlin/classes.dex")) + + close() + } + + if (replaceBg.get()) { + log += "Replacing icon background\n" + + val icon1Bytes = assetManager.open("icon1.png").readBytes() + val icon2Bytes = assetManager.open("icon2.png").readBytes() + + // use androguard to figure out entries + // androguard arsc resources.arsc --id 0x7f0f0000 (icon1) + // androguard arsc resources.arsc --id 0x7f0f0002 and androguard arsc resources.arsc --id 0x7f0f0006 (icon2) + val icon1Entries = listOf("MbV.png", "kbF.png", "_eu.png", "EtS.png") + val icon2Entries = listOf("_h_.png", "9MB.png", "Dy7.png", "kC0.png", "oEH.png", "RG0.png", "ud_.png", "W_3.png") + + with(Zip(outputApk.absolutePath, 0, 'a')) { + icon1Entries.forEach { entry -> deleteEntry("res/$entry") } + icon2Entries.forEach { entry -> deleteEntry("res/$entry") } + + icon1Entries.forEach { entry -> writeEntry("res/$entry", icon1Bytes) } + icon2Entries.forEach { entry -> writeEntry("res/$entry", icon2Bytes) } + + close() + } + } + + log += "Signing APK\n" + + Signer.newKeystore(File(context.filesDir, "ks.keystore")) + Signer.signApk(outputApk) + + log += "Signed APK\n" + + val intent = Intent(Intent.ACTION_VIEW).apply { + setDataAndType( + FileProvider.getUriForFile(context, "${BuildConfig.APPLICATION_ID}.provider", outputApk), + "application/vnd.android.package-archive" + ) + + addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION) + } + + context.startActivity(intent) + + launch(Dispatchers.Main) { + navController.navigate(Screen.Home.route) + } + } + } + + Column( + modifier = Modifier.fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text(log) + + Spacer(Modifier.weight(1f, true)) + + LinearProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .height(8.dp), + progress = 0.5f + ) + } +} + +private fun Zip.writeEntry(entryName: String, stream: InputStream) { + openEntry(entryName) + stream.readBytes().let { bytes -> writeEntry(bytes, bytes.size.toLong()) } + closeEntry() +} + +private fun Zip.writeEntry(entryName: String, bytes: ByteArray) { + openEntry(entryName) + writeEntry(bytes, bytes.size.toLong()) + closeEntry() +} \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/screens/SettingsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt similarity index 96% rename from app/src/main/java/com/aliucord/manager/ui/screens/SettingsScreen.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt index 31206e2..80d29c6 100644 --- a/app/src/main/java/com/aliucord/manager/ui/screens/SettingsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/SettingsScreen.kt @@ -77,8 +77,12 @@ fun SettingsScreen() = Column( themePref.set(selected.ordinal) } ) { + Icon( + imageVector = Icons.Default.Check, + contentDescription = "Confirm" + ) + Spacer(Modifier.padding(4.dp)) Text("Apply") - Icon(Icons.Default.Check, "Confirm") } } ) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screens/StoreScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screens/StoreScreen.kt new file mode 100644 index 0000000..5c93f54 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screens/StoreScreen.kt @@ -0,0 +1,66 @@ +package com.aliucord.manager.ui.screens + +import androidx.annotation.Keep +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.items +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.aliucord.manager.utils.httpClient +import io.ktor.client.request.* +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch + +@Keep +private data class Tree( + val tree: List +) { + @Keep + data class File( + val mode: String, + val sha: String + ) +} + +@Composable +fun StoreScreen() { + val plugins = remember { mutableStateListOf() } + + Column { + if (plugins.isEmpty()) { + Box( + modifier = Modifier.fillMaxSize().padding(12.dp), + contentAlignment = Alignment.Center, + content = { CircularProgressIndicator() } + ) + + LaunchedEffect(Unit) { + launch(Dispatchers.IO) { + val content = + httpClient.get("https://api.github.com/repos/DiamondMiner88/AllACPlugins/git/trees/master") { + header("accept", "application/json") + } + + plugins.addAll(content.tree.filter { it.mode == "160000" }) + } + } + } else { + LazyColumn { + items(plugins) { item -> + Text("SHA: ${item.sha}", style = MaterialTheme.typography.labelSmall) + } + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/theme/Color.kt b/app/src/main/kotlin/com/aliucord/manager/ui/theme/Color.kt new file mode 100644 index 0000000..98e6bbf --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/theme/Color.kt @@ -0,0 +1,20 @@ +/* + * Copyright (c) 2021 Juby210 + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.manager.ui.theme + +import androidx.compose.ui.graphics.Color + +val darkBackground = Color(0xff1a1a1a) + +val primaryColor = Color(0xff00c853) +val primaryColorLight = Color(0xff5efc82) +val primaryColorDark = Color(0xff009624) + +val discordBrand = Color(0xff5865F2) +val discordGreen = Color(0xff57F287) +val discordRed = Color(0xffED4245) + +val errorColor = discordRed \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/ui/theme/Theme.kt b/app/src/main/kotlin/com/aliucord/manager/ui/theme/Theme.kt similarity index 73% rename from app/src/main/java/com/aliucord/manager/ui/theme/Theme.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/theme/Theme.kt index 7941fb3..ede3048 100644 --- a/app/src/main/java/com/aliucord/manager/ui/theme/Theme.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/theme/Theme.kt @@ -14,21 +14,23 @@ import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import com.google.accompanist.systemuicontroller.rememberSystemUiController -private val DarkColorScheme = darkColorScheme( - -) - -private val LightColorScheme = lightColorScheme( - -) - -private val BlackColorPalette = darkColorScheme( - primary = Color.Black, - onPrimary = Color.White, +private val LightColorScheme = lightColorScheme() +private val DarkColorScheme = darkColorScheme() +private val BlackColorScheme = darkColorScheme( + background = Color.Black, + surface = Color.Black, + primary = Color.LightGray, + onPrimary = Color.DarkGray, + secondary = Color.Gray, + onSecondary = Color.LightGray, + secondaryContainer = Color.DarkGray, + onSecondaryContainer = Color.White, + outline = Color.LightGray ) @Composable fun ManagerTheme( + isBlack: Boolean = false, isDarkTheme: Boolean = isSystemInDarkTheme(), isDynamicColor: Boolean = true, content: @Composable () -> Unit @@ -37,7 +39,7 @@ fun ManagerTheme( val colorScheme = when { dynamicColor && isDarkTheme -> dynamicDarkColorScheme(LocalContext.current) dynamicColor && !isDarkTheme -> dynamicLightColorScheme(LocalContext.current) - isDarkTheme -> DarkColorScheme + isDarkTheme -> if (isBlack) BlackColorScheme else DarkColorScheme else -> LightColorScheme } diff --git a/app/src/main/java/com/aliucord/manager/ui/theme/Type.kt b/app/src/main/kotlin/com/aliucord/manager/ui/theme/Type.kt similarity index 100% rename from app/src/main/java/com/aliucord/manager/ui/theme/Type.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/theme/Type.kt diff --git a/app/src/main/java/com/aliucord/manager/utils/Constants.kt b/app/src/main/kotlin/com/aliucord/manager/utils/Constants.kt similarity index 75% rename from app/src/main/java/com/aliucord/manager/utils/Constants.kt rename to app/src/main/kotlin/com/aliucord/manager/utils/Constants.kt index c1ffe16..3abe5bf 100644 --- a/app/src/main/java/com/aliucord/manager/utils/Constants.kt +++ b/app/src/main/kotlin/com/aliucord/manager/utils/Constants.kt @@ -4,9 +4,12 @@ import android.os.Environment import com.google.gson.Gson import io.ktor.client.* import io.ktor.client.engine.android.* +import io.ktor.client.features.json.* import java.io.File -val httpClient = HttpClient(Android) +val httpClient = HttpClient(Android) { + install(JsonFeature) +} 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/kotlin/com/aliucord/manager/utils/DownloadUtils.kt b/app/src/main/kotlin/com/aliucord/manager/utils/DownloadUtils.kt new file mode 100644 index 0000000..855d00b --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/utils/DownloadUtils.kt @@ -0,0 +1,52 @@ +package com.aliucord.manager.utils + +import android.app.DownloadManager +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import androidx.core.net.toUri +import java.io.File +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine + +object DownloadUtils { + private const val backendHost = "https://aliucord.com/" + + suspend fun downloadDiscordApk(ctx: Context, version: String) = + download(ctx, "$backendHost/download/discord?v=$version", "discord-$version.apk") + + suspend fun downloadManifest(ctx: Context) = + download(ctx, Github.getDownloadUrl("builds", "AndroidManifest.xml"), "AndroidManifest.xml") + + suspend fun downloadInjector(ctx: Context) = + download(ctx, Github.getDownloadUrl("builds", "Injector.dex"), "Injector.dex") + + private suspend fun download(ctx: Context, url: String, fileName: String): File { + val downloadManager = ctx.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager + + return suspendCoroutine { continuation -> + val receiver = object : BroadcastReceiver() { + var downloadId = 0L + + override fun onReceive(context: Context, intent: Intent) { + val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1) + + if (downloadId == id) { + ctx.unregisterReceiver(this) + continuation.resume(context.filesDir) + } + } + } + + ctx.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE)) + + receiver.downloadId = DownloadManager.Request(url.toUri()).run { +// Users could cancel downloads, guhhhh + setNotificationVisibility(DownloadManager.Request.VISIBILITY_VISIBLE) + setDestinationUri(File(ctx.externalCacheDir, fileName).toUri()) + downloadManager.enqueue(this) + } + } + } +} \ No newline at end of file diff --git a/app/src/main/kotlin/com/aliucord/manager/utils/Github.kt b/app/src/main/kotlin/com/aliucord/manager/utils/Github.kt new file mode 100644 index 0000000..744d679 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/utils/Github.kt @@ -0,0 +1,48 @@ +/* + * Copyright (c) 2021 Juby210 + * Licensed under the Open Software License version 3.0 + */ + +package com.aliucord.manager.utils + +import com.aliucord.manager.models.Commit +import com.aliucord.manager.models.Version +import io.ktor.client.request.* +import java.io.InputStreamReader +import java.net.URL +import kotlin.collections.set + +object Github { + private const val org = "Aliucord" + private const val repo = "Aliucord" + + private const val commitsUrl = "https://api.github.com/repos/$org/$repo/commits" + const val contributorsUrl = "https://api.github.com/repos/$org/$repo/contributors" + const val dataUrl = "https://raw.githubusercontent.com/$org/$repo/builds/data.json" + + fun checkForUpdates() { + // TODO + } + + val version: Version by lazy { + gson.fromJson(InputStreamReader(URL(dataUrl).openStream()), Version::class.java) + } + + private val commitsCache = HashMap>() + + suspend fun getCommits(vararg params: Pair): Array { + val query = params.joinToString("&", "?") { it.first + "=" + it.second } + + if (commitsCache.containsKey(query)) return commitsCache[query] ?: emptyArray() + + val res = httpClient.get>(commitsUrl) { + params.forEach { (key, value) -> parameter(key, value) } + } + + commitsCache[query] = res + + return res + } + + fun getDownloadUrl(ref: String, file: String) = "https://raw.githubusercontent.com/$org/$repo/$ref/$file" +} \ No newline at end of file diff --git a/app/src/main/java/com/aliucord/manager/utils/Plugin.kt b/app/src/main/kotlin/com/aliucord/manager/utils/Plugin.kt similarity index 76% rename from app/src/main/java/com/aliucord/manager/utils/Plugin.kt rename to app/src/main/kotlin/com/aliucord/manager/utils/Plugin.kt index cedba47..9a9d13d 100644 --- a/app/src/main/java/com/aliucord/manager/utils/Plugin.kt +++ b/app/src/main/kotlin/com/aliucord/manager/utils/Plugin.kt @@ -2,31 +2,20 @@ package com.aliucord.manager.utils import android.util.Log import com.aliucord.manager.BuildConfig +import com.aliucord.manager.models.Manifest 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: ArrayList, - val description: String, - val version: String, - val updateUrl: 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") + ZipFile(file).use { zipFile -> + val entry = zipFile.getEntry("manifest.json") ?: throw ZipException("Plugin ${file.nameWithoutExtension} has no manifest.") - it.getInputStream(entry).use { zis -> + zipFile.getInputStream(entry).use { zis -> manifest = gson.fromJson(InputStreamReader(zis), Manifest::class.java) } } diff --git a/app/src/main/res/drawable/ic_launch_24dp.xml b/app/src/main/res/drawable/ic_launch_24dp.xml new file mode 100644 index 0000000..0f16d33 --- /dev/null +++ b/app/src/main/res/drawable/ic_launch_24dp.xml @@ -0,0 +1,4 @@ + + + + diff --git a/app/src/main/res/drawable/ic_launcher_monochrome.xml b/app/src/main/res/drawable/ic_launcher_monochrome.xml new file mode 100644 index 0000000..b25c33f --- /dev/null +++ b/app/src/main/res/drawable/ic_launcher_monochrome.xml @@ -0,0 +1,61 @@ + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml index 7353dbd..1413a31 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml index 7353dbd..1413a31 100644 --- a/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml +++ b/app/src/main/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -2,4 +2,5 @@ + \ No newline at end of file diff --git a/app/src/main/res/values/colors.xml b/app/src/main/res/values/colors.xml index dbdc918..5e4598f 100644 --- a/app/src/main/res/values/colors.xml +++ b/app/src/main/res/values/colors.xml @@ -1,4 +1,5 @@ #00c853 - + #FFFFFFFF + \ No newline at end of file diff --git a/app/src/main/res/values/ic_launcher_background.xml b/app/src/main/res/values/ic_launcher_background.xml deleted file mode 100644 index a5b623a..0000000 --- a/app/src/main/res/values/ic_launcher_background.xml +++ /dev/null @@ -1,4 +0,0 @@ - - - #FFFFFFFF - \ 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 1c30b45..806fd99 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -15,7 +15,7 @@ Launch Plugins Page %1$s v%2$s By %1$s - Delete %1$s + Uninstall %1$s Search View changelog for %1$s Are you sure you want to uninstall this plugin? This action cannot be undone. diff --git a/app/src/main/res/values/themes.xml b/app/src/main/res/values/themes.xml index 89d42b9..884dc47 100644 --- a/app/src/main/res/values/themes.xml +++ b/app/src/main/res/values/themes.xml @@ -1,6 +1,6 @@ - - + \ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts index 2107f69..cc1bdf7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,8 +9,9 @@ buildscript { google() mavenCentral() } + dependencies { - classpath("com.android.tools.build:gradle:7.1.1") + classpath("com.android.tools.build:gradle:7.1.2") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10") // NOTE: Do not place your application dependencies here; they belong @@ -18,6 +19,14 @@ buildscript { } } +allprojects { + repositories { + google() + mavenCentral() + maven(url = "https://jitpack.io") + } +} + tasks.register("clean") { delete(rootProject.buildDir) } \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 07c94aa..0dc41c9 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -3,13 +3,5 @@ * Licensed under the Open Software License version 3.0 */ -dependencyResolutionManagement { - repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) - repositories { - google() - mavenCentral() - } -} - rootProject.name = "Aliucord Manager" include(":app") \ No newline at end of file