Add Koin and other minor changes

This commit is contained in:
zt64 2022-08-15 00:25:40 -04:00
parent 68a56e880d
commit 7a34c03981
65 changed files with 1172 additions and 1373 deletions

View file

@ -1,7 +1,3 @@
val accompanistVersion = "0.26.1-alpha"
val composeVersion = "1.3.0-alpha03"
val ktorVersion = "2.1.0"
plugins {
id("com.android.application")
id("kotlin-android")
@ -28,6 +24,7 @@ android {
buildTypes {
release {
isMinifyEnabled = true
isShrinkResources = true
proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro")
}
}
@ -42,14 +39,6 @@ android {
freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn"
}
applicationVariants.all {
kotlin.sourceSets {
getByName(name) {
kotlin.srcDir("build/generated/ksp/$name/kotlin")
}
}
}
buildFeatures.compose = true
composeOptions.kotlinCompilerExtensionVersion = "1.3.0"
}
@ -68,6 +57,8 @@ dependencies {
implementation("androidx.activity:activity-compose:1.6.0-beta01")
// compose dependencies
val composeVersion = "1.3.0-alpha03"
implementation("androidx.compose.ui:ui:${composeVersion}")
implementation("androidx.compose.ui:ui-tooling:${composeVersion}")
implementation("androidx.compose.material:material-icons-extended:${composeVersion}")
@ -76,9 +67,15 @@ dependencies {
implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0-alpha01")
// accompanist dependencies
val accompanistVersion = "0.26.1-alpha"
implementation("com.google.accompanist:accompanist-systemuicontroller:${accompanistVersion}")
implementation("com.google.accompanist:accompanist-permissions:${accompanistVersion}")
// Koin
val koinVersion = "3.2.0"
implementation("io.insert-koin:koin-android:$koinVersion")
implementation("io.insert-koin:koin-androidx-compose:$koinVersion")
// other dependencies
implementation("io.coil-kt:coil-compose:2.1.0")
implementation("org.bouncycastle:bcpkix-jdk15on:1.70")
@ -90,8 +87,9 @@ dependencies {
implementation("com.github.X1nto:Taxi:1.1.0")
// ktor
val ktorVersion = "2.1.0"
implementation("io.ktor:ktor-client-core:$ktorVersion")
implementation("io.ktor:ktor-client-android:$ktorVersion")
implementation("io.ktor:ktor-client-cio:$ktorVersion")
implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion")
implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion")
}

View file

@ -21,12 +21,8 @@
android:name="android.permission.QUERY_ALL_PACKAGES"
tools:ignore="QueryAllPackagesPermission" />
<queries>
<package android:name="com.aliucord" />
<package android:name="com.discord" />
</queries>
<application
android:name=".ManagerApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name"
android:roundIcon="@mipmap/ic_launcher_round"

View file

@ -3,10 +3,8 @@
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager
import android.content.Context
import android.content.Intent
import android.os.*
import android.provider.Settings
@ -19,24 +17,26 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.preferences.sharedPreferences
import com.aliucord.manager.domain.manager.PreferencesManager
import com.aliucord.manager.ui.navigation.AppDestination
import com.aliucord.manager.ui.screen.*
import com.aliucord.manager.ui.theme.ManagerTheme
import com.aliucord.manager.ui.theme.Theme
import com.xinto.taxi.Taxi
import com.xinto.taxi.rememberBackstackNavigator
import org.koin.android.ext.android.inject
val aliucordDir = Environment.getExternalStorageDirectory().resolve("Aliucord")
class MainActivity : ComponentActivity() {
private val preferences: PreferencesManager by inject()
@OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen()
super.onCreate(savedInstanceState)
sharedPreferences = getPreferences(Context.MODE_PRIVATE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
.setData("package:com.aliucord.manager".toUri())
@ -46,17 +46,12 @@ class MainActivity : ComponentActivity() {
setContent {
ManagerTheme(
isBlack = Prefs.useBlack.get(),
isDarkTheme = run {
val theme = Theme.from(Prefs.theme.get())
theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK
}
isDarkTheme = preferences.theme == Theme.DARK || preferences.theme == Theme.SYSTEM && isSystemInDarkTheme(),
) {
val navigator = rememberBackstackNavigator<AppDestination>(AppDestination.Home)
BackHandler {
navigator.pop()
if (!navigator.pop()) finish()
}
Taxi(
@ -66,11 +61,11 @@ class MainActivity : ComponentActivity() {
) { destination ->
when (destination) {
is AppDestination.Home -> MainRootScreen(
navigator = navigator
)
is AppDestination.Plugins -> PluginsScreen(
navigator = navigator
onClickInstall = { navigator.push(AppDestination.Install) },
onClickAbout = { navigator.push(AppDestination.About) },
onClickSettings = { navigator.push(AppDestination.Settings) }
)
is AppDestination.Plugins -> PluginsScreen()
is AppDestination.Install -> InstallerScreen(
onClickBack = navigator::pop,
)
@ -80,9 +75,6 @@ class MainActivity : ComponentActivity() {
is AppDestination.About -> AboutScreen(
onClickBack = navigator::pop
)
is AppDestination.Store -> StoreScreen(
onClickBack = navigator::pop
)
}
}
}

View file

@ -0,0 +1,17 @@
package com.aliucord.manager
import android.app.Application
import com.aliucord.manager.di.*
import org.koin.android.ext.koin.androidContext
import org.koin.core.context.startKoin
class ManagerApplication : Application() {
override fun onCreate() {
super.onCreate()
startKoin {
androidContext(this@ManagerApplication)
modules(appModule, serviceModule, repositoryModule, viewModelModule, managerModule)
}
}
}

View file

@ -0,0 +1,14 @@
package com.aliucord.manager.di
import kotlinx.serialization.json.Json
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val appModule = module {
fun provideJson(): Json = Json {
ignoreUnknownKeys = true
isLenient = true
}
singleOf(::provideJson)
}

View file

@ -0,0 +1,18 @@
package com.aliucord.manager.di
import android.content.Context
import com.aliucord.manager.domain.manager.DownloadManager
import com.aliucord.manager.domain.manager.PreferencesManager
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val managerModule = module {
fun providePreferences(context: Context): PreferencesManager {
return PreferencesManager(context.getSharedPreferences("preferences", Context.MODE_PRIVATE))
}
fun provideDownloadManager(context: Context) = DownloadManager(context)
singleOf(::providePreferences)
singleOf(::provideDownloadManager)
}

View file

@ -0,0 +1,9 @@
package com.aliucord.manager.di
import com.aliucord.manager.domain.repository.GithubRepository
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val repositoryModule = module {
singleOf(::GithubRepository)
}

View file

@ -0,0 +1,9 @@
package com.aliucord.manager.di
import com.aliucord.manager.network.service.GithubService
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
val serviceModule = module {
singleOf(::GithubService)
}

View file

@ -0,0 +1,13 @@
package com.aliucord.manager.di
import com.aliucord.manager.ui.viewmodel.*
import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.dsl.module
val viewModelModule = module {
viewModelOf(::HomeViewModel)
viewModelOf(::PluginsViewModel)
viewModelOf(::AboutViewModel)
viewModelOf(::InstallViewModel)
viewModelOf(::SettingsViewModel)
}

View file

@ -0,0 +1,52 @@
package com.aliucord.manager.domain.manager
import android.app.DownloadManager
import android.content.*
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import java.io.File
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
class DownloadManager(
private val context: Context
) {
private val downloadManager = context.getSystemService<DownloadManager>()!!
suspend fun downloadDiscordApk(version: String): File {
return download("$backendHost/download/discord?v=$version", "base-$version.apk")
}
suspend fun downloadSplit(version: String, split: String): File {
return download("$backendHost/download/discord?v=$version&split=$split", "$split-$version.apk")
}
private suspend fun download(url: String, fileName: String): File {
return suspendCoroutine { continuation ->
val receiver = object : BroadcastReceiver() {
var downloadId = 0L
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (downloadId == id) {
context.unregisterReceiver(this)
continuation.resume(context.filesDir)
}
}
}
context.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
receiver.downloadId = DownloadManager.Request(url.toUri()).run {
setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
setDestinationUri(context.externalCacheDir!!.resolve(fileName).toUri())
downloadManager.enqueue(this)
}
}
}
companion object {
private const val backendHost = "https://aliucord.com/"
}
}

View file

@ -0,0 +1,18 @@
package com.aliucord.manager.domain.manager
import android.content.SharedPreferences
import com.aliucord.manager.aliucordDir
import com.aliucord.manager.domain.manager.base.BasePreferenceManager
import com.aliucord.manager.ui.theme.Theme
class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager(preferences) {
var theme by enumPreference("theme", Theme.SYSTEM)
var replaceBg by booleanPreference("replace_bg", true)
var devMode by booleanPreference("dev_mode", false)
var debuggable by booleanPreference("debuggable", false)
var useDexFromStorage by booleanPreference("use_dex_from_storage", false)
var dexLocation by stringPreference("dex_location", "$aliucordDir/Injector.dex")
var appName by stringPreference("app_name", "Aliucord")
var packageName by stringPreference("package_name", "com.aliucord")
var version by stringPreference("version", "124206")
}

View file

@ -0,0 +1,90 @@
package com.aliucord.manager.domain.manager.base
import android.content.SharedPreferences
import androidx.compose.runtime.*
import androidx.core.content.edit
import kotlin.reflect.KProperty
abstract class BasePreferenceManager(
private val prefs: SharedPreferences
) {
protected fun getString(key: String, defaultValue: String) = prefs.getString(key, defaultValue) ?: defaultValue
private fun getBoolean(key: String, defaultValue: Boolean) = prefs.getBoolean(key, defaultValue)
private fun getInt(key: String, defaultValue: Int) = prefs.getInt(key, defaultValue)
private fun getFloat(key: String, defaultValue: Float) = prefs.getFloat(key, defaultValue)
protected inline fun <reified E : Enum<E>> getEnum(key: String, defaultValue: E) =
enumValueOf<E>(getString(key, defaultValue.name))
protected fun putString(key: String, value: String?) = prefs.edit { putString(key, value) }
private fun putBoolean(key: String, value: Boolean) = prefs.edit { putBoolean(key, value) }
private fun putInt(key: String, value: Int) = prefs.edit { putInt(key, value) }
private fun putFloat(key: String, value: Float) = prefs.edit { putFloat(key, value) }
protected inline fun <reified E : Enum<E>> putEnum(key: String, value: E) = putString(key, value.name)
protected class Preference<T>(
private val key: String,
defaultValue: T,
getter: (key: String, defaultValue: T) -> T,
private val setter: (key: String, newValue: T) -> Unit
) {
@Suppress("RedundantSetter")
var value by mutableStateOf(getter(key, defaultValue))
private set
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
value = newValue
setter(key, newValue)
}
}
protected fun stringPreference(
key: String,
defaultValue: String
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getString,
setter = ::putString
)
protected fun booleanPreference(
key: String,
defaultValue: Boolean
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getBoolean,
setter = ::putBoolean
)
protected fun intPreference(
key: String,
defaultValue: Int
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getInt,
setter = ::putInt
)
protected fun floatPreference(
key: String,
defaultValue: Float
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getFloat,
setter = ::putFloat
)
protected inline fun <reified E : Enum<E>> enumPreference(
key: String,
defaultValue: E
) = Preference(
key = key,
defaultValue = defaultValue,
getter = ::getEnum,
setter = ::putEnum
)
}

View file

@ -3,9 +3,10 @@
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.model
package com.aliucord.manager.domain.model
import kotlinx.serialization.Serializable
import kotlinx.serialization.json.JsonObject
@Serializable
data class Manifest(
@ -15,8 +16,13 @@ data class Manifest(
val version: String,
val updateUrl: String,
val changelog: String?,
val changelogMedia: String?
val changelogMedia: String?,
val links: JsonObject
) {
val repositoryUrl = updateUrl
.replace("raw.githubusercontent.com", "github.com")
.replaceFirst("/builds.*".toRegex(), "")
@Serializable
data class Author(val name: String, val id: Long)
}

View file

@ -0,0 +1,31 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.domain.model
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream
import java.io.File
import java.util.zip.ZipException
import java.util.zip.ZipFile
private val json = Json {
ignoreUnknownKeys = true
}
@OptIn(ExperimentalSerializationApi::class)
data class Plugin(val file: File) {
val manifest: Manifest
init {
ZipFile(file).use { zipFile ->
val entry = zipFile.getEntry("manifest.json")
?: throw ZipException("Plugin ${file.nameWithoutExtension} has no manifest.")
manifest = zipFile.getInputStream(entry).use { zis -> json.decodeFromStream(zis) }
}
}
}

View file

@ -0,0 +1,19 @@
package com.aliucord.manager.domain.repository
import com.aliucord.manager.network.dto.GithubUser
import com.aliucord.manager.network.dto.Version
import com.aliucord.manager.network.service.GithubService
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
class GithubRepository(
private val json: Json,
private val service: GithubService
) {
suspend fun getCommits(page: Int) = service.getCommits(page)
suspend fun getContributors() = service.getContributors()
.sortedByDescending(GithubUser::contributions)
suspend fun getVersion() = json.decodeFromString<Version>(service.getVersion())
}

View file

@ -6,28 +6,30 @@ import android.content.pm.PackageInstaller
import android.os.IBinder
import android.util.Log
import android.widget.Toast
import com.aliucord.manager.BuildConfig
class InstallService : Service() {
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
when (val statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
confirmationIntent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
@Suppress("DEPRECATION") // No.
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)!!
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(confirmationIntent)
}
PackageInstaller.STATUS_SUCCESS -> Toast.makeText(
this,
"Successfully installed Aliucord!",
Toast.LENGTH_LONG
/* context = */ this,
/* text = */ "Successfully installed Aliucord!",
/* duration = */ Toast.LENGTH_LONG
).show()
PackageInstaller.STATUS_FAILURE_ABORTED -> Toast.makeText(
this,
"Aborted installation",
Toast.LENGTH_LONG
/* context = */ this,
/* text = */ "Aborted installation",
/* duration = */ Toast.LENGTH_LONG
).show()
else -> {
Log.i("Installer", "Installation failed: error code $statusCode")
Log.i(BuildConfig.TAG, "Installation failed: error code $statusCode")
Toast.makeText(this, "Failed: Error $statusCode", Toast.LENGTH_LONG).show()
}
}

View file

@ -1,50 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.installer.util
import android.app.DownloadManager
import android.content.*
import androidx.core.net.toUri
import java.io.File
import kotlin.coroutines.resume
import kotlin.coroutines.suspendCoroutine
object DownloadUtils {
private const val backendHost = "https://aliucord.com/"
suspend fun downloadDiscordApk(ctx: Context, version: String) =
download(ctx, "$backendHost/download/discord?v=$version", "base-$version.apk")
suspend fun downloadSplit(ctx: Context, version: String, split: String) =
download(ctx, "$backendHost/download/discord?v=$version&split=$split", "$split-$version.apk")
private suspend fun download(ctx: Context, url: String, fileName: String): File {
val downloadManager = ctx.getSystemService(Context.DOWNLOAD_SERVICE) as DownloadManager
return suspendCoroutine { continuation ->
val receiver = object : BroadcastReceiver() {
var downloadId = 0L
override fun onReceive(context: Context, intent: Intent) {
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
if (downloadId == id) {
ctx.unregisterReceiver(this)
continuation.resume(context.filesDir)
}
}
}
ctx.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
receiver.downloadId = DownloadManager.Request(url.toUri()).run {
setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
setDestinationUri(ctx.externalCacheDir!!.resolve(fileName).toUri())
downloadManager.enqueue(this)
}
}
}
}

View file

@ -5,7 +5,6 @@
package com.aliucord.manager.installer.util
import com.aliucord.manager.preferences.Prefs
import pxb.android.axml.*
object ManifestPatcher {
@ -15,8 +14,12 @@ object ManifestPatcher {
private const val DEBUGGABLE = "debuggable"
private const val LABEL = "label"
fun patchManifest(manifestBytes: ByteArray): ByteArray {
val packageName = Prefs.packageName.get()
fun patchManifest(
manifestBytes: ByteArray,
packageName: String,
appName: String,
debuggable: Boolean
): ByteArray {
val reader = AxmlReader(manifestBytes)
val writer = AxmlWriter()
@ -38,12 +41,12 @@ object ManifestPatcher {
"application" -> object : ReplaceAttrsVisitor(
nv,
mapOf(
LABEL to Prefs.appName.get(),
DEBUGGABLE to Prefs.debuggable.get(),
LABEL to appName,
DEBUGGABLE to debuggable,
USES_CLEARTEXT_TRAFFIC to true
)
) {
private var addDebuggable = Prefs.debuggable.get()
private var addDebuggable = debuggable
private var addUseClearTextTraffic = true
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
@ -56,12 +59,15 @@ object ManifestPatcher {
val visitor = super.child(ns, name)
return when (name) {
"activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to Prefs.appName.get()))
"activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to appName))
"provider" -> object : NodeVisitor(visitor) {
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
super.attr(
ns, name, resourceId, type,
if (name == "authorities") (value as String).replace("com.discord", packageName) else value
/* ns = */ ns,
/* name = */ name,
/* resourceId = */ resourceId,
/* type = */ type,
/* obj = */ if (name == "authorities") (value as String).replace("com.discord", packageName) else value
)
}
}
@ -91,16 +97,20 @@ object ManifestPatcher {
return writer.toByteArray()
}
fun renamePackage(manifestBytes: ByteArray): ByteArray {
val packageName = Prefs.packageName.get()
fun renamePackage(
manifestBytes: ByteArray,
packageName: String
): ByteArray {
val reader = AxmlReader(manifestBytes)
val writer = AxmlWriter()
reader.accept(object : AxmlVisitor(writer) {
override fun child(ns: String?, name: String?): ReplaceAttrsVisitor {
return ReplaceAttrsVisitor(super.child(ns, name), mapOf("package" to packageName))
reader.accept(
object : AxmlVisitor(writer) {
override fun child(ns: String?, name: String?): ReplaceAttrsVisitor {
return ReplaceAttrsVisitor(super.child(ns, name), mapOf("package" to packageName))
}
}
})
)
return writer.toByteArray()
}

View file

@ -3,8 +3,9 @@
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.util
package com.aliucord.manager.installer.util
import com.aliucord.manager.aliucordDir
import com.android.apksig.ApkSigner
import org.bouncycastle.asn1.x500.X500Name
import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo
@ -34,9 +35,9 @@ object Signer {
val certificate = keyStore.getCertificate(alias) as X509Certificate
ApkSigner.SignerConfig.Builder(
"Aliucord Manager signer",
keyStore.getKey(alias, password) as PrivateKey,
listOf(certificate),
/* name = */ "Aliucord Manager signer",
/* privateKey = */ keyStore.getKey(alias, password) as PrivateKey,
/* certificates = */ listOf(certificate),
).build()
}
@ -62,13 +63,13 @@ object Signer {
generateKeyPair()
}
val builder = X509v3CertificateBuilder(
x500Name,
serialNumber,
Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L),
Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L),
Locale.ENGLISH,
x500Name,
SubjectPublicKeyInfo.getInstance(pair.public.encoded)
/* issuer = */ x500Name,
/* serial = */ serialNumber,
/* notBefore = */ Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L),
/* notAfter = */ Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L),
/* dateLocale = */ Locale.ENGLISH,
/* subject = */ x500Name,
/* publicKeyInfo = */ SubjectPublicKeyInfo.getInstance(pair.public.encoded)
)
val signer = JcaContentSignerBuilder("SHA1withRSA").build(pair.private)

View file

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.util
package com.aliucord.manager.installer.util
import com.aliucord.libzip.Zip
import java.io.InputStream

View file

@ -1,57 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.model
import android.util.Log
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.util.aliucordDir
import com.aliucord.manager.util.json
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.decodeFromStream
import java.io.File
import java.util.zip.ZipException
import java.util.zip.ZipFile
@OptIn(ExperimentalSerializationApi::class)
data class Plugin(val file: File) {
val manifest: Manifest
init {
ZipFile(file).use { zipFile ->
val entry = zipFile.getEntry("manifest.json")
?: throw ZipException("Plugin ${file.nameWithoutExtension} has no manifest.")
manifest = zipFile.getInputStream(entry).use { zis -> json.decodeFromStream(zis) }
}
}
companion object {
private val pluginsDir = aliucordDir.resolve("plugins")
fun loadAll(): List<Plugin> {
if (!pluginsDir.exists() && !pluginsDir.mkdirs()) {
Log.e(BuildConfig.TAG, "Failed to create plugins dir. Missing Permissions?")
return emptyList()
}
val files = pluginsDir.listFiles() ?: return emptyList()
return files.mapNotNull { file ->
if (file.extension == "zip") {
try {
Plugin(file)
} catch (th: Throwable) {
Log.e(BuildConfig.TAG, "Failed to load plugin ${file.nameWithoutExtension}", th)
null
}
} else {
Log.w(BuildConfig.TAG, "Found non zip ${file.name} in plugins folder.")
null
}
}.sortedBy { it.manifest.name }
}
}
}

View file

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.model.github
package com.aliucord.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
@ -16,6 +16,8 @@ data class Commit(
val commit: Commit,
val author: Author
) {
@Serializable data class Author(@SerialName("login") val name: String)
@Serializable data class Commit(val message: String)
@Serializable
data class Author(@SerialName("login") val name: String)
@Serializable
data class Commit(val message: String)
}

View file

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.model.github
package com.aliucord.manager.network.dto
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable

View file

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.model.github
package com.aliucord.manager.network.dto
import kotlinx.serialization.Serializable

View file

@ -0,0 +1,47 @@
package com.aliucord.manager.network.service
import com.aliucord.manager.network.dto.Commit
import com.aliucord.manager.network.dto.GithubUser
import io.ktor.client.*
import io.ktor.client.call.*
import io.ktor.client.engine.cio.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
import kotlinx.serialization.json.Json
class GithubService(
private val json: Json
) {
private val httpClient = HttpClient(CIO) {
install(ContentNegotiation) {
json(json)
}
}
suspend fun getCommits(page: Int): List<Commit> = withContext(Dispatchers.IO) {
httpClient.get(commitsUrl) {
parameter("page", page)
}.body()
}
suspend fun getVersion(): String = withContext(Dispatchers.IO) {
httpClient.get(dataUrl).bodyAsText()
}
suspend fun getContributors(): List<GithubUser> = withContext(Dispatchers.IO) {
httpClient.get(contributorsUrl).body()
}
companion object {
private const val org = "Aliucord"
private const val repo = "Aliucord"
private const val commitsUrl = "https://api.github.com/repos/$org/$repo/commits"
private const val contributorsUrl = "https://api.github.com/repos/$org/$repo/contributors"
private const val dataUrl = "https://raw.githubusercontent.com/$org/$repo/builds/data.json"
}
}

View file

@ -1,67 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.preferences
import android.content.SharedPreferences
import androidx.compose.runtime.mutableStateOf
import androidx.core.content.edit
import kotlin.reflect.KProperty
lateinit var sharedPreferences: SharedPreferences
class Preference<T>(
private val key: String,
defaultValue: T,
getter: SharedPreferences.(key: String, defaultValue: T) -> T?,
private val setter: SharedPreferences.Editor.(key: String, newValue: T) -> Unit
) {
private val value = mutableStateOf(sharedPreferences.getter(key, defaultValue) ?: defaultValue)
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value.value
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) = set(newValue)
fun get() = value.value
fun set(newValue: T) {
if (value.value == newValue) return
value.value = newValue
sharedPreferences.edit { setter(key, newValue) }
}
}
fun boolPreference(
key: String,
defaultValue: Boolean = false
) = Preference(
key,
defaultValue,
SharedPreferences::getBoolean,
SharedPreferences.Editor::putBoolean
)
fun intPreference(
key: String,
defaultValue: Int = 0
) = Preference(
key,
defaultValue,
SharedPreferences::getInt,
SharedPreferences.Editor::putInt
)
fun stringPreference(
key: String,
defaultValue: String
) = Preference(
key,
defaultValue,
SharedPreferences::getString,
SharedPreferences.Editor::putString
)

View file

@ -1,20 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.preferences
import com.aliucord.manager.util.aliucordDir
object Prefs {
val theme = intPreference("theme")
val useBlack = boolPreference("use_black")
val replaceBg = boolPreference("replace_bg", true)
val devMode = boolPreference("dev_mode", false)
val debuggable = boolPreference("debuggable")
val useDexFromStorage = boolPreference("use_dex_from_storage")
val dexLocation = stringPreference("dex_location", "$aliucordDir/Injector.dex")
val appName = stringPreference("app_name", "Aliucord")
val packageName = stringPreference("package_name", "com.aliucord")
}

View file

@ -3,26 +3,27 @@
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.component.about
package com.aliucord.manager.ui.component
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.grid.*
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.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.aliucord.manager.R
import com.aliucord.manager.model.github.GithubUser
import com.aliucord.manager.network.dto.GithubUser
@Composable
fun ContributorsCard(contributors: List<GithubUser>) {
ElevatedCard(
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
) {
ElevatedCard {
Column(
modifier = Modifier
.padding(16.dp)
@ -56,3 +57,17 @@ fun ContributorsCard(contributors: List<GithubUser>) {
}
}
}
@Composable
fun ContributorEntry(name: String) {
val uriHandler = LocalUriHandler.current
AsyncImage(
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
.clickable { uriHandler.openUri("https://github.com/$name") },
model = "https://github.com/$name.png",
contentDescription = name
)
}

View file

@ -32,7 +32,7 @@ fun GrantPermission(permissionState: PermissionState) {
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
stringResource(R.string.permission_required),
text = stringResource(R.string.permission_required),
style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center
)
@ -42,7 +42,7 @@ fun GrantPermission(permissionState: PermissionState) {
Row {
val context = LocalContext.current
Button(onClick = { permissionState.launchPermissionRequest() }) {
Button(onClick = permissionState::launchPermissionRequest) {
Text(stringResource(R.string.permission_grant))
}
@ -52,15 +52,11 @@ fun GrantPermission(permissionState: PermissionState) {
onClick = {
context.startActivity(
Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
/* action = */ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
/* uri = */ Uri.fromParts("package", BuildConfig.APPLICATION_ID, null)
)
)
},
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
),
}
) {
Text(stringResource(R.string.open_settings))
}

View file

@ -3,15 +3,17 @@
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.component.installer
package com.aliucord.manager.ui.component
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Download
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.aliucord.manager.R
@ -22,34 +24,27 @@ fun InstallerDialog(
onDismissRequest: () -> Unit,
onConfirm: (DownloadMethod) -> Unit
) {
var selectedMethod by remember { mutableStateOf(DownloadMethod.DOWNLOAD) }
var selectedMethod by rememberSaveable { mutableStateOf(DownloadMethod.DOWNLOAD) }
AlertDialog(
icon = {
Icon(
painter = painterResource(id = R.drawable.ic_download_24dp),
imageVector = Icons.Default.Download,
contentDescription = stringResource(R.string.download_method)
)
},
title = {
Row(
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(stringResource(R.string.download_method))
}
Text(stringResource(R.string.download_method))
},
text = {
Column {
mapOf(
DownloadMethod.DOWNLOAD to stringResource(R.string.download_apk),
DownloadMethod.SELECT to stringResource(R.string.select_apk)
// DownloadMethod.SELECT to stringResource(R.string.select_apk)
).forEach { (method, name) ->
Column {
Row(
modifier = Modifier.clickable {
selectedMethod = method
},
modifier = Modifier.clickable { selectedMethod = method },
verticalAlignment = Alignment.CenterVertically
) {
Text(

View file

@ -1,31 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.component.about
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource
import coil.compose.AsyncImage
import com.aliucord.manager.R
@Composable
fun ContributorEntry(name: String) {
val uriHandler = LocalUriHandler.current
AsyncImage(
modifier = Modifier
.fillMaxSize()
.clip(CircleShape)
.clickable { uriHandler.openUri("https://github.com/$name") },
model = "https://github.com/$name.png",
contentDescription = stringResource(R.string.contributor)
)
}

View file

@ -1,37 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.component.about
import androidx.compose.foundation.layout.*
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
@Composable
fun TeamCard() = ElevatedCard(
modifier = Modifier.wrapContentHeight().fillMaxWidth()
) {
Column(
modifier = Modifier.padding(16.dp).fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = stringResource(R.string.team),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
UserEntry("Juby210", "Fox")
UserEntry("Vendicated", "the ven")
}
}
}

View file

@ -1,40 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.component.about
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
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.unit.dp
import coil.compose.AsyncImage
@Composable
fun UserEntry(name: String, roles: String) = Column {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
AsyncImage(
modifier = Modifier
.size(54.dp)
.clip(CircleShape),
model = "https://github.com/$name.png",
contentDescription = null
)
Column {
Text(
text = name,
style = MaterialTheme.typography.titleLarge
)
Text(roles)
}
}
}

View file

@ -5,38 +5,32 @@
package com.aliucord.manager.ui.component.plugins
import android.annotation.SuppressLint
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.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Person
import androidx.compose.material.icons.filled.History
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.aliucord.manager.R
import com.aliucord.manager.model.Plugin
import com.aliucord.manager.domain.model.Plugin
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)?\\}")
@SuppressLint("ComposableNaming") // Can't use MaterialTheme without Composable, but this is not a component
@Composable
private fun AnnotatedString.Builder.hyperlink(content: String) {
var idx = 0
@ -51,7 +45,7 @@ private fun AnnotatedString.Builder.hyperlink(content: String) {
append(content.substring(idx, start))
withStyle(
style = SpanStyle(
SpanStyle(
color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline
)
@ -77,9 +71,8 @@ fun Changelog(
onDismissRequest = onDismiss,
icon = {
Icon(
painter = painterResource(R.drawable.ic_history_white_24dp),
contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name),
tint = MaterialTheme.colorScheme.onSurface
imageVector = Icons.Default.History,
contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name)
)
},
title = { Text(plugin.manifest.name) },
@ -91,17 +84,12 @@ fun Changelog(
.fillMaxWidth()
.heightIn(max = 90.dp)
.clip(RoundedCornerShape(14.dp)),
model = ImageRequest.Builder(LocalContext.current)
.data(mediaUrl)
.build(),
placeholder = rememberVectorPainter(image = Icons.Default.Person),
model = mediaUrl,
contentDescription = stringResource(R.string.changelog_media)
)
}
LazyColumn(
modifier = Modifier.wrapContentHeight()
) {
LazyColumn {
items(plugin.manifest.changelog!!.lines()) {
var line = it.trim()
@ -113,16 +101,17 @@ fun Changelog(
} while (line.startsWith("#"))
Text(
line.trimStart(),
text = line.trimStart(),
fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
)
}
'*' -> {
LinkText(
buildAnnotatedString {
modifier = Modifier.padding(bottom = 2.dp),
annotatedString = buildAnnotatedString {
withStyle(
style = SpanStyle(
SpanStyle(
color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold
)
@ -131,26 +120,25 @@ fun Changelog(
}
hyperlink(line.substring(1))
},
Modifier.padding(bottom = 2.dp)
}
)
}
else -> {
when {
line.endsWith("marginTop}") -> {
val color = MaterialTheme.colorScheme.onSurface
Text(
line,
text = line,
fontWeight = FontWeight.Bold,
color = color,
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
)
}
line.all { c -> c == '=' } -> {
} // Discord ignores =======
line.all { c -> c == '=' } -> { } // Discord ignores =======
else -> {
LinkText(
buildAnnotatedString {
annotatedString = buildAnnotatedString {
hyperlink(line)
}
)
@ -164,9 +152,7 @@ fun Changelog(
}
},
confirmButton = {
Button(
onClick = onDismiss
) {
Button(onClick = onDismiss) {
Text(stringResource(R.string.close))
}
}
@ -174,17 +160,20 @@ fun Changelog(
}
@Composable
private fun LinkText(annotatedString: AnnotatedString, modifier: Modifier = Modifier) {
private fun LinkText(
modifier: Modifier = Modifier,
annotatedString: AnnotatedString
) {
val urlHandler = LocalUriHandler.current
ClickableText(
modifier = modifier,
text = annotatedString,
style = TextStyle(color = LocalContentColor.current),
onClick = { offset ->
annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let {
urlHandler.openUri(it.item)
}
},
modifier = modifier
}
)
}

View file

@ -5,13 +5,15 @@
package com.aliucord.manager.ui.component.plugins
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.selection.toggleable
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.History
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.platform.LocalUriHandler
@ -21,29 +23,26 @@ import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
import com.aliucord.manager.model.Plugin
import com.aliucord.manager.domain.model.Plugin
@Composable
fun PluginCard(
plugin: Plugin,
onDelete: () -> Unit,
onShowChangelog: () -> Unit
onClickDelete: () -> Unit,
onClickShowChangelog: () -> Unit
) {
val uriHandler = LocalUriHandler.current
ElevatedCard(
modifier = Modifier.fillMaxWidth()
) {
ElevatedCard {
Column(
modifier = Modifier.padding(16.dp)
modifier = Modifier
.fillMaxWidth()
.padding(16.dp)
) {
var isEnabled by remember { mutableStateOf(true) }
val uriHandler = LocalUriHandler.current
var isEnabled by rememberSaveable { mutableStateOf(true) }
// Header
Row(
modifier = Modifier.toggleable(value = isEnabled) {
isEnabled = it
}
modifier = Modifier.clickable { isEnabled = !isEnabled }
) {
Column {
// Name
@ -79,16 +78,14 @@ fun PluginCard(
}
}
}
ClickableText(
text = authors,
style = MaterialTheme.typography.labelMedium,
onClick = {
authors.getStringAnnotations("authorId", it, it)
onClick = { offset ->
authors.getStringAnnotations("authorId", offset, offset)
.firstOrNull()
?.let { (id) ->
val url = "https://discord.com/users/$id"
uriHandler.openUri(url)
}
?.let { (id) -> uriHandler.openUri("https://discord.com/users/$id") }
}
)
}
@ -102,15 +99,15 @@ fun PluginCard(
)
}
Divider(modifier = Modifier.alpha(0.1f))
Divider(modifier = Modifier.alpha(0.3f))
// Description
Text(
text = plugin.manifest.description,
style = MaterialTheme.typography.bodyMedium,
modifier = Modifier
.heightIn(max = 150.dp, min = 40.dp)
.padding(top = 10.dp),
text = plugin.manifest.description,
style = MaterialTheme.typography.bodyMedium
)
Row(
@ -118,23 +115,19 @@ fun PluginCard(
) {
IconButton(
onClick = {
uriHandler.openUri(
plugin.manifest.updateUrl
.replace("raw.githubusercontent.com", "github.com")
.replaceFirst("/builds.*".toRegex(), "")
)
uriHandler.openUri(plugin.manifest.repositoryUrl)
}
) {
Icon(
painter = painterResource(R.drawable.ic_account_github_white_24dp),
contentDescription = "GitHub"
contentDescription = stringResource(R.string.github)
)
}
if (plugin.manifest.changelog != null) {
IconButton(onClick = onShowChangelog) {
IconButton(onClick = onClickShowChangelog) {
Icon(
painter = painterResource(R.drawable.ic_history_white_24dp),
imageVector = Icons.Default.History,
contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name)
)
}
@ -142,12 +135,10 @@ fun PluginCard(
Spacer(Modifier.weight(1f, true))
IconButton(
onClick = onDelete
) {
IconButton(onClick = onClickDelete) {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.uninstall_plugin, plugin.manifest.name),
contentDescription = stringResource(R.string.uninstall),
tint = MaterialTheme.colorScheme.error
)
}

View file

@ -1,34 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.component.settings
import androidx.compose.foundation.clickable
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.aliucord.manager.preferences.Preference
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PreferenceItem(
icon: @Composable (() -> Unit)? = null,
title: @Composable () -> Unit,
description: @Composable (() -> Unit)? = null,
preference: Preference<Boolean>
) = ListItem(
modifier = Modifier.clickable {
preference.set(!preference.get())
},
leadingContent = icon,
headlineText = title,
supportingText = description,
trailingContent = {
Switch(
checked = preference.get(),
onCheckedChange = preference::set
)
}
)

View file

@ -1,72 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.component.settings
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Style
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.aliucord.manager.R
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.theme.Theme
@Composable
fun ThemeDialog(
onDismissRequest: () -> Unit,
onConfirm: (Theme) -> Unit
) {
var selectedTheme by remember { mutableStateOf(Theme.from(Prefs.theme.get())) }
AlertDialog(
onDismissRequest = onDismissRequest,
icon = {
Icon(
imageVector = Icons.Default.Style,
contentDescription = "Theme"
)
},
title = { Text(stringResource(R.string.theme)) },
text = {
Column {
Theme.values().forEach { theme ->
Row(
modifier = Modifier.clickable {
selectedTheme = theme
},
verticalAlignment = Alignment.CenterVertically
) {
Text(
theme.displayName,
style = MaterialTheme.typography.labelLarge
)
Spacer(Modifier.weight(1f, true))
RadioButton(
selected = theme == selectedTheme,
onClick = { selectedTheme = theme }
)
}
}
}
},
confirmButton = {
Button(
onClick = {
onConfirm(selectedTheme)
onDismissRequest()
}
) {
Text(stringResource(R.string.apply))
}
}
)
}

View file

@ -1,32 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.component.settings
import androidx.compose.foundation.clickable
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import com.aliucord.manager.R
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.theme.Theme
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun ThemeSetting(
onClick: () -> Unit,
) = ListItem(
modifier = Modifier.clickable { onClick() },
headlineText = { Text(stringResource(R.string.theme)) },
supportingText = { Text(stringResource(R.string.theme_setting_description)) },
trailingContent = {
FilledTonalButton(
onClick = onClick
) {
Text(Theme.from(Prefs.theme.get()).displayName)
}
}
)

View file

@ -23,9 +23,6 @@ sealed interface AppDestination : Destination {
@Parcelize
object Install : AppDestination
@Parcelize
object Store : AppDestination
@Parcelize
object About : AppDestination

View file

@ -6,24 +6,30 @@
package com.aliucord.manager.ui.screen
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NavigateBefore
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.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import coil.compose.AsyncImage
import com.aliucord.manager.R
import com.aliucord.manager.ui.component.about.ContributorsCard
import com.aliucord.manager.ui.component.about.TeamCard
import com.aliucord.manager.ui.component.ContributorsCard
import com.aliucord.manager.ui.viewmodel.AboutViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AboutScreen(onClickBack: () -> Unit) {
fun AboutScreen(
viewModel: AboutViewModel = getViewModel(),
onClickBack: () -> Unit
) {
Scaffold(
topBar = {
MediumTopAppBar(
@ -40,14 +46,35 @@ fun AboutScreen(onClickBack: () -> Unit) {
}
) { paddingValues ->
Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier
.padding(paddingValues)
.padding(horizontal = 12.dp, vertical = 14.dp)
.padding(horizontal = 12.dp, vertical = 14.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
val uriHandler = LocalUriHandler.current
val viewModel: AboutViewModel = viewModel()
TeamCard()
ElevatedCard {
Column(
modifier = Modifier
.padding(16.dp)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Text(
text = stringResource(R.string.team),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)
Column(
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
UserEntry("Juby210", "Fox")
UserEntry("Vendicated", "the ven")
}
}
}
ContributorsCard(viewModel.contributors)
Spacer(Modifier.weight(1f, true))
@ -58,11 +85,40 @@ fun AboutScreen(onClickBack: () -> Unit) {
) {
Icon(
painter = painterResource(R.drawable.ic_account_github_white_24dp),
contentDescription = "GitHub"
contentDescription = stringResource(R.string.github)
)
Spacer(Modifier.padding(4.dp))
Spacer(Modifier.width(ButtonDefaults.IconSpacing))
Text(stringResource(R.string.github))
}
}
}
}
@Composable
private fun UserEntry(name: String, roles: String) {
Column {
Row(
verticalAlignment = Alignment.CenterVertically,
horizontalArrangement = Arrangement.spacedBy(8.dp)
) {
AsyncImage(
modifier = Modifier
.size(54.dp)
.clip(CircleShape),
model = "https://github.com/$name.png",
contentDescription = name
)
Column {
Text(
text = name,
style = MaterialTheme.typography.titleLarge
)
Text(roles)
}
}
}
}

View file

@ -1,42 +1,35 @@
package com.aliucord.manager.ui.screen
import android.content.res.Configuration
import androidx.compose.animation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.layout.padding
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material.icons.filled.Info
import androidx.compose.material.icons.filled.Settings
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.dp
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import com.aliucord.manager.ui.navigation.AppDestination
import com.aliucord.manager.ui.navigation.HomeDestination
import com.xinto.taxi.*
import com.xinto.taxi.Taxi
import com.xinto.taxi.rememberNavigator
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable
fun MainRootScreen(navigator: BackstackNavigator<AppDestination>) {
fun MainRootScreen(
onClickInstall: () -> Unit,
onClickAbout: () -> Unit,
onClickSettings: () -> Unit
) {
val mainRootNavigator = rememberNavigator(HomeDestination.HOME)
val currentDestination = mainRootNavigator.currentDestination
val orientation = LocalConfiguration.current.orientation
var isMenuExpanded by remember { mutableStateOf(false) }
Scaffold(
modifier = Modifier
.fillMaxSize(),
topBar = {
MediumTopAppBar(
title = {
Text(stringResource(mainRootNavigator.currentDestination.label))
},
title = { Text(stringResource(mainRootNavigator.currentDestination.label)) },
actions = {
val localUriHandler = LocalUriHandler.current
@ -47,125 +40,64 @@ fun MainRootScreen(navigator: BackstackNavigator<AppDestination>) {
) {
Icon(
painter = painterResource(R.drawable.ic_discord),
contentDescription = stringResource(R.string.support_server),
tint = MaterialTheme.colorScheme.onBackground
contentDescription = stringResource(R.string.support_server)
)
}
Box(
modifier = Modifier.wrapContentSize(Alignment.TopEnd)
) {
IconButton(
onClick = { isMenuExpanded = !isMenuExpanded }
) {
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(R.string.show_menu),
tint = MaterialTheme.colorScheme.onBackground
)
}
IconButton(onClick = onClickAbout) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = stringResource(R.string.about)
)
}
DropdownMenu(
offset = DpOffset(x = 12.dp, y = 0.dp),
expanded = isMenuExpanded,
onDismissRequest = { isMenuExpanded = false }
) {
DropdownMenuItem(
onClick = {
isMenuExpanded = false
navigator.push(AppDestination.About)
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Info,
contentDescription = "About"
)
},
text = { Text(stringResource(R.string.about)) }
)
DropdownMenuItem(
onClick = {
isMenuExpanded = false
navigator.push(AppDestination.Settings)
},
leadingIcon = {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = "Settings"
)
},
text = { Text(stringResource(R.string.settings)) }
)
}
IconButton(onClick = onClickSettings) {
Icon(
imageVector = Icons.Default.Settings,
contentDescription = stringResource(R.string.settings)
)
}
}
)
},
bottomBar = {
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
NavigationBar {
val currentDestination = mainRootNavigator.currentDestination
HomeDestination.values().forEach { destination ->
NavigationBarItem(
selected = currentDestination == destination,
icon = {
if (currentDestination == destination) Icon(
destination.selectedIcon,
stringResource(destination.label)
) else Icon(destination.unselectedIcon, stringResource(destination.label))
},
label = { Text(stringResource(destination.label)) },
onClick = { mainRootNavigator.replace(destination) }
)
}
NavigationBar {
HomeDestination.values().forEach { destination ->
NavigationBarItem(
selected = currentDestination == destination,
icon = {
if (currentDestination == destination) {
Icon(
imageVector = destination.selectedIcon,
contentDescription = stringResource(destination.label)
)
} else {
Icon(
imageVector = destination.unselectedIcon,
contentDescription = stringResource(destination.label)
)
}
},
label = { Text(stringResource(destination.label)) },
onClick = { mainRootNavigator.replace(destination) }
)
}
}
}
) { paddingValues ->
Row(
modifier = Modifier
.padding(paddingValues)
.padding(horizontal = 12.dp, vertical = 14.dp)
) {
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
NavRail(
currentDestination = currentDestination,
onClickDestination = mainRootNavigator::replace
Taxi(
modifier = Modifier.padding(paddingValues),
navigator = mainRootNavigator,
transitionSpec = { fadeIn() with fadeOut() }
) { destination ->
when (destination) {
HomeDestination.HOME -> HomeScreen(
onClickInstall = onClickInstall
)
}
Taxi(
modifier = Modifier.weight(1f, true),
navigator = mainRootNavigator,
transitionSpec = { fadeIn() with fadeOut() }
) { destination ->
when (destination) {
HomeDestination.HOME -> HomeScreen(navigator = navigator)
HomeDestination.PLUGINS -> PluginsScreen(navigator = navigator)
}
HomeDestination.PLUGINS -> PluginsScreen()
}
}
}
}
@Composable
fun NavRail(
currentDestination: HomeDestination,
onClickDestination: (HomeDestination) -> Unit
) = NavigationRail {
HomeDestination.values().forEach { destination ->
NavigationRailItem(
selected = currentDestination == destination,
icon = {
if (currentDestination == destination) Icon(
destination.selectedIcon,
stringResource(destination.label)
) else Icon(destination.unselectedIcon, stringResource(destination.label))
},
label = { Text(stringResource(destination.label)) },
onClick = { onClickDestination(destination) }
)
}
}

View file

@ -12,63 +12,56 @@ import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import androidx.paging.*
import androidx.paging.LoadState
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import com.aliucord.manager.R
import com.aliucord.manager.model.github.Commit
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.component.installer.DownloadMethod
import com.aliucord.manager.ui.component.installer.InstallerDialog
import com.aliucord.manager.ui.navigation.AppDestination
import com.aliucord.manager.ui.component.DownloadMethod
import com.aliucord.manager.ui.component.InstallerDialog
import com.aliucord.manager.ui.viewmodel.HomeViewModel
import com.aliucord.manager.util.Github
import com.xinto.taxi.BackstackNavigator
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
val viewModel: HomeViewModel = viewModel()
fun HomeScreen(
viewModel: HomeViewModel = getViewModel(),
onClickInstall: () -> Unit
) {
var showOptionsDialog by remember { mutableStateOf(false) }
if (showOptionsDialog) {
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri == null) return@rememberLauncherForActivityResult
navigator.push(AppDestination.Install)
onClickInstall()
}
InstallerDialog(
onDismissRequest = { showOptionsDialog = false },
onConfirm = { method ->
if (method == DownloadMethod.DOWNLOAD) {
navigator.push(AppDestination.Install)
} else {
filePicker.launch(arrayOf("application/octet-stream"))
when (method) {
DownloadMethod.DOWNLOAD -> onClickInstall()
DownloadMethod.SELECT -> filePicker.launch(arrayOf("application/octet-stream"))
}
}
)
}
Column(
verticalArrangement = Arrangement.spacedBy(12.dp)
modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
ElevatedCard(
modifier = Modifier.wrapContentHeight()
) {
ElevatedCard {
Column(
modifier = Modifier
.padding(16.dp)
@ -77,52 +70,53 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
) {
Column {
Text(
"Aliucord${Prefs.packageName.get().let { if (it != "com.aliucord") " ($it)" else "" }}",
text = "Aliucord${viewModel.preferences.packageName.let { if (it != "com.aliucord") " ($it)" else "" }}",
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)
Text(buildAnnotatedString {
append("Supported version: ")
Text(
buildAnnotatedString {
append("Supported version: ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(viewModel.supportedVersion)
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(viewModel.supportedVersion)
}
append("\nInstalled version: ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(viewModel.installedVersion)
}
}
append("\nInstalled version: ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(viewModel.installedVersion)
}
})
}
val (drawable, description) = when (viewModel.installedVersion) {
"-" -> R.drawable.ic_download_24dp to R.string.install
viewModel.supportedVersion -> R.drawable.ic_reinstall_24dp to R.string.reinstall
else -> R.drawable.ic_update_24dp to R.string.update
)
}
Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
val (icon, description) = when (viewModel.installedVersion) {
"-" -> Icons.Default.Download to R.string.install
viewModel.supportedVersion -> Icons.Default.Update to R.string.reinstall
else -> Icons.Default.Update to R.string.update
}
Row(
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
Button(
modifier = Modifier.weight(1f, true),
onClick = {
if (Prefs.devMode.get()) {
if (viewModel.preferences.devMode) {
showOptionsDialog = true
} else {
navigator.push(AppDestination.Install)
onClickInstall()
}
}
) {
Icon(
modifier = Modifier.padding(8.dp),
painter = painterResource(drawable),
imageVector = icon,
contentDescription = stringResource(description),
tint = MaterialTheme.colorScheme.onPrimary
)
@ -132,9 +126,7 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
}
if (viewModel.installedVersion != "-") {
Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp)
) {
CompositionLocalProvider(
@ -142,14 +134,12 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
) {
FilledTonalButton(
modifier = Modifier.weight(1f),
onClick = {
viewModel.uninstallAliucord()
}
onClick = viewModel::uninstallAliucord
) {
Icon(
modifier = Modifier.padding(8.dp),
imageVector = Icons.Default.Delete,
contentDescription = "Uninstall"
contentDescription = stringResource(R.string.uninstall)
)
}
@ -159,8 +149,8 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
) {
Icon(
modifier = Modifier.padding(8.dp),
painter = painterResource(R.drawable.ic_launch_24dp),
contentDescription = "Launch"
imageVector = Icons.Default.Launch,
contentDescription = stringResource(R.string.launch)
)
}
}
@ -169,89 +159,60 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
}
}
}
val pager = remember {
Pager(
PagingConfig(
pageSize = 30,
enablePlaceholders = true,
maxSize = 200
)
) {
object : PagingSource<Int, Commit>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Commit> {
val pageNumber = params.key ?: 0
val response = Github.getCommits("page" to pageNumber.toString())
val prevKey = if (pageNumber > 0) pageNumber - 1 else null
val nextKey = if (response.isNotEmpty()) pageNumber + 1 else null
return LoadResult.Page(
data = response,
prevKey = prevKey,
nextKey = nextKey
)
}
override fun getRefreshKey(state: PagingState<Int, Commit>) = state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
}
}
ElevatedCard(
Modifier
.fillMaxWidth()
.weight(1f)
modifier = Modifier.padding(bottom = 16.dp)
) {
Column(
modifier = Modifier.padding(16.dp, 16.dp, 16.dp),
modifier = Modifier
.padding(16.dp)
.weight(1f, true)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
Row(
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = stringResource(R.string.commits),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)
val lazyPagingItems = viewModel.commits.collectAsLazyPagingItems()
Text(
text = stringResource(R.string.commits),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary
)
Spacer(Modifier.weight(1f, true))
}
val lazyPagingItems = pager.flow.collectAsLazyPagingItems()
LazyColumn(
modifier = Modifier
.fillMaxWidth(),
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
if (lazyPagingItems.loadState.refresh == LoadState.Loading) item {
Text(
text = stringResource(R.string.paging_initial_load),
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
)
if (lazyPagingItems.loadState.refresh == LoadState.Loading) {
item {
Text(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally),
text = stringResource(R.string.paging_initial_load)
)
}
}
items(lazyPagingItems) { commitData ->
if (commitData == null) return@items
val localUriHandler = LocalUriHandler.current
val uriHandler = LocalUriHandler.current
ListItem(
modifier = Modifier.clickable { localUriHandler.openUri(commitData.htmlUrl) },
modifier = Modifier.clickable { uriHandler.openUri(commitData.htmlUrl) },
overlineText = { Text(commitData.sha.substring(0, 7)) },
headlineText = { Text("${commitData.commit.message.split("\n").first()} - ${commitData.author.name}") }
)
}
if (lazyPagingItems.loadState.append == LoadState.Loading) item {
CircularProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
)
if (lazyPagingItems.loadState.append == LoadState.Loading) {
item {
CircularProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally)
)
}
}
}
}

View file

@ -5,8 +5,6 @@
package com.aliucord.manager.ui.screen
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
@ -16,25 +14,21 @@ import androidx.compose.material.icons.filled.NavigateBefore
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.viewmodel.compose.viewModel
import com.aliucord.manager.R
import com.aliucord.manager.ui.viewmodel.InstallViewModel
import org.koin.androidx.compose.getViewModel
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable
fun InstallerScreen(
viewModel: InstallViewModel = getViewModel(),
onClickBack: () -> Unit
) {
val viewModel: InstallViewModel = viewModel()
val navigateMain by viewModel.returnToHome.collectAsState(initial = false)
if (navigateMain) onClickBack
LaunchedEffect(Unit) {
viewModel.startInstallation()
}
if (navigateMain) onClickBack()
Scaffold(
topBar = {
@ -44,7 +38,7 @@ fun InstallerScreen(
IconButton(onClick = onClickBack) {
Icon(
imageVector = Icons.Default.NavigateBefore,
contentDescription = "Back"
contentDescription = stringResource(R.string.back)
)
}
}
@ -62,6 +56,7 @@ fun InstallerScreen(
modifier = Modifier.fillParentMaxWidth()
)
}
item {
Text(
text = viewModel.log,

View file

@ -14,50 +14,100 @@ import androidx.compose.foundation.text.KeyboardOptions
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.*
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.Composable
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 androidx.compose.ui.window.Dialog
import com.aliucord.manager.R
import com.aliucord.manager.model.Plugin
import com.aliucord.manager.ui.component.plugins.Changelog
import com.aliucord.manager.ui.component.plugins.PluginCard
import com.aliucord.manager.ui.navigation.AppDestination
import com.xinto.taxi.BackstackNavigator
import com.aliucord.manager.ui.viewmodel.PluginsViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun PluginsScreen(navigator: BackstackNavigator<AppDestination>) {
fun PluginsScreen(
viewModel: PluginsViewModel = getViewModel()
) {
if (viewModel.showUninstallDialog) {
val plugin = viewModel.selectedPlugin!!
AlertDialog(
onDismissRequest = viewModel::hideUninstallDialog,
icon = {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.uninstall)
)
},
title = {
Text("${stringResource(R.string.uninstall)} ${plugin.manifest.name}")
},
text = {
Text(stringResource(R.string.delete_plugin_body))
},
confirmButton = {
Button(
onClick = { viewModel.uninstallPlugin(plugin) }
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
Button(
onClick = viewModel::hideUninstallDialog,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Text(stringResource(android.R.string.cancel))
}
}
)
}
if (viewModel.showChangelogDialog) {
Changelog(
plugin = viewModel.selectedPlugin!!,
onDismiss = viewModel::hideChangelogDialog
)
}
Column(
modifier = Modifier.fillMaxSize(),
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
var plugins by remember { mutableStateOf(Plugin.loadAll()) }
val focusManager = LocalFocusManager.current
var search by remember { mutableStateOf("") }
val search = viewModel.search
if (viewModel.plugins.isNotEmpty()) {
val focusManager = LocalFocusManager.current
if (plugins.isNotEmpty()) {
OutlinedTextField(
maxLines = 1,
value = search,
onValueChange = { search = it },
singleLine = true,
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
value = viewModel.search,
onValueChange = viewModel::search,
singleLine = true,
shape = RoundedCornerShape(12.dp),
label = { Text(stringResource(R.string.search)) },
trailingIcon = {
if (search.isNotBlank()) {
IconButton(onClick = { search = "" }) {
if (viewModel.search.isNotBlank()) {
IconButton(onClick = viewModel::clearSearch) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.clear)
)
}
} else {
Icon(Icons.Default.Search, stringResource(R.string.search_plugins))
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(R.string.search_plugins)
)
}
},
keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Search),
@ -67,59 +117,19 @@ fun PluginsScreen(navigator: BackstackNavigator<AppDestination>) {
LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp)
) {
items(plugins.filter { plugin ->
plugin.manifest.run {
name.contains(search, true)
|| description.contains(search, true)
|| authors.any { a -> a.name.contains(search, true) }
}
}) { plugin ->
var showDeleteDialog by remember { mutableStateOf(false) }
var showChangelogDialog by remember { mutableStateOf(false) }
if (showDeleteDialog) AlertDialog(
onDismissRequest = { showDeleteDialog = false },
icon = {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.uninstall_plugin, plugin.manifest.name)
)
},
title = { Text(stringResource(R.string.uninstall_plugin, plugin.manifest.name)) },
text = { Text(stringResource(R.string.delete_plugin_body)) },
confirmButton = {
Button(
onClick = {
showDeleteDialog = false
plugin.file.delete()
plugins = plugins.filter { it != plugin }
}
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
Button(
onClick = { showDeleteDialog = false },
colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
) {
Text(stringResource(android.R.string.cancel))
}
items(
viewModel.plugins.filter { plugin ->
plugin.manifest.run {
name.contains(search, true)
|| description.contains(search, true)
|| authors.any { (name) -> name.contains(search, true) }
}
)
if (showChangelogDialog) Dialog(
onDismissRequest = { showChangelogDialog = false },
content = { Changelog(plugin, onDismiss = { showChangelogDialog = false }) }
)
}
) { plugin ->
PluginCard(
plugin = plugin,
onDelete = { showDeleteDialog = true },
onShowChangelog = { showChangelogDialog = true }
onClickDelete = { viewModel.showUninstallDialog(plugin) },
onClickShowChangelog = { viewModel.showChangelogDialog(plugin) }
)
}
}

View file

@ -5,28 +5,30 @@
package com.aliucord.manager.ui.screen
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.shrinkVertically
import androidx.compose.animation.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.rememberScrollState
import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NavigateBefore
import androidx.compose.material.icons.filled.Style
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aliucord.manager.R
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.ui.component.settings.*
import com.aliucord.manager.ui.theme.Theme
import com.aliucord.manager.ui.viewmodel.SettingsViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsScreen(onClickBack: () -> Unit) {
fun SettingsScreen(
viewModel: SettingsViewModel = getViewModel(),
onClickBack: () -> Unit
) {
Scaffold(
topBar = {
MediumTopAppBar(
@ -49,23 +51,25 @@ fun SettingsScreen(onClickBack: () -> Unit) {
.verticalScroll(state = rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp)
) {
val viewModifier: SettingsViewModel = viewModel()
val preferences = viewModel.preferences
var showThemeDialog by remember { mutableStateOf(false) }
if (showThemeDialog) {
if (viewModel.showThemeDialog) {
ThemeDialog(
onDismissRequest = { showThemeDialog = false },
onConfirm = { Prefs.theme.set(it.ordinal) }
currentTheme = preferences.theme,
onDismissRequest = viewModel::hideThemeDialog,
onConfirm = viewModel::setTheme
)
}
ThemeSetting(onClick = { showThemeDialog = true })
PreferenceItem(
title = { Text(stringResource(R.string.use_black)) },
description = { Text(stringResource(R.string.use_black_description)) },
preference = Prefs.useBlack
ListItem(
modifier = Modifier.clickable(onClick = viewModel::showThemeDialog),
headlineText = { Text(stringResource(R.string.theme)) },
supportingText = { Text(stringResource(R.string.theme_setting_description)) },
trailingContent = {
FilledTonalButton(onClick = viewModel::showThemeDialog) {
Text(preferences.theme.displayName)
}
}
)
Divider()
@ -74,27 +78,28 @@ fun SettingsScreen(onClickBack: () -> Unit) {
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp),
value = Prefs.appName.get(),
onValueChange = Prefs.appName::set,
value = preferences.appName,
onValueChange = viewModel::setAppName,
label = { Text(stringResource(R.string.app_name_setting)) },
placeholder = { Text("Aliucord") },
singleLine = true
)
PreferenceItem(
SwitchSetting(
checked = preferences.replaceBg,
title = { Text(stringResource(R.string.replace_bg)) },
preference = Prefs.replaceBg
onCheckedChange = { preferences.replaceBg = it }
)
val devModeOn = Prefs.devMode.get()
PreferenceItem(
title = { Text(stringResource(R.string.developer_mode)) },
preference = Prefs.devMode
SwitchSetting(
checked = preferences.devMode,
title = { Text(stringResource(R.string.advanced_options)) },
onCheckedChange = { preferences.devMode = it }
)
AnimatedVisibility(
visible = devModeOn,
visible = preferences.devMode,
enter = expandVertically(),
exit = shrinkVertically()
) {
Column(
@ -103,18 +108,29 @@ fun SettingsScreen(onClickBack: () -> Unit) {
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 8.dp),
value = Prefs.packageName.get(),
onValueChange = Prefs.packageName::set,
.padding(horizontal = 10.dp),
value = preferences.packageName,
onValueChange = viewModel::setPackageName,
label = { Text(stringResource(R.string.package_name)) },
placeholder = { Text("com.aliucord") },
singleLine = true
)
PreferenceItem(
OutlinedTextField(
modifier = Modifier
.fillMaxWidth()
.padding(horizontal = 10.dp),
value = preferences.version,
onValueChange = viewModel::setVersion,
label = { Text(stringResource(R.string.version)) },
singleLine = true
)
SwitchSetting(
checked = preferences.debuggable,
title = { Text(stringResource(R.string.debuggable)) },
description = { Text(stringResource(R.string.debuggable_description)) },
preference = Prefs.debuggable
onCheckedChange = { preferences.debuggable = it }
)
}
}
@ -123,10 +139,88 @@ fun SettingsScreen(onClickBack: () -> Unit) {
modifier = Modifier
.fillMaxWidth()
.padding(8.dp),
onClick = viewModifier::clearCacheDir
onClick = viewModel::clearCacheDir
) {
Text(stringResource(R.string.clear_files_cache), textAlign = TextAlign.Center)
Text(
text = stringResource(R.string.clear_files_cache),
textAlign = TextAlign.Center
)
}
}
}
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SwitchSetting(
checked: Boolean,
icon: @Composable (() -> Unit)? = null,
title: @Composable () -> Unit,
description: @Composable (() -> Unit)? = null,
onCheckedChange: (Boolean) -> Unit,
) {
ListItem(
modifier = Modifier.clickable { onCheckedChange(!checked) },
leadingContent = icon,
headlineText = title,
supportingText = description,
trailingContent = {
Switch(
checked = checked,
onCheckedChange = onCheckedChange
)
}
)
}
@Composable
fun ThemeDialog(
currentTheme: Theme,
onDismissRequest: () -> Unit,
onConfirm: (Theme) -> Unit
) {
var selectedTheme by remember { mutableStateOf(currentTheme) }
AlertDialog(
onDismissRequest = onDismissRequest,
icon = {
Icon(
imageVector = Icons.Default.Style,
contentDescription = stringResource(R.string.theme)
)
},
title = { Text(stringResource(R.string.theme)) },
text = {
Column {
Theme.values().forEach { theme ->
Row(
modifier = Modifier.clickable { selectedTheme = theme },
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = theme.displayName,
style = MaterialTheme.typography.labelLarge
)
Spacer(Modifier.weight(1f, true))
RadioButton(
selected = theme == selectedTheme,
onClick = { selectedTheme = theme }
)
}
}
}
},
confirmButton = {
Button(
onClick = {
onConfirm(selectedTheme)
onDismissRequest()
}
) {
Text(stringResource(R.string.apply))
}
}
)
}

View file

@ -1,89 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screen
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NavigateBefore
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
import com.aliucord.manager.util.httpClient
import io.ktor.client.call.*
import io.ktor.client.request.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.serialization.Serializable
@Serializable
private data class Tree(
val tree: List<File>
) {
@Serializable
data class File(
val mode: String,
val sha: String
)
}
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun StoreScreen(onClickBack: () -> Unit) {
val plugins = remember { mutableStateListOf<Tree.File>() }
// TODO: Display cards with option to install plugin
Scaffold(
topBar = {
MediumTopAppBar(
title = { Text(stringResource(R.string.store)) },
navigationIcon = {
IconButton(onClick = onClickBack) {
Icon(
imageVector = Icons.Default.NavigateBefore,
contentDescription = stringResource(R.string.back)
)
}
}
)
}
) { paddingValues ->
Column(
modifier = Modifier
.padding(paddingValues)
) {
if (plugins.isEmpty()) {
Box(
modifier = Modifier
.fillMaxSize()
.padding(12.dp),
contentAlignment = Alignment.Center,
content = { CircularProgressIndicator() }
)
LaunchedEffect(Unit) {
launch(Dispatchers.IO) {
val content = httpClient.get("https://api.github.com/repos/DiamondMiner88/AllACPlugins/git/trees/master") {
header("accept", "application/json")
}.body<Tree>()
plugins.addAll(content.tree.filter { it.mode == "160000" })
}
}
} else {
LazyColumn {
items(plugins) { item ->
Text("SHA: ${item.sha}", style = MaterialTheme.typography.labelSmall)
}
}
}
}
}
}

View file

@ -1,21 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.theme
import androidx.compose.ui.graphics.Color
val darkBackground = Color(0xff1a1a1a)
val primaryColor = Color(0xff00c853)
val primaryColorLight = Color(0xff5efc82)
val primaryColorDark = Color(0xff009624)
val discordBrand = Color(0xff5865F2)
val discordGreen = Color(0xff57F287)
val discordRed = Color(0xffED4245)
val errorColor = discordRed

View file

@ -10,27 +10,11 @@ import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.systemuicontroller.rememberSystemUiController
private val LightColorScheme = lightColorScheme()
private val DarkColorScheme = darkColorScheme()
private val BlackColorScheme = darkColorScheme(
background = Color.Black,
surface = Color.Black,
primary = Color.LightGray,
onPrimary = Color.DarkGray,
secondary = Color.Gray,
onSecondary = Color.LightGray,
secondaryContainer = Color.DarkGray,
onSecondaryContainer = Color.White,
outline = Color.LightGray
)
@Composable
fun ManagerTheme(
isBlack: Boolean = false,
isDarkTheme: Boolean = isSystemInDarkTheme(),
isDynamicColor: Boolean = true,
content: @Composable () -> Unit
@ -39,8 +23,8 @@ fun ManagerTheme(
val colorScheme = when {
dynamicColor && isDarkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !isDarkTheme -> dynamicLightColorScheme(LocalContext.current)
isDarkTheme -> if (isBlack) BlackColorScheme else DarkColorScheme
else -> LightColorScheme
isDarkTheme -> darkColorScheme()
else -> lightColorScheme()
}
val systemUiController = rememberSystemUiController()
@ -54,19 +38,12 @@ fun ManagerTheme(
MaterialTheme(
colorScheme = colorScheme,
typography = Typography,
content = content
)
}
enum class Theme(
val displayName: String
) {
enum class Theme(val displayName: String) {
SYSTEM("System"),
LIGHT("Light"),
DARK("Dark");
companion object {
fun from(index: Int) = values()[index]
}
DARK("Dark")
}

View file

@ -1,11 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.theme
import androidx.compose.material3.Typography
val Typography = Typography()

View file

@ -3,29 +3,18 @@ package com.aliucord.manager.ui.viewmodel
import androidx.compose.runtime.mutableStateListOf
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aliucord.manager.model.github.GithubUser
import com.aliucord.manager.util.Github
import com.aliucord.manager.util.httpClient
import io.ktor.client.call.*
import io.ktor.client.request.*
import com.aliucord.manager.network.dto.GithubUser
import com.aliucord.manager.domain.repository.GithubRepository
import kotlinx.coroutines.launch
class AboutViewModel : ViewModel() {
class AboutViewModel(
private val githubRepository: GithubRepository
) : ViewModel() {
val contributors = mutableStateListOf<GithubUser>()
fun load() {
init {
viewModelScope.launch {
val githubContributors = httpClient.get(Github.contributorsUrl).body<List<GithubUser>>()
.sortedByDescending {
it.contributions
}
contributors.addAll(githubContributors)
contributors.addAll(githubRepository.getContributors())
}
}
init {
load()
}
}

View file

@ -5,18 +5,21 @@ import android.content.Intent
import android.net.Uri
import android.widget.Toast
import androidx.compose.runtime.*
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aliucord.manager.model.github.Version
import com.aliucord.manager.preferences.Prefs
import com.aliucord.manager.util.*
import io.ktor.client.request.*
import io.ktor.client.statement.*
import kotlinx.coroutines.*
import kotlinx.serialization.decodeFromString
import androidx.paging.*
import com.aliucord.manager.domain.manager.PreferencesManager
import com.aliucord.manager.domain.repository.GithubRepository
import com.aliucord.manager.network.dto.Commit
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
class HomeViewModel(application: Application) : AndroidViewModel(application) {
private val packageManager = getApplication<Application>().packageManager
class HomeViewModel(
private val application: Application,
private val githubRepository: GithubRepository,
val preferences: PreferencesManager
) : ViewModel() {
private val packageManager = application.packageManager
var supportedVersion by mutableStateOf("")
private set
@ -24,43 +27,64 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
var installedVersion by mutableStateOf("-")
private set
private fun load() {
viewModelScope.launch(Dispatchers.IO) {
val version = json.decodeFromString<Version>(httpClient.get(Github.dataUrl).bodyAsText())
val commits = Pager(PagingConfig(pageSize = 30)) {
object : PagingSource<Int, Commit>() {
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Commit> {
val page = params.key ?: 0
withContext(Dispatchers.Main) {
supportedVersion = "${version.versionName} - " + when (version.versionCode[3]) {
'0' -> "Stable"
'1' -> "Beta"
'2' -> "Alpha"
else -> throw NoWhenBranchMatchedException()
}
installedVersion = try {
packageManager.getPackageInfo(Prefs.packageName.get(), 0).versionName
} catch (th: Throwable) {
"-"
}
val response = githubRepository.getCommits(page)
val prevKey = if (page > 0) page - 1 else null
val nextKey = if (response.isNotEmpty()) page + 1 else null
return LoadResult.Page(
data = response,
prevKey = prevKey,
nextKey = nextKey
)
}
override fun getRefreshKey(state: PagingState<Int, Commit>) = state.anchorPosition?.let {
state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
}.flow.cachedIn(viewModelScope)
init {
viewModelScope.launch(Dispatchers.IO) {
val version = githubRepository.getVersion()
supportedVersion = "${version.versionName} - " + when (version.versionCode[3]) {
'0' -> "Stable"
'1' -> "Beta"
'2' -> "Alpha"
else -> throw NoWhenBranchMatchedException()
}
installedVersion = try {
@Suppress("DEPRECATION")
packageManager.getPackageInfo(preferences.packageName, 0).versionName
} catch (th: Throwable) {
"-"
}
}
}
fun launchAliucord() {
packageManager.getLaunchIntentForPackage(Prefs.packageName.get())?.let {
getApplication<Application>().startActivity(it)
} ?: Toast.makeText(getApplication(), "Failed to launch Aliucord", Toast.LENGTH_LONG)
.show()
val launchIntent = packageManager.getLaunchIntentForPackage(preferences.packageName)
if (launchIntent != null) {
application.startActivity(launchIntent)
} else {
Toast.makeText(application, "Failed to launch Aliucord", Toast.LENGTH_LONG).show()
}
}
fun uninstallAliucord() {
val packageURI = Uri.parse("package:${Prefs.packageName.get()}")
val packageURI = Uri.parse("package:${preferences.packageName}")
val uninstallIntent = Intent(Intent.ACTION_DELETE, packageURI).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
getApplication<Application>().startActivity(uninstallIntent)
}
init {
load()
application.startActivity(uninstallIntent)
}
}

View file

@ -8,21 +8,29 @@ import android.content.pm.PackageInstaller
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.runtime.*
import androidx.lifecycle.AndroidViewModel
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aliucord.manager.domain.manager.DownloadManager
import com.aliucord.manager.domain.manager.PreferencesManager
import com.aliucord.manager.installer.service.InstallService
import com.aliucord.manager.installer.util.DownloadUtils
import com.aliucord.manager.installer.util.ManifestPatcher
import com.aliucord.manager.util.Signer
import com.aliucord.manager.installer.util.Signer
import com.android.zipflinger.*
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import java.io.File
import java.util.zip.Deflater
class InstallViewModel(application: Application) : AndroidViewModel(application) {
class InstallViewModel(
private val application: Application,
private val downloadManager: DownloadManager,
private val preferences: PreferencesManager
) : ViewModel() {
private val assetManager = application.assets
private val externalCacheDir = application.externalCacheDir!!
private val packageInstaller = application.packageManager.packageInstaller
private val _returnToHome = MutableSharedFlow<Boolean>()
val returnToHome = _returnToHome.asSharedFlow()
@ -31,39 +39,39 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
var log by mutableStateOf("")
private set
init {
viewModelScope.launch {
startInstallation()
}
}
@RequiresApi(Build.VERSION_CODES.O)
fun startInstallation() {
suspend fun startInstallation() {
if (installationRunning) return
viewModelScope.launch(Dispatchers.IO) {
withContext(Dispatchers.IO) {
installationRunning = true
val context = getApplication<Application>().applicationContext
val assetManager = context.assets
val externalCacheDir = context.externalCacheDir!!
val arch = Build.SUPPORTED_ABIS.first()
val supportedVersion = "124206"
val supportedVersion = preferences.version
val baseApk = externalCacheDir.resolve("base-${supportedVersion}.apk").also { file ->
val archiveInfo = context.packageManager.getPackageArchiveInfo(file.path, 0)
@Suppress("DEPRECATION")
val archiveInfo = application.packageManager.getPackageArchiveInfo(file.path, 0)
if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also run { log += "Using cached base APK\n"}
if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also run { log += "Using cached base APK\n" }
log += "Downloading Discord APK... "
DownloadUtils.downloadDiscordApk(context, supportedVersion)
downloadManager.downloadDiscordApk(supportedVersion)
log += "Done\n"
}
val libArch = arch.replace("-v", "_v")
val libsApk = externalCacheDir.resolve("config.$libArch-${supportedVersion}.apk").also { file ->
if (file.exists()) return@also run { log += "Used cached libs APK\n" }
if (file.exists()) return@also run { log += "Using cached libs APK\n" }
log += "Downloading libs APK... "
DownloadUtils.downloadSplit(context, supportedVersion, "config.$libArch")
downloadManager.downloadSplit(supportedVersion, "config.$libArch")
log += "Done\n"
}
@ -71,23 +79,19 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
if (file.exists()) return@also run { log += "Using cached locale APK\n" }
log += "Downloading locale APK... "
DownloadUtils.downloadSplit(context, supportedVersion, "config.en")
downloadManager.downloadSplit(supportedVersion, "config.en")
log += "Done\n"
}
val xxhdpiApk = externalCacheDir.resolve("config.xxhdpi-${supportedVersion}.apk").also { file ->
if (file.exists()) return@also run { log += "Using cached drawables APK\n"}
if (file.exists()) return@also run { log += "Using cached drawables APK\n" }
log += "Downloading drawables APK... "
DownloadUtils.downloadSplit(context, supportedVersion, "config.xxhdpi")
downloadManager.downloadSplit(supportedVersion, "config.xxhdpi")
log += "Done\n"
}
val apks = listOf(baseApk, libsApk, localeApk, xxhdpiApk)
val apks = arrayOf(baseApk, libsApk, localeApk, xxhdpiApk)
log += "Patching manifest...\n"
@ -97,7 +101,14 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
.array()
ZipArchive(baseApk.toPath()).use { zip ->
val source = BytesSource(ManifestPatcher.patchManifest(byteArray), "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION)
val patchedBytesArray = ManifestPatcher.patchManifest(
manifestBytes = byteArray,
packageName = preferences.packageName,
appName = preferences.appName,
debuggable = preferences.debuggable,
)
val source = BytesSource(patchedBytesArray, "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION)
zip.delete("AndroidManifest.xml")
zip.add(source)
@ -109,15 +120,18 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
.array()
ZipArchive(apk.toPath()).use { zip ->
val source = BytesSource(ManifestPatcher.renamePackage(byteArray), "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION)
val patchedBytes = ManifestPatcher.renamePackage(byteArray, preferences.packageName)
val source = BytesSource(patchedBytes, "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION)
zip.delete("AndroidManifest.xml")
zip.add(source)
}
}
} catch (e: Exception) {
log += "An error has occurred"
log += "An error has occurred: ${e.message}\n"
e.printStackTrace()
return@withContext
}
log += "Patching libraries...\n"
@ -141,7 +155,7 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
log += "Done\n"
installApks(*apks.toTypedArray())
installApks(*apks)
_returnToHome.emit(true)
installationRunning = false
@ -149,9 +163,7 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
}
private fun installApks(vararg apks: File) {
val context = getApplication<Application>().applicationContext
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val packageInstaller = context.packageManager.packageInstaller
val sessionId = packageInstaller.createSession(params)
val session = packageInstaller.openSession(sessionId)
@ -163,13 +175,13 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
}
}
val callbackIntent = Intent(context, InstallService::class.java)
val callbackIntent = Intent(application, InstallService::class.java)
@SuppressLint("UnspecifiedImmutableFlag")
val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
PendingIntent.getService(context, 0, callbackIntent, PendingIntent.FLAG_MUTABLE)
PendingIntent.getService(application, 0, callbackIntent, PendingIntent.FLAG_MUTABLE)
} else {
PendingIntent.getService(context, 0, callbackIntent, 0)
PendingIntent.getService(application, 0, callbackIntent, 0)
}
session.commit(contentIntent.intentSender)

View file

@ -0,0 +1,85 @@
package com.aliucord.manager.ui.viewmodel
import android.util.Log
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.aliucordDir
import com.aliucord.manager.domain.model.Plugin
import kotlinx.coroutines.*
class PluginsViewModel : ViewModel() {
var showChangelogDialog by mutableStateOf(false)
private set
var showUninstallDialog by mutableStateOf(false)
private set
var search by mutableStateOf("")
private set
var selectedPlugin by mutableStateOf<Plugin?>(null)
private set
val plugins = mutableStateListOf<Plugin>()
init {
viewModelScope.launch {
loadPlugins()
}
}
fun search(search: String) {
this.search = search
}
fun clearSearch() {
search = ""
}
fun showChangelogDialog(plugin: Plugin) {
showChangelogDialog = true
selectedPlugin = plugin
}
fun hideChangelogDialog() {
showChangelogDialog = false
}
fun showUninstallDialog(plugin: Plugin) {
showUninstallDialog = true
selectedPlugin = plugin
}
fun hideUninstallDialog() {
showUninstallDialog = false
}
fun uninstallPlugin(plugin: Plugin) {
plugin.file.delete()
plugins.remove(plugin)
}
private suspend fun loadPlugins(): Unit = withContext(Dispatchers.IO) {
val pluginsDir = aliucordDir.resolve("plugins")
if (!pluginsDir.exists() && !pluginsDir.mkdirs()) {
Log.e(BuildConfig.TAG, "Failed to create plugins dir. Missing Permissions?")
return@withContext
}
val files = pluginsDir.listFiles { file -> file.extension == "zip" } ?: return@withContext
plugins.addAll(
files.mapNotNull { file ->
try {
Plugin(file)
} catch (e: Exception) {
Log.e(BuildConfig.TAG, "Failed to load plugin ${file.nameWithoutExtension}", e)
null
}
}.sortedBy { it.manifest.name }
)
}
}

View file

@ -1,12 +1,43 @@
package com.aliucord.manager.ui.viewmodel
import android.app.Application
import androidx.lifecycle.AndroidViewModel
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import com.aliucord.manager.domain.manager.PreferencesManager
import com.aliucord.manager.ui.theme.Theme
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
class SettingsViewModel(
private val application: Application,
val preferences: PreferencesManager
) : ViewModel() {
var showThemeDialog by mutableStateOf(false)
private set
fun clearCacheDir() {
getApplication<Application>().cacheDir.deleteRecursively()
fun showThemeDialog() {
showThemeDialog = true
}
fun hideThemeDialog() {
showThemeDialog = false
}
fun setPackageName(packageName: String) {
if (packageName.isNotBlank()) preferences.packageName = packageName
}
fun setAppName(appName: String) {
if (appName.isNotBlank()) preferences.appName = appName
}
fun setTheme(theme: Theme) {
preferences.theme = theme
}
fun setVersion(version: String) {
if (version.isNotBlank()) preferences.version = version
}
fun clearCacheDir() {
application.cacheDir.deleteRecursively()
}
}

View file

@ -1,24 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.util
import android.os.Environment
import io.ktor.client.*
import io.ktor.client.plugins.contentnegotiation.*
import io.ktor.serialization.kotlinx.json.*
import kotlinx.serialization.json.Json
val json = Json {
prettyPrint = true
ignoreUnknownKeys = true
}
val httpClient = HttpClient {
install(ContentNegotiation) {
json(json)
}
}
val aliucordDir = Environment.getExternalStorageDirectory().resolve("Aliucord")

View file

@ -1,49 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.util
import com.aliucord.manager.model.github.Commit
import com.aliucord.manager.model.github.Version
import io.ktor.client.call.*
import io.ktor.client.request.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.decodeFromStream
import java.net.URL
import kotlin.collections.set
object Github {
private const val org = "Aliucord"
private const val repo = "Aliucord"
private const val commitsUrl = "https://api.github.com/repos/$org/$repo/commits"
const val contributorsUrl = "https://api.github.com/repos/$org/$repo/contributors"
const val dataUrl = "https://raw.githubusercontent.com/$org/$repo/builds/data.json"
fun checkForUpdates() {
// TODO
}
@OptIn(ExperimentalSerializationApi::class)
val version: Version by lazy {
json.decodeFromStream(URL(dataUrl).openStream())
}
private val commitsCache = HashMap<String, List<Commit>>()
suspend fun getCommits(vararg params: Pair<String, String>): List<Commit> {
val query = params.joinToString("&", "?") { it.first + "=" + it.second }
if (commitsCache.containsKey(query)) return commitsCache[query] ?: emptyList()
val res = httpClient.get(commitsUrl) {
params.forEach { (key, value) -> parameter(key, value) }
}.body<List<Commit>>()
commitsCache[query] = res
return res
}
}

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M12.0001,2C9.6237,1.9976 7.3241,2.8415 5.5135,4.3806C3.7028,5.9196 2.4994,8.0532 2.1189,10.3989C1.7384,12.7446 2.2057,15.1491 3.4371,17.1816C4.6684,19.2141 6.5833,20.7416 8.8386,21.4905C9.342,21.5826 9.52,21.2695 9.52,21.0055V19.3051C6.7269,19.9128 6.1376,17.9607 6.1376,17.9607C5.9523,17.3557 5.5586,16.8359 5.0265,16.4936C4.1241,15.8797 5.1001,15.8797 5.1001,15.8797C5.4164,15.9244 5.7185,16.0404 5.9833,16.2191C6.2481,16.3978 6.4688,16.6344 6.6287,16.911C6.7645,17.1573 6.9476,17.3743 7.1675,17.5497C7.3874,17.725 7.6398,17.8552 7.9102,17.9328C8.1805,18.0103 8.4635,18.0337 8.7429,18.0017C9.0223,17.9696 9.2927,17.8827 9.5384,17.7459C9.577,17.2409 9.7947,16.7662 10.1523,16.4076C7.9301,16.1559 5.5974,15.2965 5.5974,11.4966C5.5821,10.504 5.9492,9.5435 6.6225,8.814C6.3226,7.9509 6.3577,7.0067 6.7207,6.1682C6.7207,6.1682 7.5617,5.8981 9.4709,7.1934C11.1103,6.7433 12.8407,6.7433 14.4801,7.1934C16.3892,5.8981 17.2241,6.1682 17.2241,6.1682C17.5922,6.9978 17.6361,7.9353 17.3469,8.7956C18.0203,9.5251 18.3873,10.4856 18.3721,11.4782C18.3721,15.3211 16.0332,16.1621 13.8048,16.3892C14.0438,16.6294 14.2283,16.9181 14.3459,17.2359C14.4634,17.5536 14.5113,17.8929 14.4862,18.2308V20.9748C14.4862,21.3002 14.6643,21.5519 15.1738,21.4537C17.4034,20.685 19.2896,19.1531 20.4992,17.1286C21.7088,15.104 22.164,12.7171 21.7842,10.3895C21.4045,8.0619 20.2143,5.9434 18.424,4.4082C16.6336,2.8731 14.3583,2.0201 12.0001,2Z"
android:fillType="evenOdd" />
android:fillType="evenOdd"
android:pathData="M12.0001,2C9.6237,1.9976 7.3241,2.8415 5.5135,4.3806C3.7028,5.9196 2.4994,8.0532 2.1189,10.3989C1.7384,12.7446 2.2057,15.1491 3.4371,17.1816C4.6684,19.2141 6.5833,20.7416 8.8386,21.4905C9.342,21.5826 9.52,21.2695 9.52,21.0055V19.3051C6.7269,19.9128 6.1376,17.9607 6.1376,17.9607C5.9523,17.3557 5.5586,16.8359 5.0265,16.4936C4.1241,15.8797 5.1001,15.8797 5.1001,15.8797C5.4164,15.9244 5.7185,16.0404 5.9833,16.2191C6.2481,16.3978 6.4688,16.6344 6.6287,16.911C6.7645,17.1573 6.9476,17.3743 7.1675,17.5497C7.3874,17.725 7.6398,17.8552 7.9102,17.9328C8.1805,18.0103 8.4635,18.0337 8.7429,18.0017C9.0223,17.9696 9.2927,17.8827 9.5384,17.7459C9.577,17.2409 9.7947,16.7662 10.1523,16.4076C7.9301,16.1559 5.5974,15.2965 5.5974,11.4966C5.5821,10.504 5.9492,9.5435 6.6225,8.814C6.3226,7.9509 6.3577,7.0067 6.7207,6.1682C6.7207,6.1682 7.5617,5.8981 9.4709,7.1934C11.1103,6.7433 12.8407,6.7433 14.4801,7.1934C16.3892,5.8981 17.2241,6.1682 17.2241,6.1682C17.5922,6.9978 17.6361,7.9353 17.3469,8.7956C18.0203,9.5251 18.3873,10.4856 18.3721,11.4782C18.3721,15.3211 16.0332,16.1621 13.8048,16.3892C14.0438,16.6294 14.2283,16.9181 14.3459,17.2359C14.4634,17.5536 14.5113,17.8929 14.4862,18.2308V20.9748C14.4862,21.3002 14.6643,21.5519 15.1738,21.4537C17.4034,20.685 19.2896,19.1531 20.4992,17.1286C21.7088,15.104 22.164,12.7171 21.7842,10.3895C21.4045,8.0619 20.2143,5.9434 18.424,4.4082C16.6336,2.8731 14.3583,2.0201 12.0001,2Z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z" />
</vector>

View file

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

View file

@ -1,13 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<group>
<path
android:fillColor="#ffffff"
android:pathData="M 20.835 11.512 L 19.37 11.512 L 19.37 7.607 C 19.37 6.533 18.492 5.655 17.418 5.655 L 13.513 5.655 L 13.513 4.19 C 13.513 2.824 12.439 1.75 11.073 1.75 C 9.706 1.75 8.632 2.824 8.632 4.19 L 8.632 5.655 L 4.727 5.655 C 3.654 5.655 2.775 6.533 2.775 7.607 L 2.775 11.317 L 4.239 11.317 C 5.704 11.317 6.875 12.488 6.875 13.952 C 6.875 15.417 5.704 16.588 4.239 16.588 L 2.775 16.588 L 2.775 20.298 C 2.775 21.371 3.654 22.25 4.727 22.25 L 8.437 22.25 L 8.437 20.786 C 8.437 19.321 9.608 18.15 11.073 18.15 C 12.537 18.15 13.708 19.321 13.708 20.786 L 13.708 22.25 L 17.418 22.25 C 18.492 22.25 19.37 21.371 19.37 20.298 L 19.37 16.393 L 20.835 16.393 C 22.201 16.393 23.275 15.319 23.275 13.952 C 23.275 12.586 22.201 11.512 20.835 11.512 Z" />
<group></group>
</group>
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z" />
</vector>

View file

@ -1,9 +0,0 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:fillColor="#ffffff"
android:pathData="M11,8v5l4.25,2.52l0.77,-1.28l-3.52,-2.09V8H11zM21,10V3l-2.64,2.64C16.74,4.01 14.49,3 12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9s9,-4.03 9,-9h-2c0,3.86 -3.14,7 -7,7s-7,-3.14 -7,-7s3.14,-7 7,-7c1.93,0 3.68,0.79 4.95,2.05L14,10H21z" />
</vector>

View file

@ -6,16 +6,15 @@
<string name="open_settings">Open settings</string>
<string name="permission_required">You need to grant storage permission to use this app</string>
<string name="permission_grant">Grant permission</string>
<string name="replace_bg">Replace app icon with Aliucord logo</string>
<string name="replace_bg">Replace app icon with Aliucord variant</string>
<string name="settings">Settings</string>
<string name="show_menu">Show menu</string>
<string name="theme">Theme</string>
<string name="use_dex_from_storage">Use Injector.dex from storage</string>
<string name="plugins">Plugins</string>
<string name="uninstall_plugin">Uninstall %1$s</string>
<string name="search">Search</string>
<string name="view_plugin_changelog">View changelog for %1$s</string>
<string name="delete_plugin_body">Are you sure you want to uninstall this plugin? This action cannot be undone.</string>
<string name="delete_plugin_body">Are you sure you want to uninstall this plugin?</string>
<string name="close">Close</string>
<string name="support_server">Support Server</string>
<string name="back">Back</string>
@ -23,7 +22,7 @@
<string name="install">Install</string>
<string name="reinstall">Reinstall</string>
<string name="update">Update</string>
<string name="developer_mode">Developer Mode</string>
<string name="advanced_options">Show advanced options</string>
<string name="debuggable">Debuggable</string>
<string name="debuggable_description">Enable debuggable flag</string>
<string name="about">About</string>
@ -32,11 +31,8 @@
<string name="search_plugins">Search Plugins</string>
<string name="github">GitHub</string>
<string name="installer">Installer</string>
<string name="use_black">Use black</string>
<string name="use_black_description">Use black colors for dark mode</string>
<string name="theme_setting_description">Manager appearance</string>
<string name="no_plugins_installed">No plugins installed!</string>
<string name="store">Store</string>
<string name="paging_initial_load">Loading…</string>
<string name="contributor">Contributor</string>
<string name="apply">Apply</string>
@ -49,4 +45,7 @@
<string name="app_name_setting">App Name</string>
<string name="package_name">Package Name</string>
<string name="clear">Clear</string>
<string name="launch">Launch</string>
<string name="uninstall">Uninstall</string>
<string name="version">Version</string>
</resources>

View file

@ -1,6 +1,6 @@
#Mon Jul 04 12:13:32 EDT 2022
#Sun Aug 14 21:28:22 EDT 2022
distributionBase=GRADLE_USER_HOME
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip
distributionPath=wrapper/dists
zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME