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

View file

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

View file

@ -3,10 +3,8 @@
* Licensed under the Open Software License version 3.0 * Licensed under the Open Software License version 3.0
*/ */
package com.aliucord.manager package com.aliucord.manager
import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.* import android.os.*
import android.provider.Settings import android.provider.Settings
@ -19,24 +17,26 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import com.aliucord.manager.preferences.Prefs import com.aliucord.manager.domain.manager.PreferencesManager
import com.aliucord.manager.preferences.sharedPreferences
import com.aliucord.manager.ui.navigation.AppDestination import com.aliucord.manager.ui.navigation.AppDestination
import com.aliucord.manager.ui.screen.* import com.aliucord.manager.ui.screen.*
import com.aliucord.manager.ui.theme.ManagerTheme import com.aliucord.manager.ui.theme.ManagerTheme
import com.aliucord.manager.ui.theme.Theme import com.aliucord.manager.ui.theme.Theme
import com.xinto.taxi.Taxi import com.xinto.taxi.Taxi
import com.xinto.taxi.rememberBackstackNavigator import com.xinto.taxi.rememberBackstackNavigator
import org.koin.android.ext.android.inject
val aliucordDir = Environment.getExternalStorageDirectory().resolve("Aliucord")
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
private val preferences: PreferencesManager by inject()
@OptIn(ExperimentalAnimationApi::class) @OptIn(ExperimentalAnimationApi::class)
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
installSplashScreen() installSplashScreen()
super.onCreate(savedInstanceState) super.onCreate(savedInstanceState)
sharedPreferences = getPreferences(Context.MODE_PRIVATE)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
.setData("package:com.aliucord.manager".toUri()) .setData("package:com.aliucord.manager".toUri())
@ -46,17 +46,12 @@ class MainActivity : ComponentActivity() {
setContent { setContent {
ManagerTheme( ManagerTheme(
isBlack = Prefs.useBlack.get(), isDarkTheme = preferences.theme == Theme.DARK || preferences.theme == Theme.SYSTEM && isSystemInDarkTheme(),
isDarkTheme = run {
val theme = Theme.from(Prefs.theme.get())
theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK
}
) { ) {
val navigator = rememberBackstackNavigator<AppDestination>(AppDestination.Home) val navigator = rememberBackstackNavigator<AppDestination>(AppDestination.Home)
BackHandler { BackHandler {
navigator.pop() if (!navigator.pop()) finish()
} }
Taxi( Taxi(
@ -66,11 +61,11 @@ class MainActivity : ComponentActivity() {
) { destination -> ) { destination ->
when (destination) { when (destination) {
is AppDestination.Home -> MainRootScreen( is AppDestination.Home -> MainRootScreen(
navigator = navigator onClickInstall = { navigator.push(AppDestination.Install) },
) onClickAbout = { navigator.push(AppDestination.About) },
is AppDestination.Plugins -> PluginsScreen( onClickSettings = { navigator.push(AppDestination.Settings) }
navigator = navigator
) )
is AppDestination.Plugins -> PluginsScreen()
is AppDestination.Install -> InstallerScreen( is AppDestination.Install -> InstallerScreen(
onClickBack = navigator::pop, onClickBack = navigator::pop,
) )
@ -80,9 +75,6 @@ class MainActivity : ComponentActivity() {
is AppDestination.About -> AboutScreen( is AppDestination.About -> AboutScreen(
onClickBack = navigator::pop 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 * 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.Serializable
import kotlinx.serialization.json.JsonObject
@Serializable @Serializable
data class Manifest( data class Manifest(
@ -15,8 +16,13 @@ data class Manifest(
val version: String, val version: String,
val updateUrl: String, val updateUrl: String,
val changelog: 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 @Serializable
data class Author(val name: String, val id: Long) 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.os.IBinder
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import com.aliucord.manager.BuildConfig
class InstallService : Service() { class InstallService : Service() {
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
when (val statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) { when (val statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
PackageInstaller.STATUS_PENDING_USER_ACTION -> { PackageInstaller.STATUS_PENDING_USER_ACTION -> {
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT) @Suppress("DEPRECATION") // No.
confirmationIntent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)!!
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
startActivity(confirmationIntent) startActivity(confirmationIntent)
} }
PackageInstaller.STATUS_SUCCESS -> Toast.makeText( PackageInstaller.STATUS_SUCCESS -> Toast.makeText(
this, /* context = */ this,
"Successfully installed Aliucord!", /* text = */ "Successfully installed Aliucord!",
Toast.LENGTH_LONG /* duration = */ Toast.LENGTH_LONG
).show() ).show()
PackageInstaller.STATUS_FAILURE_ABORTED -> Toast.makeText( PackageInstaller.STATUS_FAILURE_ABORTED -> Toast.makeText(
this, /* context = */ this,
"Aborted installation", /* text = */ "Aborted installation",
Toast.LENGTH_LONG /* duration = */ Toast.LENGTH_LONG
).show() ).show()
else -> { 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() 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 package com.aliucord.manager.installer.util
import com.aliucord.manager.preferences.Prefs
import pxb.android.axml.* import pxb.android.axml.*
object ManifestPatcher { object ManifestPatcher {
@ -15,8 +14,12 @@ object ManifestPatcher {
private const val DEBUGGABLE = "debuggable" private const val DEBUGGABLE = "debuggable"
private const val LABEL = "label" private const val LABEL = "label"
fun patchManifest(manifestBytes: ByteArray): ByteArray { fun patchManifest(
val packageName = Prefs.packageName.get() manifestBytes: ByteArray,
packageName: String,
appName: String,
debuggable: Boolean
): ByteArray {
val reader = AxmlReader(manifestBytes) val reader = AxmlReader(manifestBytes)
val writer = AxmlWriter() val writer = AxmlWriter()
@ -38,12 +41,12 @@ object ManifestPatcher {
"application" -> object : ReplaceAttrsVisitor( "application" -> object : ReplaceAttrsVisitor(
nv, nv,
mapOf( mapOf(
LABEL to Prefs.appName.get(), LABEL to appName,
DEBUGGABLE to Prefs.debuggable.get(), DEBUGGABLE to debuggable,
USES_CLEARTEXT_TRAFFIC to true USES_CLEARTEXT_TRAFFIC to true
) )
) { ) {
private var addDebuggable = Prefs.debuggable.get() private var addDebuggable = debuggable
private var addUseClearTextTraffic = true private var addUseClearTextTraffic = true
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) { 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) val visitor = super.child(ns, name)
return when (name) { return when (name) {
"activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to Prefs.appName.get())) "activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to appName))
"provider" -> object : NodeVisitor(visitor) { "provider" -> object : NodeVisitor(visitor) {
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) { override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
super.attr( super.attr(
ns, name, resourceId, type, /* ns = */ ns,
if (name == "authorities") (value as String).replace("com.discord", packageName) else value /* 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() return writer.toByteArray()
} }
fun renamePackage(manifestBytes: ByteArray): ByteArray { fun renamePackage(
val packageName = Prefs.packageName.get() manifestBytes: ByteArray,
packageName: String
): ByteArray {
val reader = AxmlReader(manifestBytes) val reader = AxmlReader(manifestBytes)
val writer = AxmlWriter() val writer = AxmlWriter()
reader.accept(object : AxmlVisitor(writer) { reader.accept(
override fun child(ns: String?, name: String?): ReplaceAttrsVisitor { object : AxmlVisitor(writer) {
return ReplaceAttrsVisitor(super.child(ns, name), mapOf("package" to packageName)) override fun child(ns: String?, name: String?): ReplaceAttrsVisitor {
return ReplaceAttrsVisitor(super.child(ns, name), mapOf("package" to packageName))
}
} }
}) )
return writer.toByteArray() return writer.toByteArray()
} }

View file

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

View file

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0 * 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 com.aliucord.libzip.Zip
import java.io.InputStream 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 * 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.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
@ -16,6 +16,8 @@ data class Commit(
val commit: Commit, val commit: Commit,
val author: Author val author: Author
) { ) {
@Serializable data class Author(@SerialName("login") val name: String) @Serializable
@Serializable data class Commit(val message: String) 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 * 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.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable

View file

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0 * 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 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 * 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.layout.*
import androidx.compose.foundation.lazy.grid.* import androidx.compose.foundation.lazy.grid.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.model.github.GithubUser import com.aliucord.manager.network.dto.GithubUser
@Composable @Composable
fun ContributorsCard(contributors: List<GithubUser>) { fun ContributorsCard(contributors: List<GithubUser>) {
ElevatedCard( ElevatedCard {
modifier = Modifier
.wrapContentHeight()
.fillMaxWidth()
) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(16.dp) .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 horizontalAlignment = Alignment.CenterHorizontally
) { ) {
Text( Text(
stringResource(R.string.permission_required), text = stringResource(R.string.permission_required),
style = MaterialTheme.typography.headlineMedium, style = MaterialTheme.typography.headlineMedium,
textAlign = TextAlign.Center textAlign = TextAlign.Center
) )
@ -42,7 +42,7 @@ fun GrantPermission(permissionState: PermissionState) {
Row { Row {
val context = LocalContext.current val context = LocalContext.current
Button(onClick = { permissionState.launchPermissionRequest() }) { Button(onClick = permissionState::launchPermissionRequest) {
Text(stringResource(R.string.permission_grant)) Text(stringResource(R.string.permission_grant))
} }
@ -52,15 +52,11 @@ fun GrantPermission(permissionState: PermissionState) {
onClick = { onClick = {
context.startActivity( context.startActivity(
Intent( Intent(
Settings.ACTION_APPLICATION_DETAILS_SETTINGS, /* action = */ Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
Uri.fromParts("package", BuildConfig.APPLICATION_ID, null) /* 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)) Text(stringResource(R.string.open_settings))
} }

View file

@ -3,15 +3,17 @@
* Licensed under the Open Software License version 3.0 * 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.clickable
import androidx.compose.foundation.layout.* 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.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import com.aliucord.manager.R import com.aliucord.manager.R
@ -22,34 +24,27 @@ fun InstallerDialog(
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
onConfirm: (DownloadMethod) -> Unit onConfirm: (DownloadMethod) -> Unit
) { ) {
var selectedMethod by remember { mutableStateOf(DownloadMethod.DOWNLOAD) } var selectedMethod by rememberSaveable { mutableStateOf(DownloadMethod.DOWNLOAD) }
AlertDialog( AlertDialog(
icon = { icon = {
Icon( Icon(
painter = painterResource(id = R.drawable.ic_download_24dp), imageVector = Icons.Default.Download,
contentDescription = stringResource(R.string.download_method) contentDescription = stringResource(R.string.download_method)
) )
}, },
title = { title = {
Row( Text(stringResource(R.string.download_method))
modifier = Modifier.fillMaxWidth(),
verticalAlignment = Alignment.CenterVertically
) {
Text(stringResource(R.string.download_method))
}
}, },
text = { text = {
Column { Column {
mapOf( mapOf(
DownloadMethod.DOWNLOAD to stringResource(R.string.download_apk), 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) -> ).forEach { (method, name) ->
Column { Column {
Row( Row(
modifier = Modifier.clickable { modifier = Modifier.clickable { selectedMethod = method },
selectedMethod = method
},
verticalAlignment = Alignment.CenterVertically verticalAlignment = Alignment.CenterVertically
) { ) {
Text( 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 package com.aliucord.manager.ui.component.plugins
import android.annotation.SuppressLint
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons 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.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.vector.rememberVectorPainter
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.text.style.TextDecoration
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage import coil.compose.AsyncImage
import coil.request.ImageRequest
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.model.Plugin import com.aliucord.manager.domain.model.Plugin
private val hyperLinkPattern = Regex("\\[(.+?)]\\((.+?\\))") private val hyperLinkPattern = Regex("\\[(.+?)]\\((.+?\\))")
@Suppress("RegExpRedundantEscape") // It is very much not redundant and causes a crash lol @Suppress("RegExpRedundantEscape") // It is very much not redundant and causes a crash lol
private val headerStylePattern = Regex("\\{(improved|added|fixed)( marginTop)?\\}") private val headerStylePattern = Regex("\\{(improved|added|fixed)( marginTop)?\\}")
@SuppressLint("ComposableNaming") // Can't use MaterialTheme without Composable, but this is not a component
@Composable @Composable
private fun AnnotatedString.Builder.hyperlink(content: String) { private fun AnnotatedString.Builder.hyperlink(content: String) {
var idx = 0 var idx = 0
@ -51,7 +45,7 @@ private fun AnnotatedString.Builder.hyperlink(content: String) {
append(content.substring(idx, start)) append(content.substring(idx, start))
withStyle( withStyle(
style = SpanStyle( SpanStyle(
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
textDecoration = TextDecoration.Underline textDecoration = TextDecoration.Underline
) )
@ -77,9 +71,8 @@ fun Changelog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
icon = { icon = {
Icon( Icon(
painter = painterResource(R.drawable.ic_history_white_24dp), imageVector = Icons.Default.History,
contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name), contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name)
tint = MaterialTheme.colorScheme.onSurface
) )
}, },
title = { Text(plugin.manifest.name) }, title = { Text(plugin.manifest.name) },
@ -91,17 +84,12 @@ fun Changelog(
.fillMaxWidth() .fillMaxWidth()
.heightIn(max = 90.dp) .heightIn(max = 90.dp)
.clip(RoundedCornerShape(14.dp)), .clip(RoundedCornerShape(14.dp)),
model = ImageRequest.Builder(LocalContext.current) model = mediaUrl,
.data(mediaUrl)
.build(),
placeholder = rememberVectorPainter(image = Icons.Default.Person),
contentDescription = stringResource(R.string.changelog_media) contentDescription = stringResource(R.string.changelog_media)
) )
} }
LazyColumn( LazyColumn {
modifier = Modifier.wrapContentHeight()
) {
items(plugin.manifest.changelog!!.lines()) { items(plugin.manifest.changelog!!.lines()) {
var line = it.trim() var line = it.trim()
@ -113,16 +101,17 @@ fun Changelog(
} while (line.startsWith("#")) } while (line.startsWith("#"))
Text( Text(
line.trimStart(), text = line.trimStart(),
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
) )
} }
'*' -> { '*' -> {
LinkText( LinkText(
buildAnnotatedString { modifier = Modifier.padding(bottom = 2.dp),
annotatedString = buildAnnotatedString {
withStyle( withStyle(
style = SpanStyle( SpanStyle(
color = MaterialTheme.colorScheme.primary, color = MaterialTheme.colorScheme.primary,
fontWeight = FontWeight.Bold fontWeight = FontWeight.Bold
) )
@ -131,26 +120,25 @@ fun Changelog(
} }
hyperlink(line.substring(1)) hyperlink(line.substring(1))
}, }
Modifier.padding(bottom = 2.dp)
) )
} }
else -> { else -> {
when { when {
line.endsWith("marginTop}") -> { line.endsWith("marginTop}") -> {
val color = MaterialTheme.colorScheme.onSurface val color = MaterialTheme.colorScheme.onSurface
Text( Text(
line, text = line,
fontWeight = FontWeight.Bold, fontWeight = FontWeight.Bold,
color = color, color = color,
modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) modifier = Modifier.padding(top = 16.dp, bottom = 6.dp)
) )
} }
line.all { c -> c == '=' } -> { line.all { c -> c == '=' } -> { } // Discord ignores =======
} // Discord ignores =======
else -> { else -> {
LinkText( LinkText(
buildAnnotatedString { annotatedString = buildAnnotatedString {
hyperlink(line) hyperlink(line)
} }
) )
@ -164,9 +152,7 @@ fun Changelog(
} }
}, },
confirmButton = { confirmButton = {
Button( Button(onClick = onDismiss) {
onClick = onDismiss
) {
Text(stringResource(R.string.close)) Text(stringResource(R.string.close))
} }
} }
@ -174,17 +160,20 @@ fun Changelog(
} }
@Composable @Composable
private fun LinkText(annotatedString: AnnotatedString, modifier: Modifier = Modifier) { private fun LinkText(
modifier: Modifier = Modifier,
annotatedString: AnnotatedString
) {
val urlHandler = LocalUriHandler.current val urlHandler = LocalUriHandler.current
ClickableText( ClickableText(
modifier = modifier,
text = annotatedString, text = annotatedString,
style = TextStyle(color = LocalContentColor.current), style = TextStyle(color = LocalContentColor.current),
onClick = { offset -> onClick = { offset ->
annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let { annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let {
urlHandler.openUri(it.item) urlHandler.openUri(it.item)
} }
}, }
modifier = modifier
) )
} }

View file

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

View file

@ -6,24 +6,30 @@
package com.aliucord.manager.ui.screen package com.aliucord.manager.ui.screen
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.NavigateBefore import androidx.compose.material.icons.filled.NavigateBefore
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import coil.compose.AsyncImage
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.ui.component.about.ContributorsCard import com.aliucord.manager.ui.component.ContributorsCard
import com.aliucord.manager.ui.component.about.TeamCard
import com.aliucord.manager.ui.viewmodel.AboutViewModel import com.aliucord.manager.ui.viewmodel.AboutViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun AboutScreen(onClickBack: () -> Unit) { fun AboutScreen(
viewModel: AboutViewModel = getViewModel(),
onClickBack: () -> Unit
) {
Scaffold( Scaffold(
topBar = { topBar = {
MediumTopAppBar( MediumTopAppBar(
@ -40,14 +46,35 @@ fun AboutScreen(onClickBack: () -> Unit) {
} }
) { paddingValues -> ) { paddingValues ->
Column( Column(
verticalArrangement = Arrangement.spacedBy(8.dp),
modifier = Modifier modifier = Modifier
.padding(paddingValues) .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 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) ContributorsCard(viewModel.contributors)
Spacer(Modifier.weight(1f, true)) Spacer(Modifier.weight(1f, true))
@ -58,11 +85,40 @@ fun AboutScreen(onClickBack: () -> Unit) {
) { ) {
Icon( Icon(
painter = painterResource(R.drawable.ic_account_github_white_24dp), 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)) 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 package com.aliucord.manager.ui.screen
import android.content.res.Configuration
import androidx.compose.animation.* 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.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.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource 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.BuildConfig
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.ui.navigation.AppDestination
import com.aliucord.manager.ui.navigation.HomeDestination 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) @OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
@Composable @Composable
fun MainRootScreen(navigator: BackstackNavigator<AppDestination>) { fun MainRootScreen(
onClickInstall: () -> Unit,
onClickAbout: () -> Unit,
onClickSettings: () -> Unit
) {
val mainRootNavigator = rememberNavigator(HomeDestination.HOME) val mainRootNavigator = rememberNavigator(HomeDestination.HOME)
val currentDestination = mainRootNavigator.currentDestination
val orientation = LocalConfiguration.current.orientation
var isMenuExpanded by remember { mutableStateOf(false) }
Scaffold( Scaffold(
modifier = Modifier
.fillMaxSize(),
topBar = { topBar = {
MediumTopAppBar( MediumTopAppBar(
title = { title = { Text(stringResource(mainRootNavigator.currentDestination.label)) },
Text(stringResource(mainRootNavigator.currentDestination.label))
},
actions = { actions = {
val localUriHandler = LocalUriHandler.current val localUriHandler = LocalUriHandler.current
@ -47,125 +40,64 @@ fun MainRootScreen(navigator: BackstackNavigator<AppDestination>) {
) { ) {
Icon( Icon(
painter = painterResource(R.drawable.ic_discord), painter = painterResource(R.drawable.ic_discord),
contentDescription = stringResource(R.string.support_server), contentDescription = stringResource(R.string.support_server)
tint = MaterialTheme.colorScheme.onBackground
) )
} }
Box( IconButton(onClick = onClickAbout) {
modifier = Modifier.wrapContentSize(Alignment.TopEnd) Icon(
) { imageVector = Icons.Default.Info,
IconButton( contentDescription = stringResource(R.string.about)
onClick = { isMenuExpanded = !isMenuExpanded } )
) { }
Icon(
imageVector = Icons.Default.MoreVert,
contentDescription = stringResource(R.string.show_menu),
tint = MaterialTheme.colorScheme.onBackground
)
}
DropdownMenu( IconButton(onClick = onClickSettings) {
offset = DpOffset(x = 12.dp, y = 0.dp), Icon(
expanded = isMenuExpanded, imageVector = Icons.Default.Settings,
onDismissRequest = { isMenuExpanded = false } contentDescription = stringResource(R.string.settings)
) { )
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)) }
)
}
} }
} }
) )
}, },
bottomBar = { bottomBar = {
if (orientation == Configuration.ORIENTATION_PORTRAIT) { val currentDestination = mainRootNavigator.currentDestination
NavigationBar {
HomeDestination.values().forEach { destination -> NavigationBar {
NavigationBarItem( HomeDestination.values().forEach { destination ->
selected = currentDestination == destination, NavigationBarItem(
icon = { selected = currentDestination == destination,
if (currentDestination == destination) Icon( icon = {
destination.selectedIcon, if (currentDestination == destination) {
stringResource(destination.label) Icon(
) else Icon(destination.unselectedIcon, stringResource(destination.label)) imageVector = destination.selectedIcon,
}, contentDescription = stringResource(destination.label)
label = { Text(stringResource(destination.label)) }, )
onClick = { mainRootNavigator.replace(destination) } } else {
) Icon(
} imageVector = destination.unselectedIcon,
contentDescription = stringResource(destination.label)
)
}
},
label = { Text(stringResource(destination.label)) },
onClick = { mainRootNavigator.replace(destination) }
)
} }
} }
} }
) { paddingValues -> ) { paddingValues ->
Row( Taxi(
modifier = Modifier modifier = Modifier.padding(paddingValues),
.padding(paddingValues) navigator = mainRootNavigator,
.padding(horizontal = 12.dp, vertical = 14.dp) transitionSpec = { fadeIn() with fadeOut() }
) { ) { destination ->
if (orientation == Configuration.ORIENTATION_LANDSCAPE) { when (destination) {
NavRail( HomeDestination.HOME -> HomeScreen(
currentDestination = currentDestination, onClickInstall = onClickInstall
onClickDestination = mainRootNavigator::replace
) )
} HomeDestination.PLUGINS -> PluginsScreen()
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)
}
} }
} }
}
}
@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.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Delete import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.* import androidx.compose.ui.text.*
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel import androidx.paging.LoadState
import androidx.paging.*
import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items import androidx.paging.compose.items
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.model.github.Commit import com.aliucord.manager.ui.component.DownloadMethod
import com.aliucord.manager.preferences.Prefs import com.aliucord.manager.ui.component.InstallerDialog
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.viewmodel.HomeViewModel import com.aliucord.manager.ui.viewmodel.HomeViewModel
import com.aliucord.manager.util.Github import org.koin.androidx.compose.getViewModel
import com.xinto.taxi.BackstackNavigator
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun HomeScreen(navigator: BackstackNavigator<AppDestination>) { fun HomeScreen(
val viewModel: HomeViewModel = viewModel() viewModel: HomeViewModel = getViewModel(),
onClickInstall: () -> Unit
) {
var showOptionsDialog by remember { mutableStateOf(false) } var showOptionsDialog by remember { mutableStateOf(false) }
if (showOptionsDialog) { if (showOptionsDialog) {
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
if (uri == null) return@rememberLauncherForActivityResult if (uri == null) return@rememberLauncherForActivityResult
navigator.push(AppDestination.Install) onClickInstall()
} }
InstallerDialog( InstallerDialog(
onDismissRequest = { showOptionsDialog = false }, onDismissRequest = { showOptionsDialog = false },
onConfirm = { method -> onConfirm = { method ->
if (method == DownloadMethod.DOWNLOAD) { when (method) {
navigator.push(AppDestination.Install) DownloadMethod.DOWNLOAD -> onClickInstall()
} else { DownloadMethod.SELECT -> filePicker.launch(arrayOf("application/octet-stream"))
filePicker.launch(arrayOf("application/octet-stream"))
} }
} }
) )
} }
Column( Column(
verticalArrangement = Arrangement.spacedBy(12.dp) modifier = Modifier.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
ElevatedCard( ElevatedCard {
modifier = Modifier.wrapContentHeight()
) {
Column( Column(
modifier = Modifier modifier = Modifier
.padding(16.dp) .padding(16.dp)
@ -77,52 +70,53 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
) { ) {
Column { Column {
Text( 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, style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary color = MaterialTheme.colorScheme.primary
) )
Text(buildAnnotatedString { Text(
append("Supported version: ") buildAnnotatedString {
append("Supported version: ")
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
append(viewModel.supportedVersion) 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( Column(
modifier = Modifier.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(12.dp) 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( Row(
horizontalArrangement = Arrangement.spacedBy(12.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
Button( Button(
modifier = Modifier.weight(1f, true), modifier = Modifier.weight(1f, true),
onClick = { onClick = {
if (Prefs.devMode.get()) { if (viewModel.preferences.devMode) {
showOptionsDialog = true showOptionsDialog = true
} else { } else {
navigator.push(AppDestination.Install) onClickInstall()
} }
} }
) { ) {
Icon( Icon(
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(8.dp),
painter = painterResource(drawable), imageVector = icon,
contentDescription = stringResource(description), contentDescription = stringResource(description),
tint = MaterialTheme.colorScheme.onPrimary tint = MaterialTheme.colorScheme.onPrimary
) )
@ -132,9 +126,7 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
} }
if (viewModel.installedVersion != "-") { if (viewModel.installedVersion != "-") {
Row( Row(
modifier = Modifier.fillMaxWidth(),
horizontalArrangement = Arrangement.spacedBy(12.dp) horizontalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
CompositionLocalProvider( CompositionLocalProvider(
@ -142,14 +134,12 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
) { ) {
FilledTonalButton( FilledTonalButton(
modifier = Modifier.weight(1f), modifier = Modifier.weight(1f),
onClick = { onClick = viewModel::uninstallAliucord
viewModel.uninstallAliucord()
}
) { ) {
Icon( Icon(
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(8.dp),
imageVector = Icons.Default.Delete, imageVector = Icons.Default.Delete,
contentDescription = "Uninstall" contentDescription = stringResource(R.string.uninstall)
) )
} }
@ -159,8 +149,8 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
) { ) {
Icon( Icon(
modifier = Modifier.padding(8.dp), modifier = Modifier.padding(8.dp),
painter = painterResource(R.drawable.ic_launch_24dp), imageVector = Icons.Default.Launch,
contentDescription = "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( ElevatedCard(
Modifier modifier = Modifier.padding(bottom = 16.dp)
.fillMaxWidth()
.weight(1f)
) { ) {
Column( Column(
modifier = Modifier.padding(16.dp, 16.dp, 16.dp), modifier = Modifier
.padding(16.dp)
.weight(1f, true)
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
Row( val lazyPagingItems = viewModel.commits.collectAsLazyPagingItems()
verticalAlignment = Alignment.CenterVertically
) { Text(
Text( text = stringResource(R.string.commits),
text = stringResource(R.string.commits), style = MaterialTheme.typography.titleLarge,
style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary
color = MaterialTheme.colorScheme.primary )
)
Spacer(Modifier.weight(1f, true))
}
val lazyPagingItems = pager.flow.collectAsLazyPagingItems()
LazyColumn( LazyColumn(
modifier = Modifier modifier = Modifier.fillMaxWidth(),
.fillMaxWidth(),
verticalArrangement = Arrangement.spacedBy(14.dp) verticalArrangement = Arrangement.spacedBy(14.dp)
) { ) {
if (lazyPagingItems.loadState.refresh == LoadState.Loading) item { if (lazyPagingItems.loadState.refresh == LoadState.Loading) {
Text( item {
text = stringResource(R.string.paging_initial_load), Text(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.wrapContentWidth(Alignment.CenterHorizontally) .wrapContentWidth(Alignment.CenterHorizontally),
) text = stringResource(R.string.paging_initial_load)
)
}
} }
items(lazyPagingItems) { commitData -> items(lazyPagingItems) { commitData ->
if (commitData == null) return@items if (commitData == null) return@items
val localUriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
ListItem( ListItem(
modifier = Modifier.clickable { localUriHandler.openUri(commitData.htmlUrl) }, modifier = Modifier.clickable { uriHandler.openUri(commitData.htmlUrl) },
overlineText = { Text(commitData.sha.substring(0, 7)) }, overlineText = { Text(commitData.sha.substring(0, 7)) },
headlineText = { Text("${commitData.commit.message.split("\n").first()} - ${commitData.author.name}") } headlineText = { Text("${commitData.commit.message.split("\n").first()} - ${commitData.author.name}") }
) )
} }
if (lazyPagingItems.loadState.append == LoadState.Loading) item { if (lazyPagingItems.loadState.append == LoadState.Loading) {
CircularProgressIndicator( item {
modifier = Modifier CircularProgressIndicator(
.fillMaxWidth() modifier = Modifier
.wrapContentWidth(Alignment.CenterHorizontally) .fillMaxWidth()
) .wrapContentWidth(Alignment.CenterHorizontally)
)
}
} }
} }
} }

View file

@ -5,8 +5,6 @@
package com.aliucord.manager.ui.screen package com.aliucord.manager.ui.screen
import android.os.Build
import androidx.annotation.RequiresApi
import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
@ -16,25 +14,21 @@ import androidx.compose.material.icons.filled.NavigateBefore
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp 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 com.aliucord.manager.ui.viewmodel.InstallViewModel
import org.koin.androidx.compose.getViewModel
@RequiresApi(Build.VERSION_CODES.O)
@OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class)
@Composable @Composable
fun InstallerScreen( fun InstallerScreen(
viewModel: InstallViewModel = getViewModel(),
onClickBack: () -> Unit onClickBack: () -> Unit
) { ) {
val viewModel: InstallViewModel = viewModel()
val navigateMain by viewModel.returnToHome.collectAsState(initial = false) val navigateMain by viewModel.returnToHome.collectAsState(initial = false)
if (navigateMain) onClickBack
LaunchedEffect(Unit) {
viewModel.startInstallation()
}
if (navigateMain) onClickBack()
Scaffold( Scaffold(
topBar = { topBar = {
@ -44,7 +38,7 @@ fun InstallerScreen(
IconButton(onClick = onClickBack) { IconButton(onClick = onClickBack) {
Icon( Icon(
imageVector = Icons.Default.NavigateBefore, imageVector = Icons.Default.NavigateBefore,
contentDescription = "Back" contentDescription = stringResource(R.string.back)
) )
} }
} }
@ -62,6 +56,7 @@ fun InstallerScreen(
modifier = Modifier.fillParentMaxWidth() modifier = Modifier.fillParentMaxWidth()
) )
} }
item { item {
Text( Text(
text = viewModel.log, 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.Icons
import androidx.compose.material.icons.filled.* import androidx.compose.material.icons.filled.*
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.Dialog
import com.aliucord.manager.R 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.Changelog
import com.aliucord.manager.ui.component.plugins.PluginCard import com.aliucord.manager.ui.component.plugins.PluginCard
import com.aliucord.manager.ui.navigation.AppDestination import com.aliucord.manager.ui.viewmodel.PluginsViewModel
import com.xinto.taxi.BackstackNavigator import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @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( Column(
modifier = Modifier.fillMaxSize(), modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
var plugins by remember { mutableStateOf(Plugin.loadAll()) } val search = viewModel.search
val focusManager = LocalFocusManager.current
var search by remember { mutableStateOf("") } if (viewModel.plugins.isNotEmpty()) {
val focusManager = LocalFocusManager.current
if (plugins.isNotEmpty()) {
OutlinedTextField( OutlinedTextField(
maxLines = 1,
value = search,
onValueChange = { search = it },
singleLine = true,
modifier = Modifier.fillMaxWidth(), modifier = Modifier.fillMaxWidth(),
maxLines = 1,
value = viewModel.search,
onValueChange = viewModel::search,
singleLine = true,
shape = RoundedCornerShape(12.dp), shape = RoundedCornerShape(12.dp),
label = { Text(stringResource(R.string.search)) }, label = { Text(stringResource(R.string.search)) },
trailingIcon = { trailingIcon = {
if (search.isNotBlank()) { if (viewModel.search.isNotBlank()) {
IconButton(onClick = { search = "" }) { IconButton(onClick = viewModel::clearSearch) {
Icon( Icon(
imageVector = Icons.Default.Close, imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.clear) contentDescription = stringResource(R.string.clear)
) )
} }
} else { } 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), keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Search),
@ -67,59 +117,19 @@ fun PluginsScreen(navigator: BackstackNavigator<AppDestination>) {
LazyColumn( LazyColumn(
verticalArrangement = Arrangement.spacedBy(8.dp) verticalArrangement = Arrangement.spacedBy(8.dp)
) { ) {
items(plugins.filter { plugin -> items(
plugin.manifest.run { viewModel.plugins.filter { plugin ->
name.contains(search, true) plugin.manifest.run {
|| description.contains(search, true) name.contains(search, true)
|| authors.any { a -> a.name.contains(search, true) } || description.contains(search, true)
} || authors.any { (name) -> 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))
}
} }
) }
) { plugin ->
if (showChangelogDialog) Dialog(
onDismissRequest = { showChangelogDialog = false },
content = { Changelog(plugin, onDismiss = { showChangelogDialog = false }) }
)
PluginCard( PluginCard(
plugin = plugin, plugin = plugin,
onDelete = { showDeleteDialog = true }, onClickDelete = { viewModel.showUninstallDialog(plugin) },
onShowChangelog = { showChangelogDialog = true } onClickShowChangelog = { viewModel.showChangelogDialog(plugin) }
) )
} }
} }

View file

@ -5,28 +5,30 @@
package com.aliucord.manager.ui.screen package com.aliucord.manager.ui.screen
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.*
import androidx.compose.animation.shrinkVertically import androidx.compose.foundation.*
import androidx.compose.foundation.layout.* 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.Icons
import androidx.compose.material.icons.filled.NavigateBefore import androidx.compose.material.icons.filled.NavigateBefore
import androidx.compose.material.icons.filled.Style
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.preferences.Prefs import com.aliucord.manager.ui.theme.Theme
import com.aliucord.manager.ui.component.settings.*
import com.aliucord.manager.ui.viewmodel.SettingsViewModel import com.aliucord.manager.ui.viewmodel.SettingsViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class) @OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsScreen(onClickBack: () -> Unit) { fun SettingsScreen(
viewModel: SettingsViewModel = getViewModel(),
onClickBack: () -> Unit
) {
Scaffold( Scaffold(
topBar = { topBar = {
MediumTopAppBar( MediumTopAppBar(
@ -49,23 +51,25 @@ fun SettingsScreen(onClickBack: () -> Unit) {
.verticalScroll(state = rememberScrollState()), .verticalScroll(state = rememberScrollState()),
verticalArrangement = Arrangement.spacedBy(12.dp) verticalArrangement = Arrangement.spacedBy(12.dp)
) { ) {
val viewModifier: SettingsViewModel = viewModel() val preferences = viewModel.preferences
var showThemeDialog by remember { mutableStateOf(false) } if (viewModel.showThemeDialog) {
if (showThemeDialog) {
ThemeDialog( ThemeDialog(
onDismissRequest = { showThemeDialog = false }, currentTheme = preferences.theme,
onConfirm = { Prefs.theme.set(it.ordinal) } onDismissRequest = viewModel::hideThemeDialog,
onConfirm = viewModel::setTheme
) )
} }
ThemeSetting(onClick = { showThemeDialog = true }) ListItem(
modifier = Modifier.clickable(onClick = viewModel::showThemeDialog),
PreferenceItem( headlineText = { Text(stringResource(R.string.theme)) },
title = { Text(stringResource(R.string.use_black)) }, supportingText = { Text(stringResource(R.string.theme_setting_description)) },
description = { Text(stringResource(R.string.use_black_description)) }, trailingContent = {
preference = Prefs.useBlack FilledTonalButton(onClick = viewModel::showThemeDialog) {
Text(preferences.theme.displayName)
}
}
) )
Divider() Divider()
@ -74,27 +78,28 @@ fun SettingsScreen(onClickBack: () -> Unit) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 10.dp), .padding(horizontal = 10.dp),
value = Prefs.appName.get(), value = preferences.appName,
onValueChange = Prefs.appName::set, onValueChange = viewModel::setAppName,
label = { Text(stringResource(R.string.app_name_setting)) }, label = { Text(stringResource(R.string.app_name_setting)) },
placeholder = { Text("Aliucord") }, placeholder = { Text("Aliucord") },
singleLine = true singleLine = true
) )
PreferenceItem( SwitchSetting(
checked = preferences.replaceBg,
title = { Text(stringResource(R.string.replace_bg)) }, title = { Text(stringResource(R.string.replace_bg)) },
preference = Prefs.replaceBg onCheckedChange = { preferences.replaceBg = it }
) )
val devModeOn = Prefs.devMode.get() SwitchSetting(
checked = preferences.devMode,
PreferenceItem( title = { Text(stringResource(R.string.advanced_options)) },
title = { Text(stringResource(R.string.developer_mode)) }, onCheckedChange = { preferences.devMode = it }
preference = Prefs.devMode
) )
AnimatedVisibility( AnimatedVisibility(
visible = devModeOn, visible = preferences.devMode,
enter = expandVertically(),
exit = shrinkVertically() exit = shrinkVertically()
) { ) {
Column( Column(
@ -103,18 +108,29 @@ fun SettingsScreen(onClickBack: () -> Unit) {
OutlinedTextField( OutlinedTextField(
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(horizontal = 8.dp), .padding(horizontal = 10.dp),
value = Prefs.packageName.get(), value = preferences.packageName,
onValueChange = Prefs.packageName::set, onValueChange = viewModel::setPackageName,
label = { Text(stringResource(R.string.package_name)) }, label = { Text(stringResource(R.string.package_name)) },
placeholder = { Text("com.aliucord") }, placeholder = { Text("com.aliucord") },
singleLine = true 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)) }, title = { Text(stringResource(R.string.debuggable)) },
description = { Text(stringResource(R.string.debuggable_description)) }, description = { Text(stringResource(R.string.debuggable_description)) },
preference = Prefs.debuggable onCheckedChange = { preferences.debuggable = it }
) )
} }
} }
@ -123,10 +139,88 @@ fun SettingsScreen(onClickBack: () -> Unit) {
modifier = Modifier modifier = Modifier
.fillMaxWidth() .fillMaxWidth()
.padding(8.dp), .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.material3.*
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.SideEffect import androidx.compose.runtime.SideEffect
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import com.google.accompanist.systemuicontroller.rememberSystemUiController import com.google.accompanist.systemuicontroller.rememberSystemUiController
private val 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 @Composable
fun ManagerTheme( fun ManagerTheme(
isBlack: Boolean = false,
isDarkTheme: Boolean = isSystemInDarkTheme(), isDarkTheme: Boolean = isSystemInDarkTheme(),
isDynamicColor: Boolean = true, isDynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit
@ -39,8 +23,8 @@ fun ManagerTheme(
val colorScheme = when { val colorScheme = when {
dynamicColor && isDarkTheme -> dynamicDarkColorScheme(LocalContext.current) dynamicColor && isDarkTheme -> dynamicDarkColorScheme(LocalContext.current)
dynamicColor && !isDarkTheme -> dynamicLightColorScheme(LocalContext.current) dynamicColor && !isDarkTheme -> dynamicLightColorScheme(LocalContext.current)
isDarkTheme -> if (isBlack) BlackColorScheme else DarkColorScheme isDarkTheme -> darkColorScheme()
else -> LightColorScheme else -> lightColorScheme()
} }
val systemUiController = rememberSystemUiController() val systemUiController = rememberSystemUiController()
@ -54,19 +38,12 @@ fun ManagerTheme(
MaterialTheme( MaterialTheme(
colorScheme = colorScheme, colorScheme = colorScheme,
typography = Typography,
content = content content = content
) )
} }
enum class Theme( enum class Theme(val displayName: String) {
val displayName: String
) {
SYSTEM("System"), SYSTEM("System"),
LIGHT("Light"), LIGHT("Light"),
DARK("Dark"); DARK("Dark")
companion object {
fun from(index: Int) = values()[index]
}
} }

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

View file

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

View file

@ -8,21 +8,29 @@ import android.content.pm.PackageInstaller
import android.os.Build import android.os.Build
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.lifecycle.AndroidViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.service.InstallService
import com.aliucord.manager.installer.util.DownloadUtils
import com.aliucord.manager.installer.util.ManifestPatcher 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 com.android.zipflinger.*
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.*
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.launch
import java.io.File import java.io.File
import java.util.zip.Deflater 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>() private val _returnToHome = MutableSharedFlow<Boolean>()
val returnToHome = _returnToHome.asSharedFlow() val returnToHome = _returnToHome.asSharedFlow()
@ -31,39 +39,39 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
var log by mutableStateOf("") var log by mutableStateOf("")
private set private set
init {
viewModelScope.launch {
startInstallation()
}
}
@RequiresApi(Build.VERSION_CODES.O) @RequiresApi(Build.VERSION_CODES.O)
fun startInstallation() { suspend fun startInstallation() {
if (installationRunning) return if (installationRunning) return
viewModelScope.launch(Dispatchers.IO) { withContext(Dispatchers.IO) {
installationRunning = true installationRunning = true
val context = getApplication<Application>().applicationContext
val assetManager = context.assets
val externalCacheDir = context.externalCacheDir!!
val arch = Build.SUPPORTED_ABIS.first() val arch = Build.SUPPORTED_ABIS.first()
val supportedVersion = "124206" val supportedVersion = preferences.version
val baseApk = externalCacheDir.resolve("base-${supportedVersion}.apk").also { file -> 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... " log += "Downloading Discord APK... "
downloadManager.downloadDiscordApk(supportedVersion)
DownloadUtils.downloadDiscordApk(context, supportedVersion)
log += "Done\n" log += "Done\n"
} }
val libArch = arch.replace("-v", "_v") val libArch = arch.replace("-v", "_v")
val libsApk = externalCacheDir.resolve("config.$libArch-${supportedVersion}.apk").also { file -> 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... " log += "Downloading libs APK... "
downloadManager.downloadSplit(supportedVersion, "config.$libArch")
DownloadUtils.downloadSplit(context, supportedVersion, "config.$libArch")
log += "Done\n" 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" } if (file.exists()) return@also run { log += "Using cached locale APK\n" }
log += "Downloading locale APK... " log += "Downloading locale APK... "
downloadManager.downloadSplit(supportedVersion, "config.en")
DownloadUtils.downloadSplit(context, supportedVersion, "config.en")
log += "Done\n" log += "Done\n"
} }
val xxhdpiApk = externalCacheDir.resolve("config.xxhdpi-${supportedVersion}.apk").also { file -> 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... " log += "Downloading drawables APK... "
downloadManager.downloadSplit(supportedVersion, "config.xxhdpi")
DownloadUtils.downloadSplit(context, supportedVersion, "config.xxhdpi")
log += "Done\n" log += "Done\n"
} }
val apks = listOf(baseApk, libsApk, localeApk, xxhdpiApk) val apks = arrayOf(baseApk, libsApk, localeApk, xxhdpiApk)
log += "Patching manifest...\n" log += "Patching manifest...\n"
@ -97,7 +101,14 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
.array() .array()
ZipArchive(baseApk.toPath()).use { zip -> 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.delete("AndroidManifest.xml")
zip.add(source) zip.add(source)
@ -109,15 +120,18 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
.array() .array()
ZipArchive(apk.toPath()).use { zip -> 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.delete("AndroidManifest.xml")
zip.add(source) zip.add(source)
} }
} }
} catch (e: Exception) { } catch (e: Exception) {
log += "An error has occurred" log += "An error has occurred: ${e.message}\n"
e.printStackTrace() e.printStackTrace()
return@withContext
} }
log += "Patching libraries...\n" log += "Patching libraries...\n"
@ -141,7 +155,7 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
log += "Done\n" log += "Done\n"
installApks(*apks.toTypedArray()) installApks(*apks)
_returnToHome.emit(true) _returnToHome.emit(true)
installationRunning = false installationRunning = false
@ -149,9 +163,7 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
} }
private fun installApks(vararg apks: File) { private fun installApks(vararg apks: File) {
val context = getApplication<Application>().applicationContext
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
val packageInstaller = context.packageManager.packageInstaller
val sessionId = packageInstaller.createSession(params) val sessionId = packageInstaller.createSession(params)
val session = packageInstaller.openSession(sessionId) 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") @SuppressLint("UnspecifiedImmutableFlag")
val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { 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 { } else {
PendingIntent.getService(context, 0, callbackIntent, 0) PendingIntent.getService(application, 0, callbackIntent, 0)
} }
session.commit(contentIntent.intentSender) 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 package com.aliucord.manager.ui.viewmodel
import android.app.Application 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() { fun showThemeDialog() {
getApplication<Application>().cacheDir.deleteRecursively() 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"?> <?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:height="24dp"
android:width="24dp" android:width="24dp"
android:height="24dp"
android:viewportWidth="24" android:viewportWidth="24"
android:viewportHeight="24"> android:viewportHeight="24">
<path <path
android:fillColor="#ffffff" 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> </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="open_settings">Open settings</string>
<string name="permission_required">You need to grant storage permission to use this app</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="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="settings">Settings</string>
<string name="show_menu">Show menu</string> <string name="show_menu">Show menu</string>
<string name="theme">Theme</string> <string name="theme">Theme</string>
<string name="use_dex_from_storage">Use Injector.dex from storage</string> <string name="use_dex_from_storage">Use Injector.dex from storage</string>
<string name="plugins">Plugins</string> <string name="plugins">Plugins</string>
<string name="uninstall_plugin">Uninstall %1$s</string>
<string name="search">Search</string> <string name="search">Search</string>
<string name="view_plugin_changelog">View changelog for %1$s</string> <string name="view_plugin_changelog">View changelog for %1$s</string>
<string name="delete_plugin_body">Are you sure you want to uninstall this plugin? This action cannot be undone.</string> <string name="delete_plugin_body">Are you sure you want to uninstall this plugin?</string>
<string name="close">Close</string> <string name="close">Close</string>
<string name="support_server">Support Server</string> <string name="support_server">Support Server</string>
<string name="back">Back</string> <string name="back">Back</string>
@ -23,7 +22,7 @@
<string name="install">Install</string> <string name="install">Install</string>
<string name="reinstall">Reinstall</string> <string name="reinstall">Reinstall</string>
<string name="update">Update</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">Debuggable</string>
<string name="debuggable_description">Enable debuggable flag</string> <string name="debuggable_description">Enable debuggable flag</string>
<string name="about">About</string> <string name="about">About</string>
@ -32,11 +31,8 @@
<string name="search_plugins">Search Plugins</string> <string name="search_plugins">Search Plugins</string>
<string name="github">GitHub</string> <string name="github">GitHub</string>
<string name="installer">Installer</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="theme_setting_description">Manager appearance</string>
<string name="no_plugins_installed">No plugins installed!</string> <string name="no_plugins_installed">No plugins installed!</string>
<string name="store">Store</string>
<string name="paging_initial_load">Loading…</string> <string name="paging_initial_load">Loading…</string>
<string name="contributor">Contributor</string> <string name="contributor">Contributor</string>
<string name="apply">Apply</string> <string name="apply">Apply</string>
@ -49,4 +45,7 @@
<string name="app_name_setting">App Name</string> <string name="app_name_setting">App Name</string>
<string name="package_name">Package Name</string> <string name="package_name">Package Name</string>
<string name="clear">Clear</string> <string name="clear">Clear</string>
<string name="launch">Launch</string>
<string name="uninstall">Uninstall</string>
<string name="version">Version</string>
</resources> </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 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 distributionPath=wrapper/dists
zipStorePath=wrapper/dists zipStorePath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME zipStoreBase=GRADLE_USER_HOME