Compare commits

...

134 commits
v1.1.2 ... main

Author SHA1 Message Date
OTKUSteyler
2cc79c59d0
Update InjectShiggyXposedStep.kt
Some checks failed
Build Debug App / build (push) Failing after 7m12s
2026-09-07 05:40:27 +02:00
OTKUSteyler
3ba2f9a460
Merge branch 'kmmiio99o:main' into main 2026-06-09 13:55:29 +02:00
kmmiio99o
0045d3b2a4
chore: bump version 2026-06-09 11:27:20 +00:00
kmmiio99o
71fab34c98
fix: use locale-independent formatting for LSPatch output filename (#4)
Fixes installation failures on non-Latin locales by ensuring the LSPatch
output filename lookup uses Latin numerals regardless of device locale.

LSPatch uses String.format(Locale.getDefault(), ...) which produces
locale-specific numerals (e.g., Arabic numerals on Arabic locales),
but the filename lookup was using hardcoded Latin numerals, causing
a mismatch that resulted in "Invalid package" errors.

Fixes #XXXX
2026-06-09 11:22:11 +00:00
OTKUSteyler
fdf93c7b1e
Update README.md 2026-05-22 21:51:05 +02:00
OTKUSteyler
460375e4ac
Update README.md 2026-05-22 21:50:25 +02:00
OTKUSteyler
28fe88c157 Merge remote-tracking branch 'upstream/main' 2026-05-08 00:02:56 +02:00
kmmiio99o
eaad9755ea
chore: bump to 1.1.2 2026-05-07 16:02:23 +02:00
kmmiio99o
50d58dc4e1
feat: use JingMatrix lspatch 2026-05-07 16:01:23 +02:00
OTKUSteyler
62c5659245
Merge branch 'kmmiio99o:main' into main 2026-04-28 12:42:55 +02:00
CatPieLeaf
7411ceb7a7
chore: update LSPatch and use JingMatrix fork (#3) 2026-04-28 11:51:17 +02:00
OTKUSteyler
ac57f05cae
Merge branch 'kmmiio99o:main' into main 2026-04-02 06:28:15 +02:00
мекатів
dcc33736a3
feat(manifestpatcher): allow connecting over http (#2)
Co-authored-by: meqativ <meqativ@users.noreply.github.com>
2026-04-01 23:47:23 +02:00
OTKUSteyler
35dca04b37
Update ic_discord_aliucord.xml 2026-03-26 09:50:04 +01:00
OTKUSteyler
748bb9dc59
Update colors.xml 2026-03-26 07:44:12 +01:00
j
1fa78ab647
proper icon referencing 2026-03-04 10:23:30 +01:00
J
6a3c90d12b fix(icon/anydpi-v26): nuked the wrong icons, replaced with right icons 2026-03-02 17:25:42 +01:00
OTKUSteyler
a00a6fdf66
Update ic_discord_aliucord.xml 2026-03-02 16:39:17 +01:00
OTKUSteyler
17b40741ea
Update ic_launcher.xml 2026-03-02 01:41:36 +01:00
OTKUSteyler
5c17dc3f9d
Update colors.xml 2026-03-02 01:30:32 +01:00
OTKUSteyler
0cd04a473a
Delete app/src/main/res/ic_launcher_round.png 2026-03-02 01:07:26 +01:00
OTKUSteyler
f234785571
Delete app/src/main/res/ic_launcher.png 2026-03-02 01:07:18 +01:00
OTKUSteyler
98625b22b8
Update colors.xml 2026-03-01 22:35:35 +01:00
OTKUSteyler
15a81b53ac
Update strings.xml 2026-03-01 22:32:15 +01:00
OTKUSteyler
3c8f78ef68
Update ic_launcher_background.xml 2026-03-01 22:27:59 +01:00
OTKUSteyler
1725bb6f0d
Update colors.xml 2026-03-01 21:53:23 +01:00
OTKUSteyler
2b62ed13a3
Update colors.xml 2026-03-01 21:43:53 +01:00
OTKUSteyler
d414b9c20a
Update fun facts in strings.xml 2026-03-01 21:41:35 +01:00
OTKUSteyler
e331dbb030
Update strings.xml 2026-03-01 21:39:35 +01:00
OTKUSteyler
ecd5c55008
Add files via upload 2026-02-20 18:01:33 +01:00
OTKUSteyler
7b82e406a4
Add files via upload 2026-02-20 18:00:08 +01:00
OTKUSteyler
eee219a4d1
Add files via upload 2026-02-20 17:59:58 +01:00
OTKUSteyler
012ece1855
Add files via upload 2026-02-20 17:59:45 +01:00
OTKUSteyler
2b12b2fb4e
Add files via upload 2026-02-20 17:59:30 +01:00
OTKUSteyler
0c58003936 Add files via upload 2026-02-20 17:59:12 +01:00
OTKUSteyler
6a5114cad0
Add files via upload 2026-02-20 17:59:01 +01:00
OTKUSteyler
17cd3d552e
Update ic_rounded_shiggy.xml 2026-02-20 06:56:39 +01:00
OTKUSteyler
9aefa5fed7
Update ic_rounded_shiggy.xml 2026-02-19 22:57:24 +01:00
OTKUSteyler
5df4a74536
Add files via upload 2026-02-19 20:34:00 +01:00
OTKUSteyler
94720320d8
Add files via upload 2026-02-19 20:22:53 +01:00
OTKUSteyler
3f29feecec
Add files via upload 2026-02-19 20:22:38 +01:00
OTKUSteyler
3529f5f7c1
Add files via upload 2026-02-19 20:22:21 +01:00
OTKUSteyler
ad6e5eb850
Add files via upload 2026-02-19 20:21:48 +01:00
OTKUSteyler
546d97844e
Add files via upload 2026-02-19 20:21:06 +01:00
OTKUSteyler
d0aff56a91
Add files via upload 2026-02-19 20:10:42 +01:00
OTKUSteyler
665b60654e
Add files via upload 2026-02-19 20:10:34 +01:00
OTKUSteyler
6eebcdb494
Add files via upload 2026-02-19 20:10:26 +01:00
OTKUSteyler
f3f352977e
Add files via upload 2026-02-19 20:10:13 +01:00
OTKUSteyler
e0aac86ec2
Add files via upload 2026-02-19 20:09:29 +01:00
OTKUSteyler
1b29c8070b
Update ic_discord.xml 2026-02-17 01:06:53 +01:00
OTKUSteyler
f60c24a259
Update ic_discord_aliucord.xml 2026-02-17 01:06:07 +01:00
OTKUSteyler
bcf39e1e65
Update ic_discord.xml 2026-02-17 01:03:33 +01:00
OTKUSteyler
1dea909ef5
Update ic_rounded_shiggy.xml 2026-02-17 00:12:38 +01:00
OTKUSteyler
b90183d4a6
Update ic_rounded_shiggy.xml 2026-02-17 00:10:40 +01:00
OTKUSteyler
ca59a08cc2
Update ic_rounded_shiggy.xml 2026-02-17 00:08:57 +01:00
OTKUSteyler
28dc73b7ae
Update ic_rounded_shiggy.xml 2026-02-17 00:06:34 +01:00
OTKUSteyler
84677d92c2
Update ic_rounded_shiggy.xml 2026-02-17 00:05:14 +01:00
OTKUSteyler
57f7d8b3dc
Update ic_rounded_shiggy.xml 2026-02-17 00:03:07 +01:00
OTKUSteyler
3c1c423283
Update ic_rounded_shiggy.xml 2026-02-17 00:02:46 +01:00
OTKUSteyler
aa4a3a6847
Update ic_discord.xml 2026-02-16 23:47:00 +01:00
OTKUSteyler
fedb711333
Update ic_rounded_shiggy.xml 2026-02-16 23:43:46 +01:00
OTKUSteyler
4b7b0009fa
Update ic_discord.xml 2026-02-16 23:42:26 +01:00
OTKUSteyler
b033d53c5b
Update ic_discord.xml 2026-02-16 23:40:51 +01:00
OTKUSteyler
cb3021e2a2
Update ic_rounded_shiggy.xml 2026-02-15 23:36:53 +01:00
OTKUSteyler
82e69d5ab1
Update ic_rounded_shiggy.xml 2026-02-15 23:30:42 +01:00
OTKUSteyler
e499c7d228
Update ic_rounded_shiggy.xml 2026-02-15 23:30:13 +01:00
OTKUSteyler
2759b71df1
Update ic_discord.xml 2026-02-15 23:27:30 +01:00
OTKUSteyler
6663e4a912
Update ic_rounded_shiggy.xml 2026-02-15 23:26:53 +01:00
OTKUSteyler
1e4b846dbd
Update ic_rounded_shiggy.xml 2026-02-14 17:22:17 +01:00
OTKUSteyler
d6fd188dce
Update ic_rounded_shiggy.xml 2026-02-12 20:13:14 +01:00
OTKUSteyler
c5d9dc9821
Update ic_rounded_shiggy.xml 2026-02-11 23:16:00 +01:00
OTKUSteyler
45f14b92fe
Update ic_rounded_shiggy.xml 2026-02-11 23:01:32 +01:00
OTKUSteyler
79a215d318
Merge branch 'kmmiio99o:main' into main 2026-01-16 21:29:00 +00:00
kmmiio99o
8cf989a239
feat: Add basic implementation of Amoled mode in the manager and fix
some misspells

TODO:
- Fix instance card to also use Amoled when needed
2026-01-16 20:44:29 +01:00
OTKUSteyler
d07c8e953d
Merge branch 'kmmiio99o:main' into main 2026-01-10 16:32:35 +00:00
kmmiio99o
3c14917b2f
bump to 10_11_01 2026-01-10 17:21:38 +01:00
kmmiio99o
48b5e39f3a
feat: possible fix rate limiting and github being blocked in some
countries?
2026-01-10 17:21:14 +01:00
OTKUSteyler
eb41f0ec8e
Update DownloadShiggyXposedStep.kt 2025-11-26 09:54:16 +01:00
OTKUSteyler
5dcee025b6
Update README.md 2025-11-23 16:26:35 +01:00
kmmiio99o
49392ab273 Fix: Fix Manager and About Manager Page
Some checks failed
Build Debug App / build (push) Failing after 5s
Build & Publish Release App / publish (push) Failing after 5s
- Updates the file name to goon.json, consistent with the GoonCord
rebrand - Fixes the metadata reading code to use the correct name -
Update references in `InstallMetadata.kt` and `SaveMetadataStep.kt`. -
Replace "ShiggyCord" with "GoonCord" in `AboutModel.kt`. - Rename
`ContributorListItem` to `RegularContributorListItem` - Add a new
"LeadContributorItem" composable. - Rename `shiggyXposedVersion` to
`goonXposedVersion`. - Fix version up to date comparison
2025-11-23 14:52:24 +01:00
OTKUSteyler
ba79a54b83
Update HomeModel.kt 2025-11-23 11:16:29 +01:00
OTKUSteyler
96400eb22c
Delete app/src/main/kotlin/com/aliucord/manager/ui/screens/plugins directory 2025-11-23 11:14:09 +01:00
OTKUSteyler
2d213f46e8
Update ContributorListItem.kt 2025-11-22 22:29:03 +01:00
OTKUSteyler
a51e82c077
Update AboutModel.kt 2025-11-22 22:26:03 +01:00
OTKUSteyler
a1d70359a2
Delete app/src/main/kotlin/com/aliucord/manager/network/models/Developers.kt 2025-11-22 22:22:45 +01:00
OTKUSteyler
4f96d33abd Merge remote-tracking branch 'upstream/main' 2025-11-22 21:58:40 +01:00
OTKUSteyler
af71c66820
Rename ContributorListItem.kt. to ContributorListItem.kt 2025-11-22 08:41:38 +01:00
OTKUSteyler
54312834b8
Rename ContributorListItem.kt.txt to ContributorListItem.kt. 2025-11-22 08:41:28 +01:00
OTKUSteyler
b99015fa99
Add files via upload 2025-11-22 08:40:57 +01:00
OTKUSteyler
87190ad7aa
Update AboutModel.kt 2025-11-22 08:39:23 +01:00
OTKUSteyler
13ec434a2d
Update AboutScreen.kt 2025-11-22 08:32:27 +01:00
OTKUSteyler
2ddd8b5964
Update Developers.kt 2025-11-22 08:27:38 +01:00
OTKUSteyler
974119863e
Update Developers.kt 2025-11-22 08:23:19 +01:00
OTKUSteyler
9783e33885
Rename Contributors.kt to Developers.kt 2025-11-22 01:37:10 +01:00
OTKUSteyler
855869f2d9
Update Developers.kt 2025-11-22 01:34:10 +01:00
OTKUSteyler
a6ab500b8a
Rename Contributors.kt to Developers.kt 2025-11-22 01:31:14 +01:00
OTKUSteyler
48de3a521e
Rename Developer.kt to Contributors.kt 2025-11-22 01:09:45 +01:00
OTKUSteyler
c1cf8365ff
Add files via upload 2025-11-21 23:42:44 +01:00
OTKUSteyler
38dec41e9e
Update HomeScreen.kt 2025-11-21 23:40:44 +01:00
OTKUSteyler
c41083a237
Update HomeScreenLoadedPreview.kt 2025-11-21 23:38:56 +01:00
OTKUSteyler
72b669d349
Update PathManager.kt 2025-11-21 23:36:23 +01:00
OTKUSteyler
d8e7984302
Update and rename Contributor.kt to Developer.kt 2025-11-21 23:35:25 +01:00
OTKUSteyler
3952c72540
Update ManagerApplication.kt 2025-11-21 23:32:54 +01:00
OTKUSteyler
e76bd45ef6
Update AboutModel.kt 2025-11-21 23:31:38 +01:00
OTKUSteyler
c67a6a116f
Update LeadContributor.kt 2025-11-21 23:30:28 +01:00
OTKUSteyler
7680cb7b3b
Update AboutModel.kt 2025-11-21 23:29:56 +01:00
OTKUSteyler
cbe6392946
Update AboutScreenState.kt 2025-11-21 23:28:20 +01:00
OTKUSteyler
2142ba837e
Update AboutScreen.kt 2025-11-21 23:27:55 +01:00
OTKUSteyler
de1cbb9009
Update InstalledItemCard.kt 2025-11-21 23:26:35 +01:00
OTKUSteyler
c5e3bd7c6b
Update HomeModel.kt 2025-11-21 23:25:30 +01:00
OTKUSteyler
2e8b3b07d8
Update build.gradle.kts 2025-11-21 23:18:10 +01:00
kmmiio99o
47737d1c19 `` feat: Bump app version to 1.1.1 `` - Fix: Use correct version name
- Remove: Unused code, plugins screen, and contributor model - Refactor:
Use new `Developer` model - Feat: Update about screen to display
developer list - Feat: Add github icon to plugin card - Chore: Fix
installer icon
2025-11-21 22:27:54 +01:00
OTKUSteyler
87d5c1efce
Update AboutScreen.kt 2025-11-20 13:27:41 +01:00
OTKUSteyler
37e4553df5
Update AboutScreen.kt 2025-11-20 13:26:03 +01:00
OTKUSteyler
383af45097
Update AboutScreen.kt 2025-11-20 13:21:09 +01:00
OTKUSteyler
0db19a65fe
Update AboutScreen.kt 2025-11-20 09:56:43 +01:00
OTKUSteyler
9cdb4e78e7
Update AboutScreen.kt 2025-11-19 23:28:58 +01:00
OTKUSteyler
e9bd7b6253
Update strings.xml 2025-11-19 23:28:32 +01:00
OTKUSteyler
8d2edb6f58
Update strings.xml 2025-11-19 23:26:38 +01:00
OTKUSteyler
e319e2b0f4
Update AboutScreen.kt 2025-11-19 23:22:40 +01:00
OTKUSteyler
9f87a3dff6
Update strings.xml 2025-11-19 23:18:21 +01:00
OTKUSteyler
8c2777e713
Update strings.xml 2025-11-19 23:16:25 +01:00
OTKUSteyler
a4cb364a24
Update AboutScreen.kt 2025-11-19 23:08:50 +01:00
OTKUSteyler
321b3ad3e0
Update AboutScreen.kt 2025-11-19 23:06:27 +01:00
OTKUSteyler
9185bfa37a
Update AboutScreen.kt 2025-11-19 23:03:11 +01:00
OTKUSteyler
8d4a93fc38
Update AboutScreen.kt 2025-11-19 23:00:32 +01:00
OTKUSteyler
770d624256
Update AboutScreen.kt 2025-11-19 22:56:21 +01:00
OTKUSteyler
aff5822da1
Update AboutScreen.kt 2025-11-19 22:52:28 +01:00
OTKUSteyler
ff545d86fc
Update AboutScreen.kt 2025-11-19 22:45:52 +01:00
OTKUSteyler
1e815a3991
Update AboutScreen.kt 2025-11-19 22:43:12 +01:00
OTKUSteyler
03300a8fbb
Update AboutScreen.kt 2025-11-19 22:36:07 +01:00
OTKUSteyler
5e961e4ec7
Update AboutScreen.kt 2025-11-19 22:34:56 +01:00
OTKUSteyler
8d2662f919
Update AboutModel.kt 2025-11-19 22:28:06 +01:00
OTKUSteyler
69829fa3cf
Update README.md 2025-11-11 09:45:17 +01:00
70 changed files with 645 additions and 1469 deletions

View file

@ -26,7 +26,7 @@ Goon Manager makes installing and managing Discord mods simple and safe. It's bu
<p><strong>Download & run</strong> — get a released APK from Releases and install with:</p>
Get newest apk from [release list](https://github.com/OTKUSteyler/GoonManager/releases/tag/v1.1.1) and install it.
Get newest apk from [release list](https://github.com/OTKUSteyler/GoonManager/releases/tag/v1.1.3) and install it.
<p><strong>Build from source</strong> — clone and assemble:</p>
@ -127,3 +127,5 @@ Goon Manager is open source under the OSL License. See `LICENSE` for details.
<p align="center">
<strong>Made with ❤️ for the GoonCord community — keep it playful, keep it safe.</strong>
</p>
![OTKUSteyler](https://count.getloli.com/@OTKUSteyler?name=OTKUSteyler&theme=miku&padding=7&offset=0&align=top&scale=1&pixelated=1&darkmode=1)

View file

@ -19,20 +19,20 @@ val gitHasLocalCommits = gitCurrentBranch?.let { providers.execIgnoreCode("git",
val gitHasHasLocalChanges = providers.execIgnoreCode("git", "status", "-s").isNotEmpty()
android {
namespace = "dev.goon.manager"
compileSdk = 36
namespace = "dev.shiggy.manager"
compileSdk = 37
defaultConfig {
minSdk = 28
targetSdk = 36
versionCode = 10_11_00
versionName = "1.1.0"
targetSdk = 37
versionCode = 10_12_01
versionName = "1.1.2"
vectorDrawables {
useSupportLibrary = true
}
buildConfigField("String", "APPLICATION_NAME", "\"Goon Manager\"")
buildConfigField("String", "TAG", "\"GoonManager\"")
buildConfigField("String", "APPLICATION_NAME", "\"Shiggy Manager\"")
buildConfigField("String", "TAG", "\"ShiggyManager\"")
buildConfigField("String", "GITHUB_ORG", "\"ShiggyCord\"")
buildConfigField("String", "SUPPORT_SERVER", "\"\"")

30
app/libs/convert-lspatch.sh Executable file
View file

@ -0,0 +1,30 @@
# Download LSPatch jar and convert it to an aar
# Jars cannot be used as a dependency without breaking R8 optimization
LSPATCH_URL="https://github.com/JingMatrix/LSPatch/releases/latest/download/lspatch.jar"
LSPATCH_FILE_NAME="lspatch"
function cleanup {
rm -rf $LSPATCH_FILE_NAME.jar META-INF AndroidManifest.xml classes.jar R.txt
}
cleanup
# download lspatch
curl -sSL -o $LSPATCH_FILE_NAME.jar $LSPATCH_URL
# prepare aar contents
unzip -q $LSPATCH_FILE_NAME.jar "META-INF/**"
mv $LSPATCH_FILE_NAME.jar classes.jar
touch R.txt
cat >AndroidManifest.xml <<EOF
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.lsposed.lspatch">
<uses-sdk android:minSdkVersion="28" android:targetSdkVersion="36" />
</manifest>
EOF
# add everything to aar
rm -f $LSPATCH_FILE_NAME.aar
zip -rq $LSPATCH_FILE_NAME.aar R.txt AndroidManifest.xml META-INF classes.jar
cleanup

Binary file not shown.

View file

@ -18,11 +18,11 @@
<application
android:name="com.aliucord.manager.ManagerApplication"
android:icon="@mipmap/ic_launcher"
android:icon="@drawable/ic_launcher"
android:label="@string/app_name"
android:largeHeap="true"
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:roundIcon="@drawable/ic_launcher_round"
android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.NoActionBar"
>

View file

@ -42,6 +42,7 @@ class MainActivity : ComponentActivity() {
ManagerTheme(
theme = preferences.theme,
dynamicColor = preferences.dynamicColor,
amoled = preferences.amoled,
) {
if (BuildConfig.RELEASE) {
UpdaterDialog()

View file

@ -17,7 +17,7 @@ import com.aliucord.manager.ui.screens.log.LogScreenModel
import com.aliucord.manager.ui.screens.logs.LogsListScreenModel
import com.aliucord.manager.ui.screens.patching.PatchingScreenModel
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsModel
import com.aliucord.manager.ui.screens.plugins.PluginsModel
import com.aliucord.manager.ui.screens.settings.SettingsModel
import com.aliucord.manager.ui.widgets.updater.UpdaterViewModel
import com.aliucord.manager.installers.root.RootInstaller
@ -57,7 +57,7 @@ class ManagerApplication : Application() {
// UI Models
modules(module {
factory { HomeModel(get<Application>(), get<AliucordGithubService>(), get<AliucordMavenService>(), get<ShiggyGithubService>(), get<RNATrackerService>(), get<kotlinx.serialization.json.Json>(), get<com.aliucord.manager.manager.PreferencesManager>()) }
factoryOf(::PluginsModel)
factoryOf(::AboutModel)
factoryOf(::PatchingScreenModel)
factory { SettingsModel(get<Application>(), get<PathManager>(), get<PreferencesManager>(), get<InstallerManager>()) }

View file

@ -11,28 +11,12 @@ import java.io.File
class PathManager(
private val context: Context,
) {
/**
* The Aliucord folder in which plugins/settings/themes are stored.
* Standard path: `~/Aliucord`
*/
val aliucordDir = Environment.getExternalStorageDirectory().resolve("Aliucord")
/**
* The Shiggy folder where shared Shiggy files are stored.
* Standard path: `~/Shiggy`
*/
val shiggyDir = Environment.getExternalStorageDirectory().resolve("Shiggy")
/**
* The directory in external storage in which plugins are stored by Aliucord.
*/
val pluginsDir = aliucordDir.resolve("plugins")
/**
* The settings file in which Aliucord's core uses.
*/
val coreSettingsFile = aliucordDir.resolve("settings/Aliucord.json")
/**
* Global keystore used for signing APKs.
*/

View file

@ -8,6 +8,7 @@ import com.aliucord.manager.ui.theme.Theme
@Stable
class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager(preferences) {
var theme by enumPreference("theme", Theme.System)
var amoled by booleanPreference("amoled", false)
var dynamicColor by booleanPreference("dynamic_color", true)
var devMode by booleanPreference("dev_mode", false)
var installer by enumPreference<InstallerSetting>("installer", InstallerSetting.PM)

View file

@ -16,11 +16,13 @@ import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.HttpHeaders
import io.ktor.http.contentLength
import io.ktor.http.isSuccess
import io.ktor.utils.io.readAvailable
import kotlinx.coroutines.CancellationException
import java.io.File
import java.io.IOException
import java.net.SocketTimeoutException
import java.net.URI
/**
* Handle downloading remote urls to a path with Ktor.
@ -39,7 +41,38 @@ class KtorDownloadManager(
.apply { exists() || createNewFile() }
try {
val httpStmt = http.prepareGet(url) {
val downloadUrl = rewriteRawGithubUrl(url)
// Try the CDN first (jsDelivr), then an alternate raw proxy (raw.githack.com), and finally
// fall back to the original URL if all CDN attempts fail.
val rawGithack = try {
when {
// direct raw.githubusercontent.com -> raw.githack.com
url.contains("raw.githubusercontent.com", ignoreCase = true) ->
url.replace("raw.githubusercontent.com", "raw.githack.com")
// github.com/.../raw/... -> convert to raw.githack.com/ORG/REPO/BRANCH/path
url.contains("/raw/", ignoreCase = true) && url.contains("github.com", ignoreCase = true) -> {
try {
val u = URI(url)
val segs = u.path.trimStart('/').split('/', limit = 6)
if (segs.size >= 5 && segs[2].equals("raw", ignoreCase = true)) {
"https://raw.githack.com/${segs[0]}/${segs[1]}/${segs.subList(3, segs.size).joinToString("/")}"
} else url
} catch (_: Throwable) {
url
}
}
else -> url
}
} catch (_: Throwable) {
url
}
val attempts = listOf(downloadUrl, rawGithack, url).distinct()
var lastException: Throwable? = null
var downloaded = false
for (attemptUrl in attempts) {
val httpStmt = http.prepareGet(attemptUrl) {
header(HttpHeaders.CacheControl, "no-cache, no-store")
// Disable compression due to bug on emulators
@ -49,7 +82,12 @@ class KtorDownloadManager(
}
}
try {
httpStmt.execute { resp ->
if (!resp.status.isSuccess()) {
throw IOException("HTTP error ${resp.status.value} for $attemptUrl")
}
val channel = resp.bodyAsChannel()
val total = resp.contentLength() ?: 0
var retrieved = 0L
@ -89,6 +127,23 @@ class KtorDownloadManager(
}
}
}
// If we reach here, the attempt succeeded
downloaded = true
break
} catch (e: CancellationException) {
// Propagate cancellations immediately
throw e
} catch (t: Throwable) {
// Record the last exception and try the next URL (if any)
lastException = t
}
}
if (!downloaded) {
// No attempt succeeded, throw the last failure to be handled by outer catch blocks
throw lastException ?: IOException("Failed to download resource from $url")
}
} catch (_: CancellationException) {
tmpOut.delete()
return Result.Cancelled(systemTriggered = false)
@ -107,6 +162,17 @@ class KtorDownloadManager(
return Result.Success(out)
}
private fun rewriteRawGithubUrl(url: String): String {
val regex = Regex("^https?://raw\\.githubusercontent\\.com/([^/]+)/([^/]+)/([^/]+)/(.*)$", RegexOption.IGNORE_CASE)
val match = regex.matchEntire(url)
return if (match != null) {
val (org, repo, branch, path) = match.destructured
"https://cdn.jsdelivr.net/gh/$org/$repo@$branch/$path"
} else {
url
}
}
/**
* Wrapper around an exception that occurred from invoking Ktor
*/

View file

@ -1,23 +0,0 @@
package com.aliucord.manager.network.models
import androidx.compose.runtime.Immutable
import com.aliucord.manager.util.serialization.ImmutableListSerializer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.serialization.Serializable
@Immutable
@Serializable
data class Contributor(
val username: String,
val avatarUrl: String,
val commits: Int,
@Serializable(with = ImmutableListSerializer::class)
val repositories: ImmutableList<Repository>,
) {
@Immutable
@Serializable
data class Repository(
val name: String,
val commits: Int,
)
}

View file

@ -0,0 +1,18 @@
package com.aliucord.manager.network.models
import kotlinx.serialization.Serializable
@Serializable
data class Developer(
val username: String,
val avatarUrl: String,
val commits: Int,
val repositories: List<Repository>,
val role: String? = null
) {
@Serializable
data class Repository(
val name: String,
val commits: Int
)
}

View file

@ -6,6 +6,9 @@ import kotlinx.serialization.Serializable
/**
* Data stored inside patched APKs as ~~"aliucord.json"~~ "shiggy.json" in order to preserve install-time information about Aliucord and the Manager.
* Metadata about an Aliucord installation embedded in the APK.
* @param options The patch options used for this installation
* @param goonXposedVersion The version of GoonXposed used for this installation
*/
@Serializable
data class InstallMetadata(
@ -47,5 +50,5 @@ data class InstallMetadata(
/**
* Version of ShiggyXposed embedded into the APK.
*/
val shiggyXposedVersion: SemVer?,
val goonXposedVersion: SemVer,
)

View file

@ -22,7 +22,7 @@ class DownloadShiggyXposedStep : DownloadStep() {
override suspend fun execute(container: StepRunner) {
val latestRelease = shiggyGithubService.getLatestXposedRelease().getOrThrow()
container.log("Latest ShiggyXposed release is ${latestRelease.name}")
container.log("Latest GoonXposed release is ${latestRelease.name}")
targetVersion = SemVer.parse(latestRelease.name)
targetUrl = latestRelease.assets.first { it.name == "app-release.apk" }.browserDownloadUrl

View file

@ -13,10 +13,11 @@ import org.lsposed.lspatch.share.LSPConfig
import org.lsposed.patch.LSPatch
import org.lsposed.patch.util.Logger
import java.io.File
import java.util.*
class InjectShiggyXposedStep : Step() {
class InjectGoonXposedStep : Step() {
override val group: StepGroup = StepGroup.Install
override val localizedName: Int = R.string.patch_step_inject_shiggy
override val localizedName: Int = R.string.patch_step_inject_goon
suspend fun patch(
container: StepRunner,
@ -82,7 +83,15 @@ class InjectShiggyXposedStep : Step() {
// FilenameUtils.getBaseName(apkFileName),
// LSPConfig.instance.VERSION_CODE)
// )
val patchedApkName = "${baseName}-${LSPConfig.instance.VERSION_CODE}-lspatched.apk"
// Note: LSPatch uses Locale.getDefault() which can produce locale-specific numerals
// (e.g., Arabic numerals on Arabic locales). We must use Locale.US to ensure
// consistent filename matching regardless of device locale.
val patchedApkName = String.format(
Locale.US,
"%s-%d-lspatched.apk",
baseName,
LSPConfig.instance.VERSION_CODE
)
val patchedApk = File(tempDir, patchedApkName)
if (patchedApk.exists()) {

View file

@ -31,7 +31,7 @@ class SaveMetadataStep(private val options: PatchOptions) : Step(), KoinComponen
val aliuhook = container.getStepOrNull<DownloadAliuhookStep>()
val injector = container.getStepOrNull<DownloadInjectorStep>()
val patches = container.getStepOrNull<DownloadPatchesStep>()
val shiggyXposed = container.getStepOrNull<DownloadShiggyXposedStep>()
val goonXposed = container.getStepOrNull<DownloadShiggyXposedStep>()
val metadata = InstallMetadata(
options = options,
@ -41,12 +41,12 @@ class SaveMetadataStep(private val options: PatchOptions) : Step(), KoinComponen
injectorVersion = injector?.targetVersion,
patchesVersion = patches?.targetVersion,
lspatchVersion = LSPConfig.instance.VERSION_CODE,
shiggyXposedVersion = shiggyXposed?.targetVersion,
goonXposedVersion = goonXposed?.targetVersion ?: SemVer.parse("0.0.0"), // Provide a default if null
)
container.log("Writing serialized install metadata to APK")
ZipWriter(apk, /* append = */ true).use {
it.writeEntry("shiggy.json", json.encodeToString<InstallMetadata>(metadata))
it.writeEntry("goon.json", json.encodeToString<InstallMetadata>(metadata))
}
}
}

View file

@ -252,6 +252,8 @@ object ManifestPatcher {
extractNativeLibs ?: false
private var shouldAddMetadata =
addManagerMetadata ?: false
private var shouldAddCleartext = true
override fun attr(
ns: String?,
@ -269,6 +271,12 @@ object ManifestPatcher {
shouldAddExtractNativeLibs = false
if (name == DEBUGGABLE)
shouldAddDebuggable = false
if (name == "usesCleartextTraffic") {
shouldAddCleartext = false
super.attr(ns, name, 0x010104ec, TYPE_INT_BOOLEAN, 1)
return
}
super.attr(
ns,
name,
@ -425,7 +433,15 @@ object ManifestPatcher {
0
)
}
if (shouldAddCleartext) {
super.attr(
ANDROID_NAMESPACE,
"usesCleartextTraffic",
0x010104ec,
TYPE_INT_BOOLEAN,
1
)
}
super.end()
}
}

View file

@ -17,13 +17,13 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.unit.dp
import coil3.compose.SubcomposeAsyncImage
import com.aliucord.manager.network.models.Contributor
import com.aliucord.manager.network.models.Developer
import com.valentinilk.shimmer.shimmer
import dev.goon.manager.R
@Composable
fun ContributorCommitsItem(
user: Contributor,
user: Developer,
modifier: Modifier = Modifier,
) {
val uriHandler = LocalUriHandler.current

View file

@ -1,20 +0,0 @@
package com.aliucord.manager.ui.previews.dialogs
import android.content.res.Configuration
import androidx.compose.runtime.Composable
import androidx.compose.ui.tooling.preview.Preview
import com.aliucord.manager.ui.screens.plugins.components.dialogs.UninstallPluginDialog
import com.aliucord.manager.ui.theme.ManagerTheme
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun UninstallPluginDialogPreview() {
ManagerTheme {
UninstallPluginDialog(
pluginName = "FakeNitro",
onConfirm = {},
onDismiss = {},
)
}
}

View file

@ -1,52 +0,0 @@
package com.aliucord.manager.ui.previews.screens.about
import android.content.res.Configuration
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.*
import com.aliucord.manager.network.models.Contributor
import com.aliucord.manager.ui.screens.about.AboutScreenContent
import com.aliucord.manager.ui.screens.about.AboutScreenState
import com.aliucord.manager.ui.theme.ManagerTheme
import com.aliucord.manager.ui.util.emptyImmutableList
import com.aliucord.manager.util.serialization.ImmutableListSerializer
import kotlinx.collections.immutable.*
import kotlinx.serialization.json.Json
// This preview has scrollable content that cannot be properly viewed from an IDE preview
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun AboutScreenLoadedPreview(
@PreviewParameter(ContributorsProvider::class) contributors: ImmutableList<Contributor>,
) {
ManagerTheme {
AboutScreenContent(
state = remember { mutableStateOf(AboutScreenState.Loaded(contributors)) },
)
}
}
private class ContributorsProvider : PreviewParameterProvider<ImmutableList<Contributor>> {
private val realDataRaw =
"[{\"username\":\"maisymoe\",\"avatarUrl\":\"https://avatars.githubusercontent.com/u/61095277?v=4\",\"repositories\":]"
private val realData =
Json.decodeFromString(ImmutableListSerializer(Contributor.serializer()), realDataRaw)
override val values =
sequenceOf(
emptyImmutableList<Contributor>(),
persistentListOf(
Contributor(
username =
"abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz",
avatarUrl = "UNUSED",
commits = Int.MAX_VALUE,
repositories =
(realData[0].repositories + realData[0].repositories)
.toImmutableList(),
)
),
realData,
)
}

View file

@ -34,7 +34,6 @@ private fun HomeScreenLoadedPreview(
onUpdate = {},
onOpenApp = {},
onOpenAppInfo = {},
onOpenPlugins = {},
onUninstall = {},
)
}

View file

@ -1,29 +0,0 @@
package com.aliucord.manager.ui.previews.screens.plugins
import android.content.res.Configuration
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.Preview
import com.aliucord.manager.ui.screens.plugins.PluginsScreenContent
import com.aliucord.manager.ui.theme.ManagerTheme
import com.aliucord.manager.ui.util.emptyImmutableList
// This preview has interactable content that cannot be tested from an IDE preview
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun PluginsScreenFailedPreview() {
val filterState = remember { mutableStateOf("") }
ManagerTheme {
PluginsScreenContent(
searchText = filterState,
setSearchText = filterState::value::set,
isError = true,
plugins = emptyImmutableList(),
onPluginUninstall = {},
onPluginChangelog = {},
onPluginToggle = { name, enabled -> },
)
}
}

View file

@ -1,92 +0,0 @@
package com.aliucord.manager.ui.previews.screens.plugins
import android.content.res.Configuration
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.Preview
import com.aliucord.manager.ui.screens.plugins.PluginsScreenContent
import com.aliucord.manager.ui.screens.plugins.model.PluginItem
import com.aliucord.manager.ui.screens.plugins.model.PluginManifest
import com.aliucord.manager.ui.theme.ManagerTheme
import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf
import java.util.UUID
// This preview has scrollable/interactable content that cannot be tested from an IDE preview
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun PluginsScreenLoadedPreview() {
val filterState = remember { mutableStateOf("test") }
ManagerTheme {
PluginsScreenContent(
searchText = filterState,
setSearchText = filterState::value::set,
isError = false,
plugins = plugins,
onPluginUninstall = {},
onPluginChangelog = {},
onPluginToggle = { name, enabled -> },
)
}
}
private val plugins: ImmutableList<PluginItem> = persistentListOf(
PluginItem(
path = UUID.randomUUID().toString(),
manifest = PluginManifest(
name = "CloseDMs",
authors = persistentListOf(
PluginManifest.Author(name = "Diamond", id = 0L),
),
description = "Shortcut to close DMs in the DM context menu.",
version = "1.0.0",
updateUrl = "",
changelog = "",
changelogMedia = null,
)
),
PluginItem(
path = UUID.randomUUID().toString(),
manifest = PluginManifest(
name = "ConfigurableStickerSizes",
authors = persistentListOf(
PluginManifest.Author(name = "rushii", id = 0L, hyperlink = false),
),
description = "Makes sticker sizes configurable.",
version = "1.1.5",
updateUrl = "",
changelog = "",
changelogMedia = null,
)
),
PluginItem(
path = UUID.randomUUID().toString(),
manifest = PluginManifest(
name = "AudioPlayer",
authors = persistentListOf(
PluginManifest.Author(name = "rushii", id = 0L, hyperlink = false),
),
description = "Makes audio files playable.",
version = "0.0.1",
updateUrl = "",
changelog = "",
changelogMedia = null,
)
),
PluginItem(
path = UUID.randomUUID().toString(),
manifest = PluginManifest(
name = "TypingIndicators",
authors = persistentListOf(
PluginManifest.Author(name = "rushii", id = 0L, hyperlink = false),
),
description = "Adds typing indicators to channels that people are currently typing in.",
version = "1.1.0",
updateUrl = "",
changelog = "",
changelogMedia = null,
)
),
)

View file

@ -1,29 +0,0 @@
package com.aliucord.manager.ui.previews.screens.plugins
import android.content.res.Configuration
import androidx.compose.runtime.*
import androidx.compose.ui.tooling.preview.Preview
import com.aliucord.manager.ui.screens.plugins.PluginsScreenContent
import com.aliucord.manager.ui.theme.ManagerTheme
import com.aliucord.manager.ui.util.emptyImmutableList
// This preview has interactable content that cannot be tested from an IDE preview
@Composable
@Preview(uiMode = Configuration.UI_MODE_NIGHT_YES)
@Preview(uiMode = Configuration.UI_MODE_NIGHT_NO)
private fun PluginsScreenNonePreview() {
val filterState = remember { mutableStateOf("") }
ManagerTheme {
PluginsScreenContent(
searchText = filterState,
setSearchText = filterState::value::set,
isError = false,
plugins = emptyImmutableList(),
onPluginUninstall = {},
onPluginChangelog = {},
onPluginToggle = { name, enabled -> },
)
}
}

View file

@ -2,56 +2,67 @@ package com.aliucord.manager.ui.screens.about
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.network.models.Contributor
import com.aliucord.manager.network.models.Developer
import com.aliucord.manager.network.services.HttpService
import com.aliucord.manager.network.utils.fold
import com.aliucord.manager.ui.util.toUnsafeImmutable
import com.aliucord.manager.util.launchIO
import io.ktor.client.request.url
import kotlinx.serialization.Serializable
import kotlinx.serialization.SerialName
import kotlinx.collections.immutable.persistentListOf
class AboutModel(
private val http: HttpService,
) : StateScreenModel<AboutScreenState>(AboutScreenState.Loading) {
init {
fetchContributors()
fetchDevelopers()
}
fun fetchContributors() = screenModelScope.launchIO {
fun fetchDevelopers() = screenModelScope.launchIO {
mutableState.value = AboutScreenState.Loading
// Use a curated static list of GitHub usernames (profiles + static avatar URLs).
// This avoids pulling from a specific repo and lets us include people
// who are not contributors to any particular repository.
val staticContributors = persistentListOf(
Contributor(
username = "rushiiMachine",
avatarUrl = "https://github.com/rushiiMachine.png",
val allDevelopers = persistentListOf(
Developer(
username = "OTKUSteyler",
avatarUrl = "https://github.com/OTKUSteyler.png",
commits = 0,
repositories = persistentListOf<com.aliucord.manager.network.models.Contributor.Repository>()
repositories = emptyList(),
role = "GoonCord - Creator"
),
Contributor(
username = "maisymoe",
avatarUrl = "https://github.com/maisymoe.png",
commits = 0,
repositories = persistentListOf<com.aliucord.manager.network.models.Contributor.Repository>()
),
Contributor(
Developer(
username = "kmmiio99o",
avatarUrl = "https://github.com/kmmiio99o.png",
commits = 0,
repositories = persistentListOf<com.aliucord.manager.network.models.Contributor.Repository>()
repositories = emptyList(),
role = "ShiggyCord - Creator"
),
Contributor(
Developer(
username = "rushiiMachine",
avatarUrl = "https://github.com/rushiiMachine.png",
commits = 0,
repositories = emptyList(),
role = "Manager - Creator"
),
Developer(
username = "maisymoe",
avatarUrl = "https://github.com/maisymoe.png",
commits = 0,
repositories = emptyList(),
role = "Vendetta - Creator"
),
Developer(
username = "pylixonly",
avatarUrl = "https://github.com/pylixonly.png",
commits = 0,
repositories = persistentListOf<com.aliucord.manager.network.models.Contributor.Repository>()
)
repositories = emptyList(),
role = "Bunny - Creator"
),
Developer(
username = "jonatanktk",
avatarUrl = "https://github.com/jonatanktk.png",
commits = 0,
repositories = emptyList(),
role = "Random Person there"
),
)
mutableState.value = AboutScreenState.Loaded(staticContributors.toUnsafeImmutable())
mutableState.value = AboutScreenState.Loaded(allDevelopers.toUnsafeImmutable())
}
}

View file

@ -10,6 +10,7 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
@ -20,7 +21,7 @@ import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel
import com.aliucord.manager.ui.components.*
import com.aliucord.manager.ui.screens.about.components.LeadContributor
import com.aliucord.manager.ui.screens.about.components.RegularContributorListItem
import com.aliucord.manager.ui.util.paddings.*
import dev.goon.manager.R
import kotlinx.parcelize.IgnoredOnParcel
@ -28,7 +29,8 @@ import kotlinx.parcelize.Parcelize
@Parcelize
class AboutScreen : Screen, Parcelable {
@IgnoredOnParcel override val key = "About"
@IgnoredOnParcel
override val key = "About"
@Composable
override fun Content() {
@ -51,96 +53,81 @@ fun AboutScreenContent(state: State<AboutScreenState>) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp),
horizontalAlignment = Alignment.CenterHorizontally,
contentPadding =
paddingValues
contentPadding = paddingValues
.exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top)
.add(PaddingValues(vertical = 16.dp)),
modifier =
Modifier.padding(paddingValues.exclude(PaddingValuesSides.Bottom))
.padding(horizontal = 14.dp),
modifier = Modifier
.padding(paddingValues.exclude(PaddingValuesSides.Bottom))
) {
item(key = "MANAGER_CREDIT_DISCLAIMER_BANNER") {
Box(
contentAlignment = Alignment.Center,
modifier =
Modifier.padding(bottom = 24.dp)
modifier = Modifier
.padding(horizontal = 16.dp, vertical = 8.dp)
.fillMaxWidth()
.border(
width = 2.dp,
color = MaterialTheme.colorScheme.surfaceVariant,
shape = MaterialTheme.shapes.medium
width = 1.dp,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f),
shape = MaterialTheme.shapes.large
)
.clip(MaterialTheme.shapes.medium)
.background(MaterialTheme.colorScheme.surfaceContainerHigh)
) {
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp),
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 14.dp)
.clip(MaterialTheme.shapes.large)
.background(MaterialTheme.colorScheme.surfaceContainerLow)
) {
Text(
text =
stringResource(
R.string.about_manager_credit_disclaimer_notice
),
style = MaterialTheme.typography.labelMedium,
text = stringResource(R.string.about_manager_credit_disclaimer_notice),
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp)
)
}
}
}
item(key = "PROJECT_HEADER") { ProjectHeader() }
item(key = "HEADER_DIVIDER") {
TextDivider(
text = stringResource(R.string.contributors_lead),
modifier = Modifier.padding(top = 18.dp, bottom = 20.dp),
)
}
item(key = "MAIN_CONTRIBUTORS") {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
LeadContributor("C0C0B01", stringResource(R.string.lead_role_kettu))
LeadContributor("kmmiio99o", stringResource(R.string.lead_role_shiggycord))
LeadContributor("pylixonly", stringResource(R.string.lead_role_bunny))
item(key = "PROJECT_HEADER") {
Box(modifier = Modifier.padding(horizontal = 16.dp)) {
ProjectHeader()
}
}
item(key = "CONTRIBUTORS_DIVIDER") {
TextDivider(
text = stringResource(R.string.contributors),
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
)
item(key = "TEAM_SPACER") {
Spacer(modifier = Modifier.height(32.dp))
}
when (val state = state.value) {
AboutScreenState.Loading ->
item(key = "CONTRIBUTIONS_LOADING") {
when (val currentState = state.value) {
AboutScreenState.Loading -> {
item(key = "LOADING") {
Box(
contentAlignment = Alignment.Center,
content = { CircularProgressIndicator() },
modifier = Modifier.fillMaxWidth().padding(vertical = 38.dp),
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 48.dp)
) {
CircularProgressIndicator()
}
}
}
AboutScreenState.Failure -> {
item(key = "LOAD_FAILURE") {
Box(
contentAlignment = Alignment.Center,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 48.dp)
) {
LoadFailure()
}
}
}
is AboutScreenState.Loaded -> {
itemsIndexed(
items = currentState.contributors,
key = { index, developer -> developer.username }
) { index, developer ->
RegularContributorListItem(
contributor = developer,
isLast = index == currentState.contributors.lastIndex
)
}
AboutScreenState.Failure ->
item(key = "LOAD_FAILURE") {
LoadFailure(modifier = Modifier.fillMaxSize().padding(vertical = 38.dp))
}
is AboutScreenState.Loaded -> {
// Render the two requested contributors as LeadContributor entries
item(key = "SELECTED_CONTRIBUTORS") {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.fillMaxWidth(),
) {
LeadContributor("maisymoe", "Creator - Vendetta")
LeadContributor("rushiiMachine", "Manager - Creator")
}
}
}
}
}

View file

@ -1,10 +1,10 @@
package com.aliucord.manager.ui.screens.about
import com.aliucord.manager.network.models.Contributor
import com.aliucord.manager.network.models.Developer
import kotlinx.collections.immutable.ImmutableList
sealed interface AboutScreenState {
data object Loading : AboutScreenState
data object Failure : AboutScreenState
data class Loaded(val contributors: ImmutableList<Contributor>) : AboutScreenState
data class Loaded(val contributors: ImmutableList<Developer>) : AboutScreenState
}

View file

@ -0,0 +1,96 @@
package com.aliucord.manager.ui.screens.about.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import coil3.compose.SubcomposeAsyncImage
import com.aliucord.manager.network.models.Developer
import com.valentinilk.shimmer.shimmer
@Composable
fun RegularContributorListItem(
contributor: Developer,
isLast: Boolean = false,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
Column(modifier = modifier.fillMaxWidth()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { uriHandler.openUri("https://github.com/${contributor.username}") }
)
.padding(horizontal = 24.dp, vertical = 16.dp)
) {
SubcomposeAsyncImage(
model = contributor.avatarUrl,
contentDescription = contributor.username,
contentScale = ContentScale.Crop,
loading = {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.shimmer()
) {}
},
error = {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
) {}
},
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
)
Spacer(modifier = Modifier.width(20.dp))
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = contributor.username,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
contributor.role?.let { role ->
Text(
text = role,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
if (!isLast) {
HorizontalDivider(
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.1f),
thickness = 1.dp,
modifier = Modifier.padding(start = 100.dp, end = 24.dp)
)
}
}
}

View file

@ -1,94 +1,96 @@
package com.aliucord.manager.ui.screens.about.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import coil3.compose.SubcomposeAsyncImage
import com.aliucord.manager.network.models.Developer
import com.valentinilk.shimmer.shimmer
@Composable
fun LeadContributor(
name: String,
roles: String,
username: String = name,
modifier: Modifier = Modifier,
fun LeadContributorItem(
contributor: Developer,
isLast: Boolean = false,
modifier: Modifier = Modifier
) {
val uriHandler = LocalUriHandler.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier =
modifier.clickable(
onClick = {
uriHandler.openUri("https://github.com/$username")
},
indication = ripple(bounded = false, radius = 90.dp),
interactionSource = remember(::MutableInteractionSource)
Column(modifier = modifier.fillMaxWidth()) {
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.clickable(
onClick = { uriHandler.openUri("https://github.com/${contributor.username}") }
)
.widthIn(min = 100.dp)
.padding(horizontal = 24.dp, vertical = 16.dp)
) {
SubcomposeAsyncImage(
model = "https://github.com/$username.png",
contentDescription = username,
model = contributor.avatarUrl,
contentDescription = contributor.username,
contentScale = ContentScale.Crop,
loading = {
Surface(
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
.shimmer()
) {}
},
error = {
Surface(
content = {},
tonalElevation = 2.dp,
modifier = Modifier.fillMaxSize().shimmer(),
)
color = MaterialTheme.colorScheme.surfaceVariant,
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
) {}
},
modifier = Modifier.size(80.dp).clip(CircleShape),
modifier = Modifier
.size(56.dp)
.clip(CircleShape)
)
Column(horizontalAlignment = Alignment.CenterHorizontally) {
Text(text = name, style = MaterialTheme.typography.titleMedium.copy(fontSize = 18.sp))
Spacer(modifier = Modifier.width(20.dp))
val roleParts = roles.split(Regex("\\s*-\\s*"), limit = 2)
if (roleParts.size == 2) {
// Render left and right parts on two separate lines, removing the dash
Column(
modifier = Modifier.weight(1f)
) {
Text(
text = roleParts[0],
style =
MaterialTheme.typography.titleSmall.copy(
color =
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.6f
)
)
text = contributor.username,
style = MaterialTheme.typography.titleMedium,
color = MaterialTheme.colorScheme.onSurface,
maxLines = 1,
overflow = TextOverflow.Ellipsis
)
Spacer(modifier = Modifier.height(4.dp))
contributor.role?.let { role ->
Text(
text = roleParts[1],
style =
MaterialTheme.typography.bodySmall.copy(
color =
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.6f
)
)
)
} else {
Text(
text = roles,
style =
MaterialTheme.typography.titleSmall.copy(
color =
MaterialTheme.colorScheme.onSurface.copy(
alpha = 0.6f
)
)
text = role,
style = MaterialTheme.typography.bodyMedium,
color = MaterialTheme.colorScheme.onSurfaceVariant
)
}
}
}
if (!isLast) {
HorizontalDivider(
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.1f),
thickness = 1.dp,
modifier = Modifier.padding(start = 100.dp, end = 24.dp)
)
}
}
}

View file

@ -51,7 +51,7 @@ class HomeModel(
private val refreshingLock = Mutex()
private var remoteDataJson: BuildInfo? = null
private var trackerIndexJson: RNATrackerIndex? = null
private var latestShiggyXposedVersion: SemVer? = null
private var latestGoonXposedVersion: SemVer? = null
private var latestAliuhookVersion: SemVer? = null
init {
@ -122,7 +122,7 @@ class HomeModel(
val metadata = try {
val applicationInfo = application.packageManager.getApplicationInfo(packageName, 0)
val metadataFile = ZipReader(applicationInfo.publicSourceDir)
.use { it.openEntry("shiggy.json")?.read() }
.use { it.openEntry("goon.json")?.read() }
@OptIn(ExperimentalSerializationApi::class)
metadataFile?.let { json.decodeFromStream<InstallMetadata>(it.inputStream()) }
@ -178,7 +178,9 @@ class HomeModel(
InstallData(
name = name,
packageName = pkg.packageName,
isUpToDate = try { isInstallationUpToDate(pkg) } catch (e: Exception) {
isUpToDate = try {
isInstallationUpToDate(pkg)
} catch (e: Exception) {
Log.w(BuildConfig.TAG, "Failed to check up-to-date for ${pkg.packageName}", e)
false
},
@ -234,7 +236,7 @@ class HomeModel(
private suspend fun fetchRemoteData() {
listOf(
// // These aren't needed by Shiggy
// // These aren't needed by Goon
// screenModelScope.launch(Dispatchers.IO) {
// github.getBuildData().fold(
// success = { remoteDataJson = it },
@ -258,15 +260,15 @@ class HomeModel(
success = {
val versionName = it.name
val truncatedVersion = versionName.split(".").take(3).joinToString(".")
latestShiggyXposedVersion = SemVer.parse(truncatedVersion)
latestGoonXposedVersion = SemVer.parse(truncatedVersion)
},
fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest ShiggyXposed version", it) },
fail = { Log.w(BuildConfig.TAG, "Failed to fetch latest GoonXposed version", it) },
)
},
).joinAll()
if (trackerIndexJson == null /* remoteDataJson == null || latestAliuhookVersion == null */ || latestShiggyXposedVersion == null) {
if (trackerIndexJson == null /* remoteDataJson == null || latestAliuhookVersion == null */ || latestGoonXposedVersion == null) {
mainThread { application.showToast(R.string.home_network_fail) }
}
}
@ -305,7 +307,7 @@ class HomeModel(
return application.packageManager
.getInstalledPackages(PackageManager.GET_META_DATA)
.filter {
// Shiggy doesn't have "legacy installer" whatsoever
// Goon doesn't have "legacy installer" whatsoever
return@filter it.applicationInfo?.metaData?.containsKey("isShiggy") == true
// // Packages installed via the legacy Installer do not have the metadata marker
@ -316,11 +318,11 @@ class HomeModel(
}
/**
* Checks whether the current Shiggy installation is up-to-date.
* Checks whether the current Goon installation is up-to-date.
*
* Currently mirrors the behavior of Bunny Manager by directly comparing against
* the latest available Discord and ShiggyXposed release. This is a temporary approach and will remain
* in place until Shiggy adds support for version pinning.
* the latest available Discord and GoonXposed release. This is a temporary approach and will remain
* in place until Goon adds support for version pinning.
*/
private fun isInstallationUpToDate(pkg: PackageInfo): Boolean? {
val trackerData = trackerIndexJson ?: return null
@ -332,7 +334,7 @@ class HomeModel(
// Try to parse install metadata. If none present, install was made via legacy installer.
val apkPath = pkg.applicationInfo?.publicSourceDir ?: return false
val installMetadata = try {
val metadataFile = ZipReader(apkPath).use { it.openEntry("shiggy.json")?.read() }
val metadataFile = ZipReader(apkPath).use { it.openEntry("goon.json")?.read() }
?: return false
@OptIn(ExperimentalSerializationApi::class)
@ -349,6 +351,6 @@ class HomeModel(
VersionPreference.Custom -> return true
}
return latestByPref == versionCode && installMetadata.shiggyXposedVersion == latestShiggyXposedVersion
return latestByPref == versionCode && installMetadata.goonXposedVersion == latestGoonXposedVersion
}
}

View file

@ -30,7 +30,6 @@ import com.aliucord.manager.ui.components.LoadFailure
import com.aliucord.manager.ui.components.ProjectHeader
import com.aliucord.manager.ui.screens.home.components.*
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsScreen
import com.aliucord.manager.ui.screens.plugins.PluginsScreen
import com.aliucord.manager.ui.util.paddings.PaddingValuesSides
import com.aliucord.manager.ui.util.paddings.exclude
import com.aliucord.manager.util.*
@ -78,7 +77,6 @@ class HomeScreen : Screen, Parcelable {
},
onOpenApp = model::openApp,
onOpenAppInfo = model::openAppInfo,
onOpenPlugins = { navigator.push(PluginsScreen()) }, // TODO: install-specific plugins
onUninstall = model::uninstallApp,
)
@ -129,7 +127,6 @@ fun HomeScreenLoadedContent(
onUpdate: (packageName: String) -> Unit,
onOpenApp: (packageName: String) -> Unit,
onOpenAppInfo: (packageName: String) -> Unit,
onOpenPlugins: (packageName: String) -> Unit,
onUninstall: (packageName: String) -> Unit,
) {
LazyColumn(
@ -163,7 +160,6 @@ fun HomeScreenLoadedContent(
onUpdate = { onUpdate(item.packageName) },
onOpenApp = { onOpenApp(item.packageName) },
onOpenInfo = { onOpenAppInfo(item.packageName) },
onOpenPlugins = { onOpenPlugins(item.packageName) },
onUninstall = { onUninstall(item.packageName) },
modifier = Modifier.fillMaxWidth()
)

View file

@ -26,7 +26,6 @@ fun InstalledItemCard(
onUpdate: () -> Unit,
onOpenApp: () -> Unit,
onOpenInfo: () -> Unit,
onOpenPlugins: () -> Unit,
onUninstall: () -> Unit,
modifier: Modifier = Modifier,
) {
@ -89,13 +88,6 @@ fun InstalledItemCard(
horizontalArrangement = Arrangement.spacedBy(2.dp),
modifier = Modifier.clip(MaterialTheme.shapes.large),
) {
// // Plugins cannot be managed externally yet in Shiggy
// SegmentedButton(
// icon = painterResource(R.drawable.ic_extension),
// text = stringResource(R.string.plugins_title),
// onClick = onOpenPlugins,
// )
// If the up-to-date status cannot be determined, assume it is up-to-date
if (data.isUpToDate != false)
SegmentedButton(
@ -126,7 +118,7 @@ fun InstalledItemCard(
)
SegmentedButton(
icon = painterResource(R.drawable.ic_delete_forever),
text = stringResource(R.string.action_uninstall), // Assuming R.string.action_uninstall exists or will be added
text = stringResource(R.string.action_uninstall),
iconColor = MaterialTheme.colorScheme.error,
textColor = MaterialTheme.colorScheme.error,
onClick = onUninstall,

View file

@ -1,203 +0,0 @@
package com.aliucord.manager.ui.screens.plugins
import android.app.Application
import android.util.Log
import androidx.compose.runtime.*
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.manager.PathManager
import com.aliucord.manager.ui.screens.plugins.model.PluginItem
import com.aliucord.manager.ui.screens.plugins.model.PluginManifest
import com.aliucord.manager.ui.util.emptyImmutableList
import com.aliucord.manager.ui.util.toUnsafeImmutable
import com.aliucord.manager.util.*
import com.github.diamondminer88.zip.ZipReader
import dev.goon.manager.BuildConfig
import dev.goon.manager.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.*
import kotlinx.coroutines.plus
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.sync.withLock
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.*
import java.io.File
import kotlin.time.Duration
class PluginsModel(
private val context: Application,
private val paths: PathManager,
private val json: Json,
) : ScreenModel {
private val plugins = MutableStateFlow<ImmutableList<PluginItem>>(emptyImmutableList())
private val aliucordJsonMutex = Mutex()
var error by mutableStateOf(false)
private set
var showChangelogDialog by mutableStateOf<PluginItem?>(null)
private set
var showUninstallDialog by mutableStateOf<PluginItem?>(null)
private set
val searchText: StateFlow<String>
private field = MutableStateFlow("")
val filteredPlugins: StateFlow<ImmutableList<PluginItem>> = searchText
.combine(plugins) { searchText, plugins ->
if (searchText.isBlank()) {
plugins
} else {
plugins.filter { plugin ->
plugin.manifest.name.contains(searchText, ignoreCase = true)
|| plugin.manifest.description.contains(searchText, ignoreCase = true)
|| plugin.manifest.authors.any { (name) -> name.contains(searchText, ignoreCase = true) }
}.toUnsafeImmutable()
}
}.stateIn(
scope = screenModelScope + Dispatchers.Default,
started = SharingStarted.WhileSubscribed(replayExpiration = Duration.ZERO),
initialValue = plugins.value,
)
fun setSearchText(search: String) {
searchText.value = search
}
fun showChangelogDialog(plugin: PluginItem) {
showChangelogDialog = plugin
}
fun hideChangelogDialog() {
showChangelogDialog = null
}
fun showUninstallDialog(plugin: PluginItem) {
showUninstallDialog = plugin
}
fun hideUninstallDialog() {
showUninstallDialog = null
}
fun uninstallPlugin(plugin: PluginItem) = screenModelScope.launchIO {
if (!plugins.value.any { it.path == plugin.path }) {
mainThread { hideUninstallDialog() }
return@launchIO
}
val deleteSuccess = try {
File(plugin.path).delete()
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to delete plugin", t)
false
}
if (!deleteSuccess) {
mainThread {
hideUninstallDialog()
context.showToast(R.string.plugins_error)
}
return@launchIO
}
plugins.update { (it - plugin).toUnsafeImmutable() }
mainThread { hideUninstallDialog() }
}
fun setPluginEnabled(pluginName: String, enabled: Boolean) = screenModelScope.launchIO {
aliucordJsonMutex.withLock {
@OptIn(ExperimentalSerializationApi::class)
try {
val settings = paths.coreSettingsFile.inputStream()
.use { Json.decodeFromStream<Map<String, JsonElement>>(it) }
.toMutableMap()
settings["AC_PM_$pluginName"] = JsonPrimitive(enabled)
paths.coreSettingsFile.outputStream()
.use { json.encodeToStream(settings, it) }
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to write Aliucord.json", t)
mainThread { error = true }
}
}
mainThread {
plugins.value.forEach {
if (it.manifest.name == pluginName)
it.enabled = enabled
}
}
}
// Called by screen to load initial data
fun refreshData() = screenModelScope.launchIO {
loadPlugins()
loadPluginsEnabled()
}
private suspend fun loadPluginsEnabled() {
if (!paths.coreSettingsFile.exists()) return
aliucordJsonMutex.withLock {
@OptIn(ExperimentalSerializationApi::class)
val pluginToggles = try {
paths.coreSettingsFile.inputStream()
.use { Json.decodeFromStream<Map<String, JsonElement>>(it) }
.filterKeys { it.startsWith("AC_PM_") }
.mapKeys { it.key.substring("AC_PM_".length) }
.mapValues { it.value.jsonPrimitive.boolean }
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to load Aliucord.json", t)
mainThread { error = true }
emptyMap()
}
mainThread {
plugins.value.forEach {
if (!pluginToggles.getOrDefault(it.manifest.name, true))
it.enabled = false
}
}
}
}
private suspend fun loadPlugins() {
try {
if (!paths.pluginsDir.exists() && !paths.pluginsDir.mkdirs())
throw IllegalStateException("Failed to create plugins directory")
val pluginFiles = paths.pluginsDir.listFiles { file -> file.extension == "zip" }
?: throw IllegalStateException("Failed to read plugins directory")
val pluginItems = pluginFiles.map {
PluginItem(
manifest = loadPluginManifest(it),
path = it.absolutePath,
)
}
plugins.value = pluginItems.toUnsafeImmutable()
} catch (t: Throwable) {
Log.e(BuildConfig.TAG, "Failed to load plugins", t)
mainThread { error = true }
}
}
private fun loadPluginManifest(pluginFile: File): PluginManifest {
return ZipReader(pluginFile).use {
val manifest = it.openEntry("manifest.json")
?: throw Exception("Plugin ${pluginFile.name} has no manifest")
try {
@OptIn(ExperimentalSerializationApi::class)
json.decodeFromStream(manifest.read().inputStream())
} catch (t: Throwable) {
throw Exception("Failed to parse plugin manifest for ${pluginFile.name}", t)
}
}
}
}

View file

@ -1,138 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screens.plugins
import android.os.Parcelable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.compose.LifecycleResumeEffect
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel
import com.aliucord.manager.ui.components.BackButton
import com.aliucord.manager.ui.screens.plugins.components.*
import com.aliucord.manager.ui.screens.plugins.components.dialogs.UninstallPluginDialog
import com.aliucord.manager.ui.screens.plugins.model.PluginItem
import com.aliucord.manager.ui.util.paddings.*
import dev.goon.manager.R
import kotlinx.collections.immutable.ImmutableList
import kotlinx.parcelize.IgnoredOnParcel
import kotlinx.parcelize.Parcelize
@Parcelize
class PluginsScreen : Screen, Parcelable {
@IgnoredOnParcel
override val key = "Plugins"
@Composable
override fun Content() {
val model = koinScreenModel<PluginsModel>()
// Refresh plugins list on activity resume or when this initially opens
LifecycleResumeEffect(Unit) {
model.refreshData()
onPauseOrDispose {}
}
model.showUninstallDialog?.let { plugin ->
UninstallPluginDialog(
pluginName = plugin.manifest.name,
onConfirm = { model.uninstallPlugin(plugin) },
onDismiss = model::hideUninstallDialog
)
}
model.showChangelogDialog?.let { plugin ->
Changelog(
plugin = plugin,
onDismiss = model::hideChangelogDialog
)
}
PluginsScreenContent(
searchText = model.searchText.collectAsState(),
setSearchText = model::setSearchText,
isError = model.error,
plugins = model.filteredPlugins.collectAsState().value,
onPluginUninstall = model::showUninstallDialog,
onPluginChangelog = model::showChangelogDialog,
onPluginToggle = model::setPluginEnabled,
)
}
}
@Composable
fun PluginsScreenContent(
searchText: State<String>,
setSearchText: (String) -> Unit,
isError: Boolean,
plugins: ImmutableList<PluginItem>,
onPluginUninstall: (PluginItem) -> Unit,
onPluginChangelog: (PluginItem) -> Unit,
onPluginToggle: (name: String, enabled: Boolean) -> Unit,
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.plugins_title)) },
navigationIcon = { BackButton() },
)
}
) { paddingValues ->
Column(
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.fillMaxSize()
.padding(paddingValues.exclude(PaddingValuesSides.Bottom))
.padding(horizontal = 20.dp)
) {
PluginSearch(
currentFilter = searchText,
onFilterChange = setSearchText,
modifier = Modifier.fillMaxWidth()
)
LazyColumn(
verticalArrangement = Arrangement.spacedBy(16.dp),
contentPadding = paddingValues
.exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top)
.add(PaddingValues(vertical = 12.dp)),
modifier = Modifier.fillMaxSize(),
) {
when {
isError -> item(key = "ERROR") {
PluginsError(modifier = Modifier.fillParentMaxSize())
}
plugins.isNotEmpty() -> {
items(
items = plugins,
contentType = { "PLUGIN" },
key = { it.path },
) { plugin ->
PluginCard(
plugin = plugin,
onClickDelete = { onPluginUninstall(plugin) },
onClickShowChangelog = { onPluginChangelog(plugin) },
onSetEnabled = { onPluginToggle(plugin.manifest.name, it) },
)
}
}
else -> item(key = "PLUGINS_NONE") {
PluginsNone(modifier = Modifier.fillParentMaxSize())
}
}
}
}
}
}

View file

@ -1,159 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screens.plugins.components
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
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 coil3.compose.AsyncImage
import com.aliucord.manager.ui.screens.plugins.model.PluginItem
import dev.goon.manager.R
private val hyperLinkPattern = Regex("\\[(.+?)]\\((.+?\\))")
@Suppress("RegExpRedundantEscape") // It is very much not redundant and causes a crash lol
private val headerStylePattern = Regex("\\{(improved|added|fixed)( marginTop)?\\}")
@Composable
private fun AnnotatedString.Builder.MarkdownHyperlink(content: String) {
var idx = 0
with(hyperLinkPattern.toPattern().matcher(content)) {
while (find()) {
val start = start()
val end = end()
val title = group(1)!!
val url = group(2)!!
append(content.substring(idx, start))
// @formatter:off
pushLink(LinkAnnotation.Url(
url,
TextLinkStyles(SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline
))
))
append(title)
pop()
// @formatter:on
idx = end
}
}
if (idx < content.length) append(content.substring(idx))
}
@Composable
fun Changelog(
plugin: PluginItem,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
painter = painterResource(R.drawable.ic_history),
contentDescription = stringResource(R.string.plugins_view_changelog, plugin.manifest.name)
)
},
title = { Text(plugin.manifest.name) },
text = {
Column {
plugin.manifest.changelogMedia?.let { mediaUrl ->
AsyncImage(
modifier = Modifier
.fillMaxWidth()
.heightIn(max = 90.dp)
.clip(RoundedCornerShape(14.dp)),
model = mediaUrl,
contentDescription = stringResource(R.string.plugins_changelog_media)
)
}
LazyColumn {
items(plugin.manifest.changelog!!.lines()) {
var line = it.trim()
if (line.isNotEmpty()) {
when (line[0]) {
'#' -> {
do {
line = line.substring(1)
} while (line.startsWith("#"))
Text(
text = line.trimStart(),
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
)
}
'*' -> {
Text(
modifier = Modifier.padding(bottom = 2.dp),
text = buildAnnotatedString {
withStyle(
SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
) {
append("● ")
}
MarkdownHyperlink(line.substring(1))
}
)
}
else -> {
when {
line.endsWith("marginTop}") -> {
val color = MaterialTheme.colorScheme.onSurface
Text(
text = line,
fontWeight = FontWeight.Bold,
color = color,
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
)
}
line.all { c -> c == '=' } -> {} // Discord ignores =======
else -> {
Text(buildAnnotatedString {
MarkdownHyperlink(line)
})
}
}
}
}
}
}
}
}
},
confirmButton = {
Button(onClick = onDismiss) {
Text(stringResource(R.string.action_close))
}
}
)
}

View file

@ -1,153 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screens.plugins.components
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
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 com.aliucord.manager.ui.screens.plugins.model.PluginItem
import dev.goon.manager.R
@Composable
fun PluginCard(
plugin: PluginItem,
onClickDelete: () -> Unit,
onClickShowChangelog: () -> Unit,
onSetEnabled: (Boolean) -> Unit,
) {
val uriHandler = LocalUriHandler.current
ElevatedCard {
// Header
Row(
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.clickable { onSetEnabled(!plugin.enabled) }
.padding(start = 16.dp, end = 16.dp, top = 16.dp, bottom = 14.dp),
) {
Column {
// Name
Text(
text = buildAnnotatedString {
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(plugin.manifest.name)
}
append(" v")
append(plugin.manifest.version)
}
)
// Authors
val authors = buildAnnotatedString {
withStyle(SpanStyle(color = MaterialTheme.colorScheme.primary)) {
for ((idx, author) in plugin.manifest.authors.withIndex()) {
if (idx > 0) append(", ")
if (author.hyperlink) pushLink(
LinkAnnotation.Url(
url = author.discordUrl,
styles = TextLinkStyles(
SpanStyle(
textDecoration = TextDecoration.Underline,
)
),
)
)
append(author.name)
if (author.hyperlink) pop()
}
}
}
Text(
text = authors,
style = MaterialTheme.typography.labelLarge,
)
}
Spacer(Modifier.weight(1f, true))
// Toggle Switch
Switch(
checked = plugin.enabled,
onCheckedChange = { onSetEnabled(!plugin.enabled) }
)
}
HorizontalDivider(
modifier = Modifier
.alpha(0.3f)
.padding(horizontal = 16.dp)
)
Column(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
// Description
Text(
text = plugin.manifest.description,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.heightIn(max = 150.dp, min = 40.dp)
.padding(bottom = 20.dp),
)
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp)
) {
IconButton(
onClick = { uriHandler.openUri(plugin.manifest.repositoryUrl) },
modifier = Modifier.size(25.dp),
) {
Icon(
modifier = Modifier.fillMaxSize(),
painter = painterResource(R.drawable.ic_account_github_white_24dp),
contentDescription = stringResource(R.string.github)
)
}
if (plugin.manifest.changelog != null) {
IconButton(
onClick = onClickShowChangelog,
modifier = Modifier.size(25.dp),
) {
Icon(
painter = painterResource(R.drawable.ic_history),
contentDescription = stringResource(R.string.plugins_view_changelog),
modifier = Modifier.fillMaxSize(),
)
}
}
Spacer(Modifier.weight(1f, true))
IconButton(
onClick = onClickDelete,
modifier = Modifier.size(25.dp),
) {
Icon(
modifier = Modifier.fillMaxSize(),
painter = painterResource(R.drawable.ic_delete_forever),
contentDescription = stringResource(R.string.action_uninstall),
tint = MaterialTheme.colorScheme.error
)
}
}
}
}
}

View file

@ -1,46 +0,0 @@
package com.aliucord.manager.ui.screens.plugins.components
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.text.KeyboardActions
import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import com.aliucord.manager.ui.components.ResetToDefaultButton
import dev.goon.manager.R
@Composable
fun PluginSearch(
currentFilter: State<String>,
onFilterChange: (String) -> Unit,
modifier: Modifier = Modifier.Companion,
) {
val focusManager = LocalFocusManager.current
OutlinedTextField(
value = currentFilter.value,
onValueChange = onFilterChange,
singleLine = true,
shape = MaterialTheme.shapes.medium,
label = { Text(stringResource(R.string.action_search)) },
trailingIcon = {
val isFilterBlank by remember { derivedStateOf { currentFilter.value.isEmpty() } }
ResetToDefaultButton(
enabled = !isFilterBlank,
onClick = { onFilterChange("") },
modifier = Modifier.padding(end = 4.dp),
)
},
keyboardOptions = KeyboardOptions(
autoCorrectEnabled = false,
imeAction = ImeAction.Companion.Search
),
keyboardActions = KeyboardActions { focusManager.clearFocus() },
modifier = modifier,
)
}

View file

@ -1,35 +0,0 @@
package com.aliucord.manager.ui.screens.plugins.components
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import dev.goon.manager.R
@Composable
fun PluginsError(modifier: Modifier = Modifier) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier,
) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
painter = painterResource(R.drawable.ic_warning),
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
)
Text(
text = stringResource(R.string.plugins_error),
color = MaterialTheme.colorScheme.error,
)
}
}
}

View file

@ -1,31 +0,0 @@
package com.aliucord.manager.ui.screens.plugins.components
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import dev.goon.manager.R
@Composable
fun PluginsNone(modifier: Modifier = Modifier) {
Box(
contentAlignment = Alignment.Center,
modifier = modifier,
) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
painter = painterResource(R.drawable.ic_extension_off),
contentDescription = null
)
Text(stringResource(R.string.plugins_none_installed))
}
}
}

View file

@ -1,56 +0,0 @@
package com.aliucord.manager.ui.screens.plugins.components.dialogs
import androidx.compose.foundation.layout.size
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import dev.goon.manager.R
@Composable
fun UninstallPluginDialog(
pluginName: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
painter = painterResource(R.drawable.ic_delete_forever),
contentDescription = null,
modifier = Modifier.size(32.dp),
)
},
title = {
Text(stringResource(R.string.plugins_delete_plugin, pluginName))
},
text = {
Text(
text = stringResource(R.string.plugins_delete_plugin_body, pluginName),
textAlign = TextAlign.Center,
)
},
confirmButton = {
Button(
onClick = onConfirm,
) {
Text(stringResource(R.string.action_confirm))
}
},
dismissButton = {
Button(
onClick = onDismiss,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Text(stringResource(R.string.action_cancel))
}
}
)
}

View file

@ -1,12 +0,0 @@
package com.aliucord.manager.ui.screens.plugins.model
import androidx.compose.runtime.*
@Stable
data class PluginItem(
val manifest: PluginManifest,
val path: String,
) {
// Plugins are enabled by default unless disabled
var enabled by mutableStateOf(true)
}

View file

@ -1,35 +0,0 @@
package com.aliucord.manager.ui.screens.plugins.model
import androidx.compose.runtime.Immutable
import com.aliucord.manager.util.serialization.ImmutableListSerializer
import kotlinx.collections.immutable.ImmutableList
import kotlinx.serialization.Serializable
@Immutable
@Serializable
data class PluginManifest(
val name: String,
@Serializable(with = ImmutableListSerializer::class)
val authors: ImmutableList<Author>,
val description: String,
val version: String,
val updateUrl: String,
val changelog: String?,
val changelogMedia: String?,
) {
val repositoryUrl: String
get() = updateUrl
.replace("raw.githubusercontent.com", "github.com")
.replaceFirst("/builds.*".toRegex(), "")
@Immutable
@Serializable
data class Author(
val name: String,
val id: Long,
val hyperlink: Boolean = true,
) {
val discordUrl: String
get() = "https://discord.com/users/$id"
}
}

View file

@ -98,6 +98,14 @@ class SettingsScreen : Screen, Parcelable {
}
SettingsSwitch(
label = stringResource(R.string.setting_amoled),
secondaryLabel = stringResource(R.string.setting_amoled_desc),
pref = preferences.amoled,
icon = { Icon(painterResource(R.drawable.ic_night), null) },
onPrefChange = { preferences.amoled = it },
)
// Material You theming on Android 12+
if (Build.VERSION.SDK_INT >= 31) {
SettingsSwitch(
@ -159,8 +167,8 @@ class SettingsScreen : Screen, Parcelable {
painter = painterResource(
when (preferences.installer) {
InstallerSetting.PM -> R.drawable.ic_android
InstallerSetting.ROOT -> R.drawable.ic_code
InstallerSetting.Shizuku -> R.drawable.ic_tools
InstallerSetting.ROOT -> R.drawable.ic_hashtag
InstallerSetting.Shizuku -> R.drawable.ic_shizuku
}
),
contentDescription = null,

View file

@ -56,8 +56,8 @@ fun InstallerDialog(
painter = painterResource(
when (installer) {
InstallerSetting.PM -> R.drawable.ic_android
InstallerSetting.ROOT -> R.drawable.ic_code
InstallerSetting.Shizuku -> R.drawable.ic_tools
InstallerSetting.ROOT -> R.drawable.ic_hashtag
InstallerSetting.Shizuku -> R.drawable.ic_shizuku
}
),
contentDescription = null,

View file

@ -20,6 +20,7 @@ import dev.goon.manager.R
fun ManagerTheme(
theme: Theme = Theme.System,
dynamicColor: Boolean = true,
amoled: Boolean = false,
content: @Composable () -> Unit,
) {
val dynamicColor = dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
@ -28,12 +29,63 @@ fun ManagerTheme(
Theme.Dark -> true
Theme.Light -> false
}
val colorScheme = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
val context = LocalContext.current
val baseScheme = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(context)
dynamicColor && !darkTheme -> dynamicLightColorScheme(context)
darkTheme -> darkColorScheme()
else -> lightColorScheme()
}
val colorScheme = if (amoled && darkTheme) {
baseScheme.copy(
// Core surfaces: true black with white content
background = Color(0xFF000000),
onBackground = Color(0xFFFFFFFF),
surface = Color(0xFF000000),
onSurface = Color(0xFFFFFFFF),
// Surface variants / tints for containers and subtle surfaces
surfaceVariant = Color(0xFF000000),
onSurfaceVariant = Color(0xFFFFFFFF),
surfaceTint = Color(0xFF000000),
// Primary (controls). Set both primary and primaryContainer to white so
// Filled / FilledTonal controls and other components using either token
// will have a white container with black text/icons.
primary = Color(0xFFFFFFFF),
onPrimary = Color(0xFF000000),
primaryContainer = Color(0xFFFFFFFF),
onPrimaryContainer = Color(0xFF000000),
// Secondary / tertiary: match primary behavior for consistency across different control types
secondary = Color(0xFFFFFFFF),
onSecondary = Color(0xFF000000),
secondaryContainer = Color(0xFFFFFFFF),
onSecondaryContainer = Color(0xFF000000),
tertiary = Color(0xFFFFFFFF),
onTertiary = Color(0xFF000000),
tertiaryContainer = Color(0xFFFFFFFF),
onTertiaryContainer = Color(0xFF000000),
// Inverse surfaces for components that might use them
inverseSurface = Color(0xFF121212),
inverseOnSurface = Color(0xFFFFFFFF),
// Outline and subtle elements: semi-transparent white for edges/highlights
outline = Color(0x66FFFFFF),
// Preserve error semantics from base (keeps readable error colors)
error = baseScheme.error,
onError = baseScheme.onError,
errorContainer = baseScheme.errorContainer,
onErrorContainer = baseScheme.onErrorContainer,
)
} else baseScheme
val customColors = when (darkTheme) {
true -> DarkCustomColors
false -> LightCustomColors

View file

@ -19,6 +19,10 @@ data class CustomColors(
val onWarning: Color,
val warningContainer: Color,
val onWarningContainer: Color,
val instanceCardBackground: Color,
val instanceCardContent: Color,
val instanceCardButtonContainer: Color,
val instanceCardButtonContent: Color,
)
private val YellowAlt1 = Color(0xFFE9C414)
@ -32,6 +36,10 @@ val DarkCustomColors = CustomColors(
onWarning = DarkBrown,
warningContainer = DarkBronze,
onWarningContainer = Shandy,
instanceCardBackground = Color(0xFF0E0E0E),
instanceCardContent = Color(0xFFFFFFFF),
instanceCardButtonContainer = Color(0xFFF2F2F2),
instanceCardButtonContent = Color(0xFF000000),
)
val LightCustomColors = CustomColors(
@ -39,4 +47,8 @@ val LightCustomColors = CustomColors(
onWarning = Color.White,
warningContainer = Shandy,
onWarningContainer = DarkerBrown,
instanceCardBackground = Color(0xFFFFFFFF),
instanceCardContent = Color(0xFF000000),
instanceCardButtonContainer = Color(0xFFFAFAFA),
instanceCardButtonContent = Color(0xFF000000),
)

View file

@ -1,17 +1,15 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:width="108dp"
android:height="108dp"
android:viewportWidth="256"
android:viewportHeight="256"
>
<!-- Solid circular background (Discord blurple) -->
<path android:fillColor="#5865F2" android:pathData="M256,128 A128,128 0 1,1 0,128 A128,128 0 1,1 256,128 Z" />
<!-- White Discord mark (borrowed path scaled by viewport) -->
<!-- Solid circular background (Shiggy yellow) -->
<path android:fillColor="#fcc680" android:pathData="M256,128 A128,128 0 1,1 0,128 A128,128 0 1,1 256,128 Z" />
<!-- White droplet -->
<path
android:fillColor="#FFFFFFFF"
android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z"
android:fillColor="#FFFFFF"
android:pathData="M128,38.4 C64,89.6 51.2,166.4 76.8,204.8 Q102.4,230.4 128,217.6 Q153.6,230.4 179.2,204.8 C204.8,166.4 192,89.6 128,38.4 Z"
/>
</vector>

View file

@ -0,0 +1,12 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="960"
android:viewportHeight="960"
>
<path
android:fillColor="#aaaab1"
android:pathData="m360,640 l-33,131q-3,13 -13,21t-24,8q-19,0 -31,-15t-7,-33l28,-112L171,640q-20,0 -32,-15.5t-7,-34.5q3,-14 14,-22t25,-8h129l40,-160L231,400q-20,0 -32,-15.5t-7,-34.5q3,-14 14,-22t25,-8h129l33,-131q3,-13 13,-21t24,-8q19,0 31,15t7,33l-28,112h160l33,-131q3,-13 13,-21t24,-8q19,0 31,15t7,33l-28,112h109q20,0 32,15.5t7,34.5q-3,14 -14,22t-25,8L660,400l-40,160h109q20,0 32,15.5t7,34.5q-3,14 -14,22t-25,8L600,640l-33,131q-3,13 -13,21t-24,8q-19,0 -31,-15t-7,-33l28,-112L360,640ZM380,560h160l40,-160L420,400l-40,160Z"
/>
</vector>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -6,5 +6,5 @@
android:viewportHeight="108"
>
<!-- Flat background color for launcher adaptive icon -->
<path android:pathData="M0,0H108V108H0Z" android:fillColor="#FFFFCB82" />
<path android:pathData="M0,0H108V108H0Z" android:fillColor="#070700" />
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -5,15 +5,8 @@
android:viewportWidth="256"
android:viewportHeight="256"
>
<!-- Solid circular background (Shiggy yellow) -->
<path android:fillColor="#fcc680" android:pathData="M256,128 A128,128 0 1,1 0,128 A128,128 0 1,1 256,128 Z" />
<!-- White Discord mark (scaled down and centered) -->
<group android:scaleX="0.7" android:scaleY="0.7" android:translateX="38.4" android:translateY="38.4">
<path
android:fillColor="#FFFFFFFF"
android:pathData="M216.86,45.1C200.29,37.34 182.57,31.71 164.04,28.5C161.77,32.61 159.11,38.15 157.28,42.55C137.58,39.58 118.07,39.58 98.74,42.55C96.91,38.15 94.19,32.61 91.9,28.5C73.35,31.71 55.61,37.36 39.04,45.14C5.62,95.65 -3.44,144.9 1.09,193.46C23.26,210.01 44.74,220.07 65.86,226.65C71.08,219.47 75.73,211.84 79.74,203.8C72.1,200.9 64.79,197.32 57.89,193.17C59.72,191.81 61.51,190.39 63.24,188.93C105.37,208.63 151.13,208.63 192.75,188.93C194.51,190.39 196.3,191.81 198.11,193.17C191.18,197.34 183.85,200.92 176.22,203.82C180.23,211.84 184.86,219.49 190.1,226.67C211.24,220.09 232.74,210.03 254.91,193.46C260.23,137.17 245.83,88.37 216.86,45.1ZM85.47,163.59C72.83,163.59 62.46,151.79 62.46,137.41C62.46,123.04 72.61,111.21 85.47,111.21C98.34,111.21 108.71,123.02 108.49,137.41C108.51,151.79 98.34,163.59 85.47,163.59ZM170.53,163.59C157.88,163.59 147.51,151.79 147.51,137.41C147.51,123.04 157.66,111.21 170.53,111.21C183.39,111.21 193.76,123.02 193.54,137.41C193.54,151.79 183.39,163.59 170.53,163.59Z"
android:fillColor="#000000"
android:pathData="M128,38.4 C64,89.6 51.2,166.4 76.8,204.8 Q102.4,230.4 128,217.6 Q153.6,230.4 179.2,204.8 C204.8,166.4 192,89.6 128,38.4 Z"
/>
</group>
</vector>

View file

@ -0,0 +1,17 @@
<vector
xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108"
>
<path
android:fillAlpha="0.45"
android:fillColor="#FF000000"
android:pathData="M85.8,15.3H59.3L46,38.2l13.2,22.9h2.7c-0.9,-0.6 -1,-1.8 -0.4,-2.7c0.6,-0.9 1.8,-1 2.7,-0.4c0.9,0.6 1,1.8 0.4,2.7c-0.1,0.1 -0.3,0.3 -0.4,0.4h21.5L99,38.2L85.8,15.3z"
/>
<path
android:fillColor="#FF000000"
android:pathData="M85.5,42.7c-1.7,-0.5 -3.4,-1 -5.2,-1.3c-3.9,-0.5 -7.9,-0.3 -11.7,0.6c-3.4,-3 -7.4,-5.2 -11.7,-6.5c-2.1,-0.6 -4.3,-1 -6.5,-1.3c-2.7,-5.6 -7,-10.4 -12.3,-13.6c-4.1,3.6 -7.1,8.3 -8.4,13.6c-5.6,2.1 -10.5,5.7 -14.3,10.4c-2.3,2.9 -6.4,8 -6.5,14.3c-0.3,13 16.6,22.3 23.3,25.9C40,89 48.3,91.6 57,92.7c4,0.4 8.2,-0.5 11.7,-2.6c5.7,-3.4 7.6,-8.9 9.1,-13c1.9,-5.8 2.5,-12.1 1.3,-18.2c2.2,-3.2 4,-6.6 5.2,-10.4C84.7,46.6 85.1,44.7 85.5,42.7zM39.1,50.3c-1,0 -1.9,-0.9 -1.9,-1.9s0.9,-1.9 1.9,-1.9s1.9,0.9 1.9,1.9S40.3,50.3 39.1,50.3L39.1,50.3zM63.1,61.5c-1,0 -1.9,-0.9 -1.9,-1.9c0,-1 0.9,-1.9 1.9,-1.9c1,0 1.9,0.9 1.9,1.9C65,60.6 64.2,61.5 63.1,61.5z"
/>
</vector>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -1,7 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<!-- Adaptive icon updated to use Shiggy flat background and provided foreground image -->
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@drawable/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -1,6 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@color/ic_launcher_background" />
<foreground android:drawable="@drawable/ic_launcher_foreground" />
<monochrome android:drawable="@drawable/ic_launcher_monochrome" />
</adaptive-icon>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.2 KiB

After

Width:  |  Height:  |  Size: 7.1 KiB

View file

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

View file

@ -51,8 +51,12 @@
<string name="settings_header_info">Info</string>
<string name="setting_theme">Theme</string>
<string name="setting_theme_desc">The theme to apply to this app</string>
<string name="setting_amoled">Amoled mode</string>
<string name="setting_amoled_desc">Force true-black backgrounds for AMOLED displays</string>
<string name="setting_dynamic_color">Dynamic color</string>
<string name="setting_dynamic_color_desc">Enables Material You theming on Android 12+</string>
<string
name="setting_dynamic_color_desc"
>Enables Material You theming on Android 12+; dynamic colors will also be applied to the Amoled theme when enabled</string>
<string name="setting_installer">Installer</string>
<string name="setting_installer_desc">The method to use for installing Goon</string>
<string name="setting_developer_options">Developer options</string>
@ -72,14 +76,7 @@
<string name="theme_system">System</string>
<string name="theme_dark">Dark</string>
<string name="theme_light">Light</string>
<string name="plugins_title">Plugins</string>
<string name="plugins_view_changelog">View changelog</string>
<string name="plugins_delete_plugin">Uninstall %s</string>
<string name="plugins_delete_plugin_body">Are you sure you want to uninstall the %s plugin?</string>
<string name="plugins_none_installed">No plugins installed!</string>
<string name="plugins_changelog_media">Changelog Media</string>
<string name="plugins_error">An error occurred while processing plugins!</string>
<string name="theme_amoled">Amoled</string>
<string name="navigation_back">Back</string>
<string name="navigation_about">About</string>
@ -87,14 +84,13 @@
<string name="navigation_logs">Open logs</string>
<string name="navigation_settings">Settings</string>
<string name="contributors_lead">Lead Developers</string>
<string name="contributors_contributions">%d contributions</string>
<string name="contributors">Contributors</string>
<string name="lead_role_kettu">Kettu - Creator</string>
<string name="lead_role_shiggycord">GoonCord - Creator</string>
<string name="lead_role_bunny">Bunny - Creator</string>
<string name="role_creator_vendetta">Creator of Vendetta</string>
<string name="role_creator_vendetta">Vendetta - Creator</string>
<string name="role_original_manager_creator">Original Manager Creator</string>
<string name="contributors">Other Developers</string>
<string name="contributors_contributions">%s contributions</string>
<string name="network_load_fail">Failed to load</string>
<string name="network_warning_title">Metered network</string>
@ -289,10 +285,10 @@
<string name="permissions_shizuku_denied">Failed to obtain Shizuku permissions</string>
<string name="fun_fact_prefix">Fun Fact: %s</string>
<string name="fun_fact_1">ShiggyCord on first release was meant to be custom Kettu build!</string>
<string name="fun_fact_2">The refreshed UI inside the settings was made from developer boredom</string>
<string name="fun_fact_3">ShiggyCord as first vendetta like client used to show users the built in plugins and even add extra ones!</string>
<string name="fun_fact_4">You love boys, don\'t you?</string>
<string name="fun_fact_1">GoonCord on second release was meant to be custom ShiggyCord build!</string>
<string name="fun_fact_2">This client is not same as CumCord i made it as an Joke</string>
<string name="fun_fact_3">GoonCord as second vendetta like client used to show users the built in plugins and even add extra ones!</string>
<string name="fun_fact_4">You love Femboys, don\'t you?</string>
<string name="fun_fact_5">Aliucord is the best client with old discord UI</string>
<string name="fun_fact_6">We love you cocobo and pylix!</string>
<string name="fun_fact_7">The Vendetta react native part was a total dogshit</string>
@ -300,7 +296,6 @@
<string name="fun_fact_9">This client can be run on a android TV!</string>
<string name="fun_fact_10">The Main Developer one and half a year ago didn\'t even know how to code</string>
<string name="fun_fact_11">This client does not support iOS!</string>
<string name="fun_fact_12">Boom!</string>
<string name="fun_fact_13">This client is not same as CumCord i made it as an Joke</string>
<string name="fun_fact_12">Boom :3!</string>
</resources>

View file

@ -2,7 +2,7 @@
<resources>
<style name="Theme.AliucordManager.SplashScreen" parent="Theme.SplashScreen">
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher_foreground</item>
<item name="windowSplashScreenAnimatedIcon">@drawable/ic_launcher</item>
<item name="postSplashScreenTheme">@style/Theme.AppCompat.NoActionBar</item>
<item name="android:windowActionBar">false</item>