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