Finished installer functionality

This commit is contained in:
litleck 2022-02-24 18:07:21 -05:00
parent f0cd29c500
commit 5e1a71d4d4
67 changed files with 1486 additions and 655 deletions

View file

@ -53,14 +53,14 @@ dependencies {
// core dependencies // core dependencies
implementation("androidx.appcompat:appcompat:1.4.1") implementation("androidx.appcompat:appcompat:1.4.1")
implementation("androidx.core:core-ktx:1.7.0") 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 // compose dependencies
implementation("androidx.activity:activity-compose:1.4.0") implementation("androidx.activity:activity-compose:1.4.0")
implementation("androidx.compose.ui:ui:${composeVersion}") 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.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") implementation("androidx.navigation:navigation-compose:2.4.1")
// accompanist dependencies // accompanist dependencies
@ -70,10 +70,11 @@ dependencies {
// other dependencies // other dependencies
implementation("com.google.code.gson:gson:2.8.9") implementation("com.google.code.gson:gson:2.8.9")
implementation("io.coil-kt:coil-compose:1.4.0") implementation("io.coil-kt:coil-compose:1.4.0")
implementation("androidx.datastore:datastore-preferences:1.0.0")
// ktor // ktor
implementation("io.ktor:ktor-client-core:$ktorVersion") 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-android:$ktorVersion")
implementation("io.ktor:ktor-client-gson:$ktorVersion")
implementation("org.bouncycastle:bcpkix-jdk15on:1.65")
} }

View file

@ -15,5 +15,6 @@
# Uncomment this to preserve the line number information for # Uncomment this to preserve the line number information for
# debugging stack traces. # debugging stack traces.
-keepattributes SourceFile,LineNumberTable -keepattributes SourceFile,LineNumberTable
-renamesourcefileattribute SourceFile
-keep class com.aliucord.manager.models.** { private final *; } -keep class com.aliucord.manager.models.** { private final *; }

View file

@ -3,16 +3,25 @@
xmlns:tools="http://schemas.android.com/tools" xmlns:tools="http://schemas.android.com/tools"
package="com.aliucord.manager"> package="com.aliucord.manager">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.MANAGE_EXTERNAL_STORAGE" tools:ignore="ScopedStorage" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission
android:name="android.permission.MANAGE_EXTERNAL_STORAGE"
tools:ignore="ScopedStorage" />
<uses-permission
android:name="android.permission.REQUEST_DELETE_PACKAGES"
tools:ignore="ProtectedPermissions" />
<uses-permission
android:name="android.permission.DELETE_PACKAGES"
tools:ignore="ProtectedPermissions" />
<queries>
<package android:name="com.aliucord" />
</queries>
<application <application
android:testOnly="false"
android:requestLegacyExternalStorage="true"
android:allowBackup="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
@ -30,7 +39,7 @@
</provider> </provider>
<activity <activity
android:name=".ui.MainActivity" android:name=".MainActivity"
android:exported="true"> android:exported="true">
<intent-filter> <intent-filter>
<action android:name="android.intent.action.MAIN" /> <action android:name="android.intent.action.MAIN" />

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

View file

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

Binary file not shown.

View file

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

View file

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

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

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

View file

@ -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<Preferences> by preferencesDataStore("settings")
}
init {
}
}

View file

@ -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<ImageBitmap?>(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
)
}

View file

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

View file

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

View file

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

View file

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

View file

@ -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<String, Array<Commit>>()
private val commitsType = object : TypeToken<Array<Commit>>() {}.type
private val contributorsType = object : TypeToken<Array<GithubUser>>() {}.type
fun getCommits(params: Map<String, String>?): Array<Commit> {
// 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<Array<Commit>>(it, commitsType)
}
commitsCache[query] = res
return res
}
val contributors: Array<GithubUser> by lazy() {
InputStreamReader(URL(contributorsUrl).openStream()).use { stream ->
gson.fromJson<Array<GithubUser>>(stream, contributorsType).apply {
sortByDescending { it.contributions }
}
}
}
fun getDownloadUrl(ref: String, file: String) = "https://raw.githubusercontent.com/${org}/${repo}/${ref}/${file}"
}

View file

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

View file

@ -3,14 +3,20 @@
* Licensed under the Open Software License version 3.0 * Licensed under the Open Software License version 3.0
*/ */
package com.aliucord.manager.ui package com.aliucord.manager
import android.Manifest import android.Manifest
import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.provider.Settings
import androidx.activity.ComponentActivity import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material.ExperimentalMaterialApi import androidx.compose.material.ExperimentalMaterialApi
import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.ExperimentalMaterial3Api
@ -18,15 +24,21 @@ import androidx.compose.material3.Scaffold
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.core.net.toUri
import androidx.navigation.compose.NavHost import androidx.navigation.compose.NavHost
import androidx.navigation.compose.composable import androidx.navigation.compose.composable
import androidx.navigation.compose.rememberNavController import androidx.navigation.compose.rememberNavController
import com.aliucord.manager.preferences.Preferences
import com.aliucord.manager.preferences.sharedPreferences 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.AppBar
import com.aliucord.manager.ui.components.GrantPermission import com.aliucord.manager.ui.components.GrantPermission
import com.aliucord.manager.ui.screens.* import com.aliucord.manager.ui.screens.*
import com.aliucord.manager.ui.theme.ManagerTheme import com.aliucord.manager.ui.theme.ManagerTheme
import com.aliucord.manager.ui.theme.Theme
import com.aliucord.manager.utils.Github import com.aliucord.manager.utils.Github
import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.ExperimentalPermissionsApi
import com.google.accompanist.permissions.PermissionRequired import com.google.accompanist.permissions.PermissionRequired
@ -44,7 +56,14 @@ class MainActivity : ComponentActivity() {
Github.checkForUpdates() Github.checkForUpdates()
setContent { setContent {
ManagerTheme { ManagerTheme(
isBlack = Preferences.useBlack.get(),
isDarkTheme = run {
val theme = Theme.from(themePref.get())
theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK
}
) {
MainActivityLayout() MainActivityLayout()
} }
} }
@ -57,11 +76,19 @@ class MainActivity : ComponentActivity() {
@ExperimentalPermissionsApi @ExperimentalPermissionsApi
@Composable @Composable
private fun MainActivityLayout() { private fun MainActivityLayout() {
val storagePermissionState = rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) val storagePermissionState = rememberPermissionState(Manifest.permission.READ_EXTERNAL_STORAGE)
val navController = rememberNavController() val navController = rememberNavController()
val requestAllFilesAccess = Build.VERSION.SDK_INT >= Build.VERSION_CODES.R
val context = LocalContext.current
if (!storagePermissionState.hasPermission) SideEffect { SideEffect {
storagePermissionState.launchPermissionRequest() @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( Scaffold(
@ -69,8 +96,8 @@ private fun MainActivityLayout() {
) { paddingValues -> ) { paddingValues ->
PermissionRequired( PermissionRequired(
permissionState = storagePermissionState, permissionState = storagePermissionState,
permissionNotGrantedContent = { GrantPermission(storagePermissionState) }, permissionNotAvailableContent = { GrantPermission(storagePermissionState) },
permissionNotAvailableContent = { GrantPermission(storagePermissionState) } permissionNotGrantedContent = { GrantPermission(storagePermissionState) }
) { ) {
NavHost( NavHost(
navController, navController,
@ -82,19 +109,8 @@ private fun MainActivityLayout() {
composable(Screen.About.route) { AboutScreen() } composable(Screen.About.route) { AboutScreen() }
composable(Screen.Settings.route) { SettingsScreen() } composable(Screen.Settings.route) { SettingsScreen() }
composable(Screen.Store.route) { StoreScreen() } composable(Screen.Store.route) { StoreScreen() }
composable( composable(Screen.Installer.route) { InstallerScreen(navController) }
route = Screen.Installer.route,
// arguments = listOf(
// navArgument("method") { type = NavType.EnumType( type = InstallMethod::class.java ) }
// ))
) {
InstallerScreen()
}
} }
} }
} }
} }
enum class InstallMethod {
DOWNLOAD, INSTALLED, STORAGE
}

View file

@ -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<Certificate>(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<String, String>()
val filesToRemove = ArrayList<String>()
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<String, String>()
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))
}

View file

@ -13,6 +13,6 @@ data class Commit(
val commit: Commit, val commit: Commit,
val author: Author val author: Author
) { ) {
class Author(@SerializedName("login") val name: String) data class Author(@SerializedName("login") val name: String)
class Commit(val message: String) data class Commit(val message: String)
} }

View file

@ -0,0 +1,13 @@
package com.aliucord.manager.models
data class Manifest(
val name: String,
val authors: ArrayList<Author>,
val description: String,
val version: String,
val updateUrl: String,
val changelog: String?,
val changelogMedia: String?
) {
data class Author(val name: String, val id: Long)
}

View file

@ -0,0 +1,7 @@
package com.aliucord.manager.models
data class Version(
val versionCode: String,
val versionName: String,
val aliucordHash: String
)

View file

@ -12,7 +12,7 @@ import com.aliucord.manager.R
@ExperimentalFoundationApi @ExperimentalFoundationApi
sealed class Screen(val route: String, @StringRes val displayName: Int) { sealed class Screen(val route: String, @StringRes val displayName: Int) {
companion object { 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) object Home : Screen("home", R.string.app_name)

View file

@ -44,7 +44,9 @@ fun AppBar(navController: NavController) {
IconButton( IconButton(
onClick = { onClick = {
navController.navigate(Screen.Settings.route) navController.navigate(Screen.Store.route) {
popUpTo(Screen.Home.route) { saveState = true }
}
}, },
) { ) {
Icon( Icon(

View file

@ -11,7 +11,6 @@ import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
@ -55,8 +54,8 @@ fun GrantPermission(permissionState: PermissionState) = Column(
) )
}, },
colors = ButtonDefaults.buttonColors( colors = ButtonDefaults.buttonColors(
containerColor = Color.LightGray, containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = Color.Black contentColor = MaterialTheme.colorScheme.onSecondaryContainer
), ),
) { ) {
Text(stringResource(R.string.open_settings)) Text(stringResource(R.string.open_settings))

View file

@ -12,6 +12,7 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.OutlinedTextField import androidx.compose.material.OutlinedTextField
import androidx.compose.material.TextFieldDefaults import androidx.compose.material.TextFieldDefaults
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.Search import androidx.compose.material.icons.filled.Search
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
@ -71,7 +72,13 @@ fun PluginsList() = Column(
if (showDeleteDialog) AlertDialog( if (showDeleteDialog) AlertDialog(
onDismissRequest = { showDeleteDialog = false }, 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)) }, text = { Text(stringResource(R.string.delete_plugin_body)) },
confirmButton = { confirmButton = {
Button( Button(
@ -79,13 +86,9 @@ fun PluginsList() = Column(
showDeleteDialog = false showDeleteDialog = false
plugin.file.delete() plugin.file.delete()
plugins = plugins.filter { it != plugin } 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 = { dismissButton = {
@ -98,7 +101,7 @@ fun PluginsList() = Column(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer contentColor = MaterialTheme.colorScheme.onSecondaryContainer
) )
) { ) {
Text(stringResource(android.R.string.cancel).uppercase()) Text(stringResource(android.R.string.cancel))
} }
} }
) )

View file

@ -25,10 +25,10 @@ fun Switch(
enabled = enabled, enabled = enabled,
interactionSource = interactionSource, interactionSource = interactionSource,
colors = SwitchDefaults.colors( colors = SwitchDefaults.colors(
checkedTrackColor = colors.surfaceVariant, checkedTrackColor = colors.primary,
uncheckedTrackColor = colors.secondaryContainer, uncheckedTrackColor = colors.secondary,
checkedThumbColor = colors.primary, checkedThumbColor = colors.primary,
uncheckedThumbColor = colors.onSecondaryContainer uncheckedThumbColor = colors.secondary
) )
) )
} }

View file

@ -1,26 +1,24 @@
package com.aliucord.manager.ui.components.about package com.aliucord.manager.ui.components.about
import android.util.Log
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.GridCells import androidx.compose.foundation.lazy.GridCells
import androidx.compose.foundation.lazy.LazyVerticalGrid import androidx.compose.foundation.lazy.LazyVerticalGrid
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.ElevatedCard import androidx.compose.material3.*
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.models.GithubUser import com.aliucord.manager.models.GithubUser
import com.aliucord.manager.utils.Github import com.aliucord.manager.utils.Github
import com.aliucord.manager.utils.httpClient
import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -28,9 +26,9 @@ import kotlinx.coroutines.launch
@ExperimentalFoundationApi @ExperimentalFoundationApi
@Composable @Composable
fun ContributorsCard() = ElevatedCard( fun ContributorsCard() = ElevatedCard(
modifier = Modifier.padding(bottom = 8.dp).wrapContentHeight().fillMaxWidth() modifier = Modifier.wrapContentHeight().fillMaxWidth()
) { ) {
val contributors = remember { mutableStateOf<Array<GithubUser>?>(null) } val contributors = remember { mutableStateListOf<GithubUser>() }
Column( Column(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),
@ -42,22 +40,24 @@ fun ContributorsCard() = ElevatedCard(
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
if (contributors.value != null) LazyVerticalGrid( if (contributors.isNotEmpty()) {
cells = GridCells.Adaptive(48.dp), LazyVerticalGrid(
verticalArrangement = Arrangement.spacedBy(4.dp), cells = GridCells.Adaptive(48.dp),
horizontalArrangement = Arrangement.spacedBy(4.dp) verticalArrangement = Arrangement.spacedBy(4.dp),
) { horizontalArrangement = Arrangement.spacedBy(4.dp)
items(contributors.value!!) { user -> ContributorEntry(user.name) } ) {
items(contributors) { user -> ContributorEntry(user.name) }
}
} else { } 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) { launch(Dispatchers.IO) {
try { contributors.addAll(httpClient.get<List<GithubUser>>(Github.contributorsUrl).sortedByDescending { it.contributions })
contributors.value = Github.contributors
} catch (err: Throwable) {
Log.e(BuildConfig.TAG, "Failed to load contributors", err)
}
} }
} }
} }

View file

@ -14,7 +14,7 @@ import com.aliucord.manager.R
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun TeamCard() = ElevatedCard( fun TeamCard() = ElevatedCard(
modifier = Modifier.padding(bottom = 8.dp).wrapContentHeight().fillMaxWidth() modifier = Modifier.wrapContentHeight().fillMaxWidth()
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp).fillMaxWidth(), modifier = Modifier.padding(16.dp).fillMaxWidth(),

View file

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

View file

@ -1,6 +1,7 @@
package com.aliucord.manager.ui.components.plugins package com.aliucord.manager.ui.components.plugins
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material3.* import androidx.compose.material3.*
@ -25,16 +26,21 @@ fun PluginCard(
onDelete: () -> Unit, onDelete: () -> Unit,
onShowChangelog: () -> Unit onShowChangelog: () -> Unit
) { ) {
var isEnabled by remember { mutableStateOf(true) }
ElevatedCard( ElevatedCard(
modifier = Modifier.fillMaxWidth() modifier = Modifier.fillMaxWidth(),
elevation = CardDefaults.cardElevation(defaultElevation = 2.dp)
) { ) {
Column( Column(
modifier = Modifier.padding(12.dp) modifier = Modifier.padding(12.dp)
) { ) {
var isEnabled by remember { mutableStateOf(true) }
// Header // Header
Row { Row(
modifier = Modifier.toggleable(value = isEnabled) {
isEnabled = it
}
) {
Column { Column {
// Name // Name
Text( 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) { if (plugin.manifest.changelog != null) {
@ -92,7 +101,8 @@ fun PluginCard(
) { ) {
Icon( Icon(
painter = painterResource(R.drawable.ic_history_white_24dp), 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) { IconButton(onClick = onDelete) {
Icon( Icon(
imageVector = Icons.Default.Delete, 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 tint = MaterialTheme.colorScheme.error
) )
} }

View file

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

View file

@ -1,10 +1,7 @@
package com.aliucord.manager.ui.screens package com.aliucord.manager.ui.screens
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
import androidx.compose.material3.Text import androidx.compose.material3.Text
@ -23,7 +20,9 @@ import com.aliucord.manager.ui.components.about.TeamCard
fun AboutScreen() { fun AboutScreen() {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
Column { Column(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
TeamCard() TeamCard()
ContributorsCard() ContributorsCard()
@ -35,9 +34,9 @@ fun AboutScreen() {
) { ) {
Icon( Icon(
painter = painterResource(R.drawable.ic_account_github_white_24dp), painter = painterResource(R.drawable.ic_account_github_white_24dp),
contentDescription = "GitHub", contentDescription = "GitHub"
modifier = Modifier.padding(8.dp)
) )
Spacer(Modifier.padding(4.dp))
Text(stringResource(R.string.source_code)) Text(stringResource(R.string.source_code))
} }
} }

View file

@ -1,6 +1,7 @@
package com.aliucord.manager.ui.screens package com.aliucord.manager.ui.screens
import android.util.Log import android.util.Log
import androidx.annotation.Keep
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -10,7 +11,10 @@ import androidx.compose.material.ListItem
import androidx.compose.material3.CircularProgressIndicator import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text 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.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
@ -22,7 +26,8 @@ import com.aliucord.manager.utils.Github
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
data class CommitData( @Keep
private data class CommitData(
val commit: Commit, val commit: Commit,
val buildSha: String? val buildSha: String?
) )
@ -31,38 +36,35 @@ data class CommitData(
@ExperimentalMaterialApi @ExperimentalMaterialApi
@Composable @Composable
fun CommitsScreen() { fun CommitsScreen() {
var isLoading by remember { mutableStateOf(true) }
val commits = remember { mutableStateListOf<CommitData>() } val commits = remember { mutableStateListOf<CommitData>() }
Column( Column(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
if (isLoading) { if (commits.isEmpty()) {
Box( Box(
modifier = Modifier.fillMaxSize().padding(12.dp), modifier = Modifier.fillMaxSize().padding(12.dp),
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
content = { CircularProgressIndicator() } content = { CircularProgressIndicator() }
) )
LaunchedEffect(null) { LaunchedEffect(Unit) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
try { try {
val buildCommits = Github.getCommits( val buildCommits = Github.getCommits(
mapOf( "sha" to "builds",
"sha" to "builds", "path" to "Aliucord.dex",
"path" to "Aliucord.dex", "per_page" to "50"
"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 -> commits.addAll(commits2.map { c ->
CommitData( CommitData(
c, commit = c,
buildCommits.find { bc -> bc.commit.message.substring(6) == c.sha }?.sha buildSha = buildCommits.find { bc -> bc.commit.message.substring(6) == c.sha }?.sha
) )
}) })
isLoading = false
} catch (e: Throwable) { } catch (e: Throwable) {
Log.e(BuildConfig.TAG, "Failed to get commits", e) Log.e(BuildConfig.TAG, "Failed to get commits", e)
} }

View file

@ -5,18 +5,15 @@
package com.aliucord.manager.ui.screens package com.aliucord.manager.ui.screens
import android.Manifest import android.content.Intent
import android.util.Log import android.net.Uri
import android.widget.Toast
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.ExitToApp
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.*
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext 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.text.withStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.navigation.NavController import androidx.navigation.NavController
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.models.Version
import com.aliucord.manager.preferences.devModePreference import com.aliucord.manager.preferences.devModePreference
import com.aliucord.manager.ui.Screen import com.aliucord.manager.ui.Screen
import com.aliucord.manager.ui.components.PluginsList import com.aliucord.manager.ui.components.PluginsList
import com.aliucord.manager.ui.components.installer.InstallerDialog import com.aliucord.manager.ui.components.installer.InstallerDialog
import com.aliucord.manager.utils.Github 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.ExperimentalPermissionsApi
import com.google.accompanist.permissions.rememberPermissionState import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
@ -46,10 +44,8 @@ import kotlinx.coroutines.launch
@ExperimentalFoundationApi @ExperimentalFoundationApi
@Composable @Composable
fun HomeScreen(navController: NavController) { fun HomeScreen(navController: NavController) {
val storagePermissionState = rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE)
val packageManager = LocalContext.current.packageManager val packageManager = LocalContext.current.packageManager
val versions = remember { mutableStateOf<Versions?>(null) } var supportedVersion by remember { mutableStateOf<String?>(null) }
val installedVersion = remember { val installedVersion = remember {
try { try {
packageManager.getPackageInfo("com.aliucord", 0).versionName packageManager.getPackageInfo("com.aliucord", 0).versionName
@ -60,12 +56,15 @@ fun HomeScreen(navController: NavController) {
val showOptionsDialog = remember { mutableStateOf(false) } val showOptionsDialog = remember { mutableStateOf(false) }
if (versions.value == null) LaunchedEffect(Unit) { if (supportedVersion == null) LaunchedEffect(Unit) {
launch(Dispatchers.IO) { launch(Dispatchers.IO) {
try { val version = gson.fromJson(httpClient.get<String>(Github.dataUrl), Version::class.java)
versions.value = Github.versions
} catch (th: Throwable) { supportedVersion = "${version.versionName} - " + when (version.versionCode[3].toString()) {
Log.e(BuildConfig.TAG, "Failed to get versions", th) "0" -> "Stable"
"1" -> "Beta"
"2" -> "Alpha"
else -> throw NoWhenBranchMatchedException()
} }
} }
} }
@ -93,7 +92,7 @@ fun HomeScreen(navController: NavController) {
append("Supported version: ") append("Supported version: ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(versions.value?.versionName ?: "?") append(supportedVersion ?: "?")
} }
append("\nInstalled version: ") append("\nInstalled version: ")
@ -104,11 +103,9 @@ fun HomeScreen(navController: NavController) {
}) })
} }
val (drawable, description) = when { val (drawable, description) = when (installedVersion) {
installedVersion == "-" -> R.drawable.ic_download_24dp to R.string.install "-" -> R.drawable.ic_download_24dp to R.string.install
installedVersion.startsWith( supportedVersion -> R.drawable.ic_reinstall_24dp to R.string.reinstall
versions.value?.versionName ?: "uwu"
) -> R.drawable.ic_reinstall_24dp to R.string.reinstall
else -> R.drawable.ic_update_24dp to R.string.update else -> R.drawable.ic_update_24dp to R.string.update
} }
@ -116,29 +113,6 @@ fun HomeScreen(navController: NavController) {
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp) 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( Button(
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
onClick = { onClick = {
@ -166,12 +140,18 @@ fun HomeScreen(navController: NavController) {
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
// if (installedVersion != "-") { if (installedVersion != "-") {
if (true) { val context = LocalContext.current
Button( Button(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer), 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( Icon(
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(8.dp),
@ -183,12 +163,17 @@ fun HomeScreen(navController: NavController) {
Button( Button(
modifier = Modifier.weight(1f), 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) colors = ButtonDefaults.buttonColors(containerColor = MaterialTheme.colorScheme.secondaryContainer)
) { ) {
Icon( Icon(
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(8.dp),
imageVector = Icons.Default.ExitToApp, painter = painterResource(R.drawable.ic_launch_24dp),
contentDescription = "Launch", contentDescription = "Launch",
tint = MaterialTheme.colorScheme.onSecondaryContainer tint = MaterialTheme.colorScheme.onSecondaryContainer
) )

View file

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

View file

@ -77,8 +77,12 @@ fun SettingsScreen() = Column(
themePref.set(selected.ordinal) themePref.set(selected.ordinal)
} }
) { ) {
Icon(
imageVector = Icons.Default.Check,
contentDescription = "Confirm"
)
Spacer(Modifier.padding(4.dp))
Text("Apply") Text("Apply")
Icon(Icons.Default.Check, "Confirm")
} }
} }
) )

View file

@ -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<File>
) {
@Keep
data class File(
val mode: String,
val sha: String
)
}
@Composable
fun StoreScreen() {
val plugins = remember { mutableStateListOf<Tree.File>() }
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<Tree>("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)
}
}
}
}
}

View file

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

View file

@ -14,21 +14,23 @@ import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
private val DarkColorScheme = darkColorScheme( private val LightColorScheme = lightColorScheme()
private val DarkColorScheme = darkColorScheme()
) private val BlackColorScheme = darkColorScheme(
background = Color.Black,
private val LightColorScheme = lightColorScheme( surface = Color.Black,
primary = Color.LightGray,
) onPrimary = Color.DarkGray,
secondary = Color.Gray,
private val BlackColorPalette = darkColorScheme( onSecondary = Color.LightGray,
primary = Color.Black, secondaryContainer = Color.DarkGray,
onPrimary = Color.White, onSecondaryContainer = Color.White,
outline = Color.LightGray
) )
@Composable @Composable
fun ManagerTheme( fun ManagerTheme(
isBlack: Boolean = false,
isDarkTheme: Boolean = isSystemInDarkTheme(), isDarkTheme: Boolean = isSystemInDarkTheme(),
isDynamicColor: Boolean = true, isDynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
@ -37,7 +39,7 @@ fun ManagerTheme(
val colorScheme = when { val colorScheme = when {
dynamicColor && isDarkTheme -> dynamicDarkColorScheme(LocalContext.current) dynamicColor && isDarkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !isDarkTheme -> dynamicLightColorScheme(LocalContext.current) dynamicColor && !isDarkTheme -> dynamicLightColorScheme(LocalContext.current)
isDarkTheme -> DarkColorScheme isDarkTheme -> if (isBlack) BlackColorScheme else DarkColorScheme
else -> LightColorScheme else -> LightColorScheme
} }

View file

@ -4,9 +4,12 @@ import android.os.Environment
import com.google.gson.Gson import com.google.gson.Gson
import io.ktor.client.* import io.ktor.client.*
import io.ktor.client.engine.android.* import io.ktor.client.engine.android.*
import io.ktor.client.features.json.*
import java.io.File import java.io.File
val httpClient = HttpClient(Android) val httpClient = HttpClient(Android) {
install(JsonFeature)
}
val aliucordDir = File(Environment.getExternalStorageDirectory(), "Aliucord") val aliucordDir = File(Environment.getExternalStorageDirectory(), "Aliucord")
val pluginsDir = File(aliucordDir, "plugins") val pluginsDir = File(aliucordDir, "plugins")
val gson = Gson() val gson = Gson()

View file

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

View file

@ -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<String, Array<Commit>>()
suspend fun getCommits(vararg params: Pair<String, String>): Array<Commit> {
val query = params.joinToString("&", "?") { it.first + "=" + it.second }
if (commitsCache.containsKey(query)) return commitsCache[query] ?: emptyArray()
val res = httpClient.get<Array<Commit>>(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"
}

View file

@ -2,31 +2,20 @@ package com.aliucord.manager.utils
import android.util.Log import android.util.Log
import com.aliucord.manager.BuildConfig import com.aliucord.manager.BuildConfig
import com.aliucord.manager.models.Manifest
import java.io.File import java.io.File
import java.io.InputStreamReader import java.io.InputStreamReader
import java.util.zip.ZipException import java.util.zip.ZipException
import java.util.zip.ZipFile import java.util.zip.ZipFile
data class Manifest(
val name: String,
val authors: ArrayList<Author>,
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) { class Plugin(val file: File) {
val manifest: Manifest val manifest: Manifest
init { init {
ZipFile(file).use { ZipFile(file).use { zipFile ->
val entry = it.getEntry("manifest.json") val entry = zipFile.getEntry("manifest.json")
?: throw ZipException("Plugin ${file.nameWithoutExtension} has no manifest.") ?: 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) manifest = gson.fromJson(InputStreamReader(zis), Manifest::class.java)
} }
} }

View file

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" android:height="24dp" android:width="24dp" android:viewportWidth="24" android:viewportHeight="24">
<path android:fillColor="#fff" android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z"/>
</vector>

View file

@ -0,0 +1,61 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:fillColor="#00000000"
android:pathData="m61.557,45.919c1.52,-0.596 3.04,-1.192 4.023,-1.566 0.983,-0.374 1.485,-0.545 1.095,-1.456 -0.39,-0.91 -1.413,-2.387 -3.086,-3.984 -1.673,-1.596 -2.94,-2.058 -3.336,-2.199 -0.396,-0.14 -1.091,-0.37 -1.563,-0.506 -0.472,-0.136 -0.953,-0.255 -2.288,-0.298 -1.335,-0.043 -4.019,-0.001 -5.11,0.085 -1.091,0.086 -1.859,0.315 -2.415,0.498 -0.556,0.183 -1.041,0.362 -1.492,0.551 -0.451,0.189 -0.854,0.383 -1.201,0.567 -0.348,0.184 -0.588,0.33 -1.292,0.898 -0.704,0.569 -1.737,1.451 -2.646,2.953 -0.91,1.502 -1.522,3.335 -1.864,4.87 -0.343,1.534 -0.351,2.483 -0.342,4.885 0.009,2.402 0.049,8.183 0.095,8.975 0.046,0.792 0.365,1.208 0.563,1.467"
android:strokeWidth="2.5"
android:strokeAlpha="0.5"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#00000000"
android:pathData="M49.252,56.956V49.586"
android:strokeWidth="2.5"
android:strokeAlpha="0.5019608"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#00000000"
android:pathData="m47.257,70.319 l-0.571,1.788H36.635l5.398,-15.146h14.598l-2.565,-10.416h-0.262l-1.597,6.112 -6.306,-6.082 3.663,-10.094h8.532L71.341,72.107H61.29l-0.58,-1.719c2.606,-1.365 4.614,-3.013 6.134,-5.265 0.104,-0.15 0.763,-1.271 0.763,-1.271l-6.778,-3.011c-1.095,2.369 -5.016,3.932 -7.295,4.111l-4.546,0.009z"
android:strokeWidth="2.5"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillColor="#00000000"
android:pathData="m47.257,70.319c1.626,0.664 3.253,1.328 5.061,1.47 1.808,0.142 3.8,-0.238 5.228,-0.558 1.429,-0.32 2.296,-0.581 3.164,-0.842"
android:strokeWidth="2.5"
android:strokeAlpha="0.501961"
android:strokeColor="#000000"
android:strokeLineCap="butt"
android:strokeLineJoin="miter" />
<path
android:fillAlpha="0.501961"
android:fillColor="#000000"
android:pathData="m41.318,53.87c-0.052,-4.613 -0.018,-5.691 0.218,-6.885 0.359,-1.817 1.072,-3.722 1.869,-4.993 0.637,-1.016 1.519,-1.935 2.752,-2.866 0.2,-0.151 0.501,-0.332 0.838,-0.504 0.289,-0.147 0.529,-0.263 0.533,-0.257 0.004,0.006 -0.685,1.916 -1.532,4.245 -0.847,2.329 -1.54,4.255 -1.541,4.281 -0.001,0.026 0.793,0.812 1.764,1.747l1.765,1.7v2.682,2.682h-3.323,-3.323z"
android:strokeWidth="0.078125"
android:strokeAlpha="0.5"
android:strokeColor="#00000000"
android:strokeLineJoin="miter" />
<path
android:fillAlpha="0.501961"
android:fillColor="#000000"
android:pathData="m61.176,41.134c-0.639,-1.72 -1.155,-3.134 -1.147,-3.143 0.03,-0.03 0.789,0.365 1.126,0.588 0.714,0.471 1.27,0.943 2.047,1.737 0.586,0.6 1.038,1.123 1.461,1.691 0.325,0.437 0.704,1.043 0.672,1.074 -0.013,0.013 -0.621,0.254 -1.352,0.537 -0.73,0.283 -1.4,0.543 -1.487,0.578l-0.159,0.064z"
android:strokeWidth="0.078125"
android:strokeAlpha="0.5"
android:strokeColor="#00000000"
android:strokeLineJoin="miter" />
<path
android:fillAlpha="0.501961"
android:fillColor="#000000"
android:pathData="m52.087,70.498c-0.776,-0.094 -1.676,-0.326 -2.676,-0.691 -0.312,-0.114 -0.571,-0.21 -0.577,-0.214 -0.005,-0.004 0.233,-0.762 0.531,-1.683l0.541,-1.675 1.485,-0.001c2.337,-0.002 2.808,-0.038 3.863,-0.296 1.522,-0.373 3.157,-1.096 4.342,-1.921 0.572,-0.398 1.157,-0.915 1.554,-1.372l0.174,-0.2 2.198,0.976c1.209,0.537 2.211,0.988 2.226,1.003 0.035,0.033 -0.326,0.541 -0.741,1.044 -0.373,0.452 -1.259,1.342 -1.693,1.702 -0.697,0.577 -1.494,1.133 -2.303,1.607 -0.679,0.398 -0.774,0.44 -1.419,0.627 -1.961,0.57 -4.04,0.993 -5.465,1.112 -0.518,0.043 -1.615,0.034 -2.038,-0.017z"
android:strokeWidth="0.078125"
android:strokeAlpha="0.5"
android:strokeColor="#00000000"
android:strokeLineJoin="miter" />
</vector>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>

View file

@ -2,4 +2,5 @@
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> <adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background"/> <background android:drawable="@color/ic_launcher_background"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/> <foreground android:drawable="@drawable/ic_launcher_foreground"/>
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon> </adaptive-icon>

View file

@ -1,4 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<color name="primary">#00c853</color> <color name="primary">#00c853</color>
<color name="ic_launcher_background">#FFFFFFFF</color>
</resources> </resources>

View file

@ -1,4 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<color name="ic_launcher_background">#FFFFFFFF</color>
</resources>

View file

@ -15,7 +15,7 @@
<string name="launch_plugins">Launch Plugins Page</string> <string name="launch_plugins">Launch Plugins Page</string>
<string name="plugin_title">%1$s v%2$s</string> <string name="plugin_title">%1$s v%2$s</string>
<string name="plugin_authors">By %1$s</string> <string name="plugin_authors">By %1$s</string>
<string name="delete_plugin">Delete %1$s</string> <string name="uninstall_plugin">Uninstall %1$s</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="view_plugin_changelog">View changelog for %1$s</string> <string name="view_plugin_changelog">View changelog for %1$s</string>
<string name="delete_plugin_body">Are you sure you want to uninstall this plugin? This action cannot be undone.</string> <string name="delete_plugin_body">Are you sure you want to uninstall this plugin? This action cannot be undone.</string>

View file

@ -1,6 +1,6 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<resources> <resources>
<style name="Theme.AliucordManager" parent="Theme.MaterialComponents.NoActionBar"> <style name="Theme.AliucordManager" parent="Theme.AppCompat.NoActionBar">
<item name="colorPrimary">@color/primary</item> <item name="colorPrimary">@color/primary</item>
</style> </style>
</resources> </resources>

View file

@ -9,8 +9,9 @@ buildscript {
google() google()
mavenCentral() mavenCentral()
} }
dependencies { 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") classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:1.6.10")
// NOTE: Do not place your application dependencies here; they belong // 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<Delete>("clean") { tasks.register<Delete>("clean") {
delete(rootProject.buildDir) delete(rootProject.buildDir)
} }

View file

@ -3,13 +3,5 @@
* Licensed under the Open Software License version 3.0 * Licensed under the Open Software License version 3.0
*/ */
dependencyResolutionManagement {
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {
google()
mavenCentral()
}
}
rootProject.name = "Aliucord Manager" rootProject.name = "Aliucord Manager"
include(":app") include(":app")