Compare commits

..

No commits in common. "main" and "v1.1.2" have entirely different histories.
main ... v1.1.2

70 changed files with 1473 additions and 649 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> <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.3) and install it. Get newest apk from [release list](https://github.com/OTKUSteyler/GoonManager/releases/tag/v1.1.1) and install it.
<p><strong>Build from source</strong> — clone and assemble:</p> <p><strong>Build from source</strong> — clone and assemble:</p>
@ -127,5 +127,3 @@ Goon Manager is open source under the OSL License. See `LICENSE` for details.
<p align="center"> <p align="center">
<strong>Made with ❤️ for the GoonCord community — keep it playful, keep it safe.</strong> <strong>Made with ❤️ for the GoonCord community — keep it playful, keep it safe.</strong>
</p> </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() val gitHasHasLocalChanges = providers.execIgnoreCode("git", "status", "-s").isNotEmpty()
android { android {
namespace = "dev.shiggy.manager" namespace = "dev.goon.manager"
compileSdk = 37 compileSdk = 36
defaultConfig { defaultConfig {
minSdk = 28 minSdk = 28
targetSdk = 37 targetSdk = 36
versionCode = 10_12_01 versionCode = 10_11_00
versionName = "1.1.2" versionName = "1.1.0"
vectorDrawables { vectorDrawables {
useSupportLibrary = true useSupportLibrary = true
} }
buildConfigField("String", "APPLICATION_NAME", "\"Shiggy Manager\"") buildConfigField("String", "APPLICATION_NAME", "\"Goon Manager\"")
buildConfigField("String", "TAG", "\"ShiggyManager\"") buildConfigField("String", "TAG", "\"GoonManager\"")
buildConfigField("String", "GITHUB_ORG", "\"ShiggyCord\"") buildConfigField("String", "GITHUB_ORG", "\"ShiggyCord\"")
buildConfigField("String", "SUPPORT_SERVER", "\"\"") buildConfigField("String", "SUPPORT_SERVER", "\"\"")

View file

@ -1,30 +0,0 @@
# 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 <application
android:name="com.aliucord.manager.ManagerApplication" android:name="com.aliucord.manager.ManagerApplication"
android:icon="@drawable/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:largeHeap="true" android:largeHeap="true"
android:requestLegacyExternalStorage="true" android:requestLegacyExternalStorage="true"
android:roundIcon="@drawable/ic_launcher_round" android:roundIcon="@mipmap/ic_launcher_round"
android:supportsRtl="true" android:supportsRtl="true"
android:theme="@style/Theme.AppCompat.NoActionBar" android:theme="@style/Theme.AppCompat.NoActionBar"
> >

View file

@ -42,7 +42,6 @@ class MainActivity : ComponentActivity() {
ManagerTheme( ManagerTheme(
theme = preferences.theme, theme = preferences.theme,
dynamicColor = preferences.dynamicColor, dynamicColor = preferences.dynamicColor,
amoled = preferences.amoled,
) { ) {
if (BuildConfig.RELEASE) { if (BuildConfig.RELEASE) {
UpdaterDialog() 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.logs.LogsListScreenModel
import com.aliucord.manager.ui.screens.patching.PatchingScreenModel import com.aliucord.manager.ui.screens.patching.PatchingScreenModel
import com.aliucord.manager.ui.screens.patchopts.PatchOptionsModel 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.screens.settings.SettingsModel
import com.aliucord.manager.ui.widgets.updater.UpdaterViewModel import com.aliucord.manager.ui.widgets.updater.UpdaterViewModel
import com.aliucord.manager.installers.root.RootInstaller import com.aliucord.manager.installers.root.RootInstaller
@ -57,7 +57,7 @@ class ManagerApplication : Application() {
// UI Models // UI Models
modules(module { 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>()) } 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(::AboutModel)
factoryOf(::PatchingScreenModel) factoryOf(::PatchingScreenModel)
factory { SettingsModel(get<Application>(), get<PathManager>(), get<PreferencesManager>(), get<InstallerManager>()) } factory { SettingsModel(get<Application>(), get<PathManager>(), get<PreferencesManager>(), get<InstallerManager>()) }

View file

@ -11,12 +11,28 @@ import java.io.File
class PathManager( class PathManager(
private val context: Context, 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. * The Shiggy folder where shared Shiggy files are stored.
* Standard path: `~/Shiggy` * Standard path: `~/Shiggy`
*/ */
val shiggyDir = Environment.getExternalStorageDirectory().resolve("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. * Global keystore used for signing APKs.
*/ */

View file

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

View file

@ -16,13 +16,11 @@ import io.ktor.client.request.prepareGet
import io.ktor.client.statement.bodyAsChannel import io.ktor.client.statement.bodyAsChannel
import io.ktor.http.HttpHeaders import io.ktor.http.HttpHeaders
import io.ktor.http.contentLength import io.ktor.http.contentLength
import io.ktor.http.isSuccess
import io.ktor.utils.io.readAvailable import io.ktor.utils.io.readAvailable
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.net.SocketTimeoutException import java.net.SocketTimeoutException
import java.net.URI
/** /**
* Handle downloading remote urls to a path with Ktor. * Handle downloading remote urls to a path with Ktor.
@ -41,108 +39,55 @@ class KtorDownloadManager(
.apply { exists() || createNewFile() } .apply { exists() || createNewFile() }
try { try {
val downloadUrl = rewriteRawGithubUrl(url) val httpStmt = http.prepareGet(url) {
header(HttpHeaders.CacheControl, "no-cache, no-store")
// Disable compression due to bug on emulators
// This header cannot be set with Android's DownloadManager
if (IS_PROBABLY_EMULATOR) {
header(HttpHeaders.AcceptEncoding, null)
}
}
httpStmt.execute { resp ->
val channel = resp.bodyAsChannel()
val total = resp.contentLength() ?: 0
var retrieved = 0L
val buf = ByteArray(1024 * 1024 * 1)
var bufLen: Int
tmpOut.outputStream().use { stream ->
// Preallocate space for this file
if (total > 0 && Build.VERSION.SDK_INT >= 26) {
val storageManager = application.getSystemService<StorageManager>()!!
// 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 { try {
val u = URI(url) storageManager.allocateBytes(stream.fd, total)
val segs = u.path.trimStart('/').split('/', limit = 6) } catch (e: IOException) {
if (segs.size >= 5 && segs[2].equals("raw", ignoreCase = true)) { throw InsufficientStorageException(e.message)
"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
// This header cannot be set with Android's DownloadManager
if (IS_PROBABLY_EMULATOR) {
header(HttpHeaders.AcceptEncoding, null)
}
}
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
val buf = ByteArray(1024 * 1024 * 1)
var bufLen: Int
tmpOut.outputStream().use { stream ->
// Preallocate space for this file
if (total > 0 && Build.VERSION.SDK_INT >= 26) {
val storageManager = application.getSystemService<StorageManager>()!!
try {
storageManager.allocateBytes(stream.fd, total)
} catch (e: IOException) {
throw InsufficientStorageException(e.message)
}
}
while (!channel.isClosedForRead) {
bufLen = channel.readAvailable(buf)
if (bufLen <= 0) break
stream.write(buf, 0, bufLen)
stream.flush()
retrieved += bufLen
if (total > 0) {
if (retrieved > total)
throw IOException("Total bytes received exceeds header total!")
onProgressUpdate?.onUpdate(retrieved / total.toFloat())
} else {
onProgressUpdate?.onUpdate(null)
}
}
} }
} }
// If we reach here, the attempt succeeded while (!channel.isClosedForRead) {
downloaded = true bufLen = channel.readAvailable(buf)
break if (bufLen <= 0) 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) { stream.write(buf, 0, bufLen)
// No attempt succeeded, throw the last failure to be handled by outer catch blocks stream.flush()
throw lastException ?: IOException("Failed to download resource from $url")
retrieved += bufLen
if (total > 0) {
if (retrieved > total)
throw IOException("Total bytes received exceeds header total!")
onProgressUpdate?.onUpdate(retrieved / total.toFloat())
} else {
onProgressUpdate?.onUpdate(null)
}
}
}
} }
} catch (_: CancellationException) { } catch (_: CancellationException) {
tmpOut.delete() tmpOut.delete()
@ -162,17 +107,6 @@ class KtorDownloadManager(
return Result.Success(out) 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 * Wrapper around an exception that occurred from invoking Ktor
*/ */

View file

@ -0,0 +1,23 @@
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

@ -1,18 +0,0 @@
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,9 +6,6 @@ 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. * 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 @Serializable
data class InstallMetadata( data class InstallMetadata(
@ -50,5 +47,5 @@ data class InstallMetadata(
/** /**
* Version of ShiggyXposed embedded into the APK. * Version of ShiggyXposed embedded into the APK.
*/ */
val goonXposedVersion: SemVer, val shiggyXposedVersion: SemVer?,
) )

View file

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

View file

@ -13,11 +13,10 @@ import org.lsposed.lspatch.share.LSPConfig
import org.lsposed.patch.LSPatch import org.lsposed.patch.LSPatch
import org.lsposed.patch.util.Logger import org.lsposed.patch.util.Logger
import java.io.File import java.io.File
import java.util.*
class InjectGoonXposedStep : Step() { class InjectShiggyXposedStep : Step() {
override val group: StepGroup = StepGroup.Install override val group: StepGroup = StepGroup.Install
override val localizedName: Int = R.string.patch_step_inject_goon override val localizedName: Int = R.string.patch_step_inject_shiggy
suspend fun patch( suspend fun patch(
container: StepRunner, container: StepRunner,
@ -83,15 +82,7 @@ class InjectGoonXposedStep : Step() {
// FilenameUtils.getBaseName(apkFileName), // FilenameUtils.getBaseName(apkFileName),
// LSPConfig.instance.VERSION_CODE) // LSPConfig.instance.VERSION_CODE)
// ) // )
// Note: LSPatch uses Locale.getDefault() which can produce locale-specific numerals val patchedApkName = "${baseName}-${LSPConfig.instance.VERSION_CODE}-lspatched.apk"
// (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) val patchedApk = File(tempDir, patchedApkName)
if (patchedApk.exists()) { if (patchedApk.exists()) {

View file

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

View file

@ -252,8 +252,6 @@ object ManifestPatcher {
extractNativeLibs ?: false extractNativeLibs ?: false
private var shouldAddMetadata = private var shouldAddMetadata =
addManagerMetadata ?: false addManagerMetadata ?: false
private var shouldAddCleartext = true
override fun attr( override fun attr(
ns: String?, ns: String?,
@ -271,12 +269,6 @@ object ManifestPatcher {
shouldAddExtractNativeLibs = false shouldAddExtractNativeLibs = false
if (name == DEBUGGABLE) if (name == DEBUGGABLE)
shouldAddDebuggable = false shouldAddDebuggable = false
if (name == "usesCleartextTraffic") {
shouldAddCleartext = false
super.attr(ns, name, 0x010104ec, TYPE_INT_BOOLEAN, 1)
return
}
super.attr( super.attr(
ns, ns,
name, name,
@ -433,15 +425,7 @@ object ManifestPatcher {
0 0
) )
} }
if (shouldAddCleartext) {
super.attr(
ANDROID_NAMESPACE,
"usesCleartextTraffic",
0x010104ec,
TYPE_INT_BOOLEAN,
1
)
}
super.end() 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.text.font.FontStyle
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil3.compose.SubcomposeAsyncImage import coil3.compose.SubcomposeAsyncImage
import com.aliucord.manager.network.models.Developer import com.aliucord.manager.network.models.Contributor
import com.valentinilk.shimmer.shimmer import com.valentinilk.shimmer.shimmer
import dev.goon.manager.R import dev.goon.manager.R
@Composable @Composable
fun ContributorCommitsItem( fun ContributorCommitsItem(
user: Developer, user: Contributor,
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current

View file

@ -0,0 +1,20 @@
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

@ -0,0 +1,52 @@
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,6 +34,7 @@ private fun HomeScreenLoadedPreview(
onUpdate = {}, onUpdate = {},
onOpenApp = {}, onOpenApp = {},
onOpenAppInfo = {}, onOpenAppInfo = {},
onOpenPlugins = {},
onUninstall = {}, onUninstall = {},
) )
} }

View file

@ -0,0 +1,29 @@
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

@ -0,0 +1,92 @@
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

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

View file

@ -10,7 +10,6 @@ import androidx.compose.foundation.background
import androidx.compose.foundation.border import androidx.compose.foundation.border
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.itemsIndexed
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
@ -21,7 +20,7 @@ import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.koinScreenModel import cafe.adriel.voyager.koin.koinScreenModel
import com.aliucord.manager.ui.components.* import com.aliucord.manager.ui.components.*
import com.aliucord.manager.ui.screens.about.components.RegularContributorListItem import com.aliucord.manager.ui.screens.about.components.LeadContributor
import com.aliucord.manager.ui.util.paddings.* import com.aliucord.manager.ui.util.paddings.*
import dev.goon.manager.R import dev.goon.manager.R
import kotlinx.parcelize.IgnoredOnParcel import kotlinx.parcelize.IgnoredOnParcel
@ -29,8 +28,7 @@ import kotlinx.parcelize.Parcelize
@Parcelize @Parcelize
class AboutScreen : Screen, Parcelable { class AboutScreen : Screen, Parcelable {
@IgnoredOnParcel @IgnoredOnParcel override val key = "About"
override val key = "About"
@Composable @Composable
override fun Content() { override fun Content() {
@ -43,93 +41,108 @@ class AboutScreen : Screen, Parcelable {
@Composable @Composable
fun AboutScreenContent(state: State<AboutScreenState>) { fun AboutScreenContent(state: State<AboutScreenState>) {
Scaffold( Scaffold(
topBar = { topBar = {
TopAppBar( TopAppBar(
title = { Text(stringResource(R.string.navigation_about_manager)) }, title = { Text(stringResource(R.string.navigation_about_manager)) },
navigationIcon = { BackButton() }, navigationIcon = { BackButton() },
) )
} }
) { paddingValues -> ) { paddingValues ->
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(2.dp), verticalArrangement = Arrangement.spacedBy(2.dp),
horizontalAlignment = Alignment.CenterHorizontally, horizontalAlignment = Alignment.CenterHorizontally,
contentPadding = paddingValues contentPadding =
.exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top) paddingValues
.add(PaddingValues(vertical = 16.dp)), .exclude(PaddingValuesSides.Horizontal + PaddingValuesSides.Top)
modifier = Modifier .add(PaddingValues(vertical = 16.dp)),
.padding(paddingValues.exclude(PaddingValuesSides.Bottom)) modifier =
Modifier.padding(paddingValues.exclude(PaddingValuesSides.Bottom))
.padding(horizontal = 14.dp),
) { ) {
item(key = "MANAGER_CREDIT_DISCLAIMER_BANNER") { item(key = "MANAGER_CREDIT_DISCLAIMER_BANNER") {
Box( Box(
contentAlignment = Alignment.Center, contentAlignment = Alignment.Center,
modifier = Modifier modifier =
.padding(horizontal = 16.dp, vertical = 8.dp) Modifier.padding(bottom = 24.dp)
.fillMaxWidth() .border(
.border( width = 2.dp,
width = 1.dp, color = MaterialTheme.colorScheme.surfaceVariant,
color = MaterialTheme.colorScheme.outline.copy(alpha = 0.3f), shape = MaterialTheme.shapes.medium
shape = MaterialTheme.shapes.large )
) .clip(MaterialTheme.shapes.medium)
.clip(MaterialTheme.shapes.large) .background(MaterialTheme.colorScheme.surfaceContainerHigh)
.background(MaterialTheme.colorScheme.surfaceContainerLow)
) { ) {
Text( Row(
text = stringResource(R.string.about_manager_credit_disclaimer_notice), horizontalArrangement = Arrangement.spacedBy(12.dp),
style = MaterialTheme.typography.bodyMedium, verticalAlignment = Alignment.CenterVertically,
modifier = Modifier.padding(horizontal = 20.dp, vertical = 16.dp) modifier = Modifier.padding(horizontal = 20.dp, vertical = 14.dp)
) ) {
} Text(
} text =
stringResource(
item(key = "PROJECT_HEADER") { R.string.about_manager_credit_disclaimer_notice
Box(modifier = Modifier.padding(horizontal = 16.dp)) { ),
ProjectHeader() style = MaterialTheme.typography.labelMedium,
}
}
item(key = "TEAM_SPACER") {
Spacer(modifier = Modifier.height(32.dp))
}
when (val currentState = state.value) {
AboutScreenState.Loading -> {
item(key = "LOADING") {
Box(
contentAlignment = Alignment.Center,
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
) )
} }
} }
} }
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 = "CONTRIBUTORS_DIVIDER") {
TextDivider(
text = stringResource(R.string.contributors),
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
)
}
when (val state = state.value) {
AboutScreenState.Loading ->
item(key = "CONTRIBUTIONS_LOADING") {
Box(
contentAlignment = Alignment.Center,
content = { CircularProgressIndicator() },
modifier = Modifier.fillMaxWidth().padding(vertical = 38.dp),
)
}
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 package com.aliucord.manager.ui.screens.about
import com.aliucord.manager.network.models.Developer import com.aliucord.manager.network.models.Contributor
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
sealed interface AboutScreenState { sealed interface AboutScreenState {
data object Loading : AboutScreenState data object Loading : AboutScreenState
data object Failure : AboutScreenState data object Failure : AboutScreenState
data class Loaded(val contributors: ImmutableList<Developer>) : AboutScreenState data class Loaded(val contributors: ImmutableList<Contributor>) : AboutScreenState
} }

View file

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

View file

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

View file

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

View file

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

View file

@ -0,0 +1,203 @@
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

@ -0,0 +1,138 @@
/*
* 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

@ -0,0 +1,159 @@
/*
* 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

@ -0,0 +1,153 @@
/*
* 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

@ -0,0 +1,46 @@
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

@ -0,0 +1,35 @@
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

@ -0,0 +1,31 @@
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

@ -0,0 +1,56 @@
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

@ -0,0 +1,12 @@
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

@ -0,0 +1,35 @@
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,14 +98,6 @@ 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+ // Material You theming on Android 12+
if (Build.VERSION.SDK_INT >= 31) { if (Build.VERSION.SDK_INT >= 31) {
SettingsSwitch( SettingsSwitch(
@ -167,8 +159,8 @@ class SettingsScreen : Screen, Parcelable {
painter = painterResource( painter = painterResource(
when (preferences.installer) { when (preferences.installer) {
InstallerSetting.PM -> R.drawable.ic_android InstallerSetting.PM -> R.drawable.ic_android
InstallerSetting.ROOT -> R.drawable.ic_hashtag InstallerSetting.ROOT -> R.drawable.ic_code
InstallerSetting.Shizuku -> R.drawable.ic_shizuku InstallerSetting.Shizuku -> R.drawable.ic_tools
} }
), ),
contentDescription = null, contentDescription = null,

View file

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

View file

@ -20,7 +20,6 @@ import dev.goon.manager.R
fun ManagerTheme( fun ManagerTheme(
theme: Theme = Theme.System, theme: Theme = Theme.System,
dynamicColor: Boolean = true, dynamicColor: Boolean = true,
amoled: Boolean = false,
content: @Composable () -> Unit, content: @Composable () -> Unit,
) { ) {
val dynamicColor = dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val dynamicColor = dynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
@ -29,63 +28,12 @@ fun ManagerTheme(
Theme.Dark -> true Theme.Dark -> true
Theme.Light -> false Theme.Light -> false
} }
val colorScheme = when {
val context = LocalContext.current dynamicColor && darkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !darkTheme -> dynamicLightColorScheme(LocalContext.current)
val baseScheme = when {
dynamicColor && darkTheme -> dynamicDarkColorScheme(context)
dynamicColor && !darkTheme -> dynamicLightColorScheme(context)
darkTheme -> darkColorScheme() darkTheme -> darkColorScheme()
else -> lightColorScheme() 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) { val customColors = when (darkTheme) {
true -> DarkCustomColors true -> DarkCustomColors
false -> LightCustomColors false -> LightCustomColors

View file

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

View file

@ -1,15 +1,17 @@
<vector <vector
xmlns:android="http://schemas.android.com/apk/res/android" xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp" android:width="24dp"
android:height="108dp" android:height="24dp"
android:viewportWidth="256" android:viewportWidth="256"
android:viewportHeight="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" /> <!-- Solid circular background (Discord blurple) -->
<!-- White droplet --> <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) -->
<path <path
android:fillColor="#FFFFFF" android:fillColor="#FFFFFFFF"
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" 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"
/> />
</vector> </vector>

View file

@ -1,12 +0,0 @@
<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: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View file

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -5,8 +5,15 @@
android:viewportWidth="256" android:viewportWidth="256"
android:viewportHeight="256" android:viewportHeight="256"
> >
<path
android:fillColor="#000000" <!-- Solid circular background (Shiggy yellow) -->
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" <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"
/>
</group>
</vector> </vector>

View file

@ -1,17 +0,0 @@
<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.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -0,0 +1,7 @@
<?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.

Before

Width:  |  Height:  |  Size: 7.1 KiB

View file

@ -0,0 +1,6 @@
<?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: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.1 KiB

After

Width:  |  Height:  |  Size: 8.2 KiB

View file

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

View file

@ -51,12 +51,8 @@
<string name="settings_header_info">Info</string> <string name="settings_header_info">Info</string>
<string name="setting_theme">Theme</string> <string name="setting_theme">Theme</string>
<string name="setting_theme_desc">The theme to apply to this app</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">Dynamic color</string>
<string <string name="setting_dynamic_color_desc">Enables Material You theming on Android 12+</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">Installer</string>
<string name="setting_installer_desc">The method to use for installing Goon</string> <string name="setting_installer_desc">The method to use for installing Goon</string>
<string name="setting_developer_options">Developer options</string> <string name="setting_developer_options">Developer options</string>
@ -76,7 +72,14 @@
<string name="theme_system">System</string> <string name="theme_system">System</string>
<string name="theme_dark">Dark</string> <string name="theme_dark">Dark</string>
<string name="theme_light">Light</string> <string name="theme_light">Light</string>
<string name="theme_amoled">Amoled</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="navigation_back">Back</string> <string name="navigation_back">Back</string>
<string name="navigation_about">About</string> <string name="navigation_about">About</string>
@ -84,13 +87,14 @@
<string name="navigation_logs">Open logs</string> <string name="navigation_logs">Open logs</string>
<string name="navigation_settings">Settings</string> <string name="navigation_settings">Settings</string>
<string name="contributors_contributions">%d contributions</string> <string name="contributors_lead">Lead Developers</string>
<string name="contributors">Contributors</string>
<string name="lead_role_kettu">Kettu - Creator</string> <string name="lead_role_kettu">Kettu - Creator</string>
<string name="lead_role_shiggycord">GoonCord - Creator</string> <string name="lead_role_shiggycord">GoonCord - Creator</string>
<string name="lead_role_bunny">Bunny - Creator</string> <string name="lead_role_bunny">Bunny - Creator</string>
<string name="role_creator_vendetta">Vendetta - Creator</string> <string name="role_creator_vendetta">Creator of Vendetta</string>
<string name="role_original_manager_creator">Original Manager 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_load_fail">Failed to load</string>
<string name="network_warning_title">Metered network</string> <string name="network_warning_title">Metered network</string>
@ -285,10 +289,10 @@
<string name="permissions_shizuku_denied">Failed to obtain Shizuku permissions</string> <string name="permissions_shizuku_denied">Failed to obtain Shizuku permissions</string>
<string name="fun_fact_prefix">Fun Fact: %s</string> <string name="fun_fact_prefix">Fun Fact: %s</string>
<string name="fun_fact_1">GoonCord on second release was meant to be custom ShiggyCord build!</string> <string name="fun_fact_1">ShiggyCord on first release was meant to be custom Kettu 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_2">The refreshed UI inside the settings was made from developer boredom</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_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 Femboys, don\'t you?</string> <string name="fun_fact_4">You love boys, don\'t you?</string>
<string name="fun_fact_5">Aliucord is the best client with old discord UI</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_6">We love you cocobo and pylix!</string>
<string name="fun_fact_7">The Vendetta react native part was a total dogshit</string> <string name="fun_fact_7">The Vendetta react native part was a total dogshit</string>
@ -296,6 +300,7 @@
<string name="fun_fact_9">This client can be run on a android TV!</string> <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_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_11">This client does not support iOS!</string>
<string name="fun_fact_12">Boom :3!</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>
</resources> </resources>

View file

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