Finished installer functionality
This commit is contained in:
parent
f0cd29c500
commit
5e1a71d4d4
67 changed files with 1486 additions and 655 deletions
|
|
@ -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")
|
||||||
}
|
}
|
||||||
1
app/proguard-rules.pro
vendored
1
app/proguard-rules.pro
vendored
|
|
@ -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 *; }
|
||||||
|
|
@ -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" />
|
||||||
|
|
|
||||||
BIN
app/src/main/assets/icon1.png
Normal file
BIN
app/src/main/assets/icon1.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 6.1 KiB |
BIN
app/src/main/assets/icon2.png
Normal file
BIN
app/src/main/assets/icon2.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.9 KiB |
202
app/src/main/assets/kotlin/LICENSE.txt
Normal file
202
app/src/main/assets/kotlin/LICENSE.txt
Normal 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.
|
||||||
BIN
app/src/main/assets/kotlin/classes.dex
Normal file
BIN
app/src/main/assets/kotlin/classes.dex
Normal file
Binary file not shown.
46
app/src/main/assets/pine/LICENSE
Normal file
46
app/src/main/assets/pine/LICENSE
Normal 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.
|
||||||
8
app/src/main/assets/pine/README.md
Normal file
8
app/src/main/assets/pine/README.md
Normal 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
|
||||||
BIN
app/src/main/assets/pine/arm64-v8a/libpine.so
Normal file
BIN
app/src/main/assets/pine/arm64-v8a/libpine.so
Normal file
Binary file not shown.
BIN
app/src/main/assets/pine/armeabi-v7a/libpine.so
Normal file
BIN
app/src/main/assets/pine/armeabi-v7a/libpine.so
Normal file
Binary file not shown.
BIN
app/src/main/assets/pine/classes.dex
Normal file
BIN
app/src/main/assets/pine/classes.dex
Normal file
Binary file not shown.
100
app/src/main/assets/pine/make_artifacts.sh
Normal file
100
app/src/main/assets/pine/make_artifacts.sh
Normal 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."
|
||||||
|
|
||||||
|
)
|
||||||
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
@ -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
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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 {
|
|
||||||
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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)
|
|
||||||
|
|
@ -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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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}"
|
|
||||||
}
|
|
||||||
6
app/src/main/kotlin/com/aliucord/manager/KeySet.kt
Normal file
6
app/src/main/kotlin/com/aliucord/manager/KeySet.kt
Normal 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)
|
||||||
|
|
@ -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
|
|
||||||
}
|
|
||||||
199
app/src/main/kotlin/com/aliucord/manager/Signer.kt
Normal file
199
app/src/main/kotlin/com/aliucord/manager/Signer.kt
Normal 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))
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
13
app/src/main/kotlin/com/aliucord/manager/models/Manifest.kt
Normal file
13
app/src/main/kotlin/com/aliucord/manager/models/Manifest.kt
Normal 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)
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
package com.aliucord.manager.models
|
||||||
|
|
||||||
|
data class Version(
|
||||||
|
val versionCode: String,
|
||||||
|
val versionName: String,
|
||||||
|
val aliucordHash: String
|
||||||
|
)
|
||||||
|
|
@ -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)
|
||||||
|
|
@ -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(
|
||||||
|
|
@ -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))
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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()) {
|
||||||
|
LazyVerticalGrid(
|
||||||
cells = GridCells.Adaptive(48.dp),
|
cells = GridCells.Adaptive(48.dp),
|
||||||
verticalArrangement = Arrangement.spacedBy(4.dp),
|
verticalArrangement = Arrangement.spacedBy(4.dp),
|
||||||
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
horizontalArrangement = Arrangement.spacedBy(4.dp)
|
||||||
) {
|
) {
|
||||||
items(contributors.value!!) { user -> ContributorEntry(user.name) }
|
items(contributors) { user -> ContributorEntry(user.name) }
|
||||||
} else {
|
|
||||||
Text(stringResource(R.string.loading))
|
|
||||||
|
|
||||||
LaunchedEffect(null) {
|
|
||||||
launch(Dispatchers.IO) {
|
|
||||||
try {
|
|
||||||
contributors.value = Github.contributors
|
|
||||||
} catch (err: Throwable) {
|
|
||||||
Log.e(BuildConfig.TAG, "Failed to load contributors", err)
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier.fillMaxWidth().padding(12.dp),
|
||||||
|
contentAlignment = Alignment.Center,
|
||||||
|
content = { CircularProgressIndicator() }
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
launch(Dispatchers.IO) {
|
||||||
|
contributors.addAll(httpClient.get<List<GithubUser>>(Github.contributorsUrl).sortedByDescending { it.contributions })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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(),
|
||||||
|
|
@ -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
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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("per_page" to "50")
|
||||||
val commits2 = Github.getCommits(mapOf("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)
|
||||||
}
|
}
|
||||||
|
|
@ -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
|
||||||
)
|
)
|
||||||
|
|
@ -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()
|
||||||
|
}
|
||||||
|
|
@ -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")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
20
app/src/main/kotlin/com/aliucord/manager/ui/theme/Color.kt
Normal file
20
app/src/main/kotlin/com/aliucord/manager/ui/theme/Color.kt
Normal 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
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -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()
|
||||||
|
|
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
48
app/src/main/kotlin/com/aliucord/manager/utils/Github.kt
Normal file
48
app/src/main/kotlin/com/aliucord/manager/utils/Github.kt
Normal 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"
|
||||||
|
}
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
4
app/src/main/res/drawable/ic_launch_24dp.xml
Normal file
4
app/src/main/res/drawable/ic_launch_24dp.xml
Normal 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>
|
||||||
61
app/src/main/res/drawable/ic_launcher_monochrome.xml
Normal file
61
app/src/main/res/drawable/ic_launcher_monochrome.xml
Normal 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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -1,4 +0,0 @@
|
||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
<resources>
|
|
||||||
<color name="ic_launcher_background">#FFFFFFFF</color>
|
|
||||||
</resources>
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
@ -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")
|
||||||
Loading…
Add table
Reference in a new issue