diff --git a/app/build.gradle.kts b/app/build.gradle.kts index cbe3413..a8f37e1 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -1,7 +1,3 @@ -val accompanistVersion = "0.26.1-alpha" -val composeVersion = "1.3.0-alpha03" -val ktorVersion = "2.1.0" - plugins { id("com.android.application") id("kotlin-android") @@ -28,6 +24,7 @@ android { buildTypes { release { isMinifyEnabled = true + isShrinkResources = true proguardFiles(getDefaultProguardFile("proguard-android-optimize.txt"), "proguard-rules.pro") } } @@ -42,14 +39,6 @@ android { freeCompilerArgs = freeCompilerArgs + "-opt-in=kotlin.RequiresOptIn" } - applicationVariants.all { - kotlin.sourceSets { - getByName(name) { - kotlin.srcDir("build/generated/ksp/$name/kotlin") - } - } - } - buildFeatures.compose = true composeOptions.kotlinCompilerExtensionVersion = "1.3.0" } @@ -68,6 +57,8 @@ dependencies { implementation("androidx.activity:activity-compose:1.6.0-beta01") // compose dependencies + val composeVersion = "1.3.0-alpha03" + implementation("androidx.compose.ui:ui:${composeVersion}") implementation("androidx.compose.ui:ui-tooling:${composeVersion}") implementation("androidx.compose.material:material-icons-extended:${composeVersion}") @@ -76,9 +67,15 @@ dependencies { implementation("androidx.lifecycle:lifecycle-viewmodel-compose:2.6.0-alpha01") // accompanist dependencies + val accompanistVersion = "0.26.1-alpha" implementation("com.google.accompanist:accompanist-systemuicontroller:${accompanistVersion}") implementation("com.google.accompanist:accompanist-permissions:${accompanistVersion}") + // Koin + val koinVersion = "3.2.0" + implementation("io.insert-koin:koin-android:$koinVersion") + implementation("io.insert-koin:koin-androidx-compose:$koinVersion") + // other dependencies implementation("io.coil-kt:coil-compose:2.1.0") implementation("org.bouncycastle:bcpkix-jdk15on:1.70") @@ -90,8 +87,9 @@ dependencies { implementation("com.github.X1nto:Taxi:1.1.0") // ktor + val ktorVersion = "2.1.0" implementation("io.ktor:ktor-client-core:$ktorVersion") - implementation("io.ktor:ktor-client-android:$ktorVersion") + implementation("io.ktor:ktor-client-cio:$ktorVersion") implementation("io.ktor:ktor-client-content-negotiation:$ktorVersion") implementation("io.ktor:ktor-serialization-kotlinx-json:$ktorVersion") } diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 8e24159..f713b06 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -21,12 +21,8 @@ android:name="android.permission.QUERY_ALL_PACKAGES" tools:ignore="QueryAllPackagesPermission" /> - - - - - = Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) { val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION) .setData("package:com.aliucord.manager".toUri()) @@ -46,17 +46,12 @@ class MainActivity : ComponentActivity() { setContent { ManagerTheme( - isBlack = Prefs.useBlack.get(), - isDarkTheme = run { - val theme = Theme.from(Prefs.theme.get()) - - theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK - } + isDarkTheme = preferences.theme == Theme.DARK || preferences.theme == Theme.SYSTEM && isSystemInDarkTheme(), ) { val navigator = rememberBackstackNavigator(AppDestination.Home) BackHandler { - navigator.pop() + if (!navigator.pop()) finish() } Taxi( @@ -66,11 +61,11 @@ class MainActivity : ComponentActivity() { ) { destination -> when (destination) { is AppDestination.Home -> MainRootScreen( - navigator = navigator - ) - is AppDestination.Plugins -> PluginsScreen( - navigator = navigator + onClickInstall = { navigator.push(AppDestination.Install) }, + onClickAbout = { navigator.push(AppDestination.About) }, + onClickSettings = { navigator.push(AppDestination.Settings) } ) + is AppDestination.Plugins -> PluginsScreen() is AppDestination.Install -> InstallerScreen( onClickBack = navigator::pop, ) @@ -80,9 +75,6 @@ class MainActivity : ComponentActivity() { is AppDestination.About -> AboutScreen( onClickBack = navigator::pop ) - is AppDestination.Store -> StoreScreen( - onClickBack = navigator::pop - ) } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt b/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt new file mode 100644 index 0000000..169283d --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ManagerApplication.kt @@ -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) + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/di/AppModule.kt b/app/src/main/kotlin/com/aliucord/manager/di/AppModule.kt new file mode 100644 index 0000000..338f4da --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/di/AppModule.kt @@ -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) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/di/ManagerModule.kt b/app/src/main/kotlin/com/aliucord/manager/di/ManagerModule.kt new file mode 100644 index 0000000..1a76776 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/di/ManagerModule.kt @@ -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) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/di/RepositoryModule.kt b/app/src/main/kotlin/com/aliucord/manager/di/RepositoryModule.kt new file mode 100644 index 0000000..43d7b88 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/di/RepositoryModule.kt @@ -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) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/di/ServiceModule.kt b/app/src/main/kotlin/com/aliucord/manager/di/ServiceModule.kt new file mode 100644 index 0000000..9d78e6b --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/di/ServiceModule.kt @@ -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) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt b/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt new file mode 100644 index 0000000..7c79b3b --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/di/ViewModelModule.kt @@ -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) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt b/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt new file mode 100644 index 0000000..402e5c5 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/domain/manager/DownloadManager.kt @@ -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()!! + + 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/" + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/manager/PreferencesManager.kt b/app/src/main/kotlin/com/aliucord/manager/domain/manager/PreferencesManager.kt new file mode 100644 index 0000000..1f3c492 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/domain/manager/PreferencesManager.kt @@ -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") +} diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/manager/base/BasePreferencesManager.kt b/app/src/main/kotlin/com/aliucord/manager/domain/manager/base/BasePreferencesManager.kt new file mode 100644 index 0000000..84ad1a9 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/domain/manager/base/BasePreferencesManager.kt @@ -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 > getEnum(key: String, defaultValue: E) = + enumValueOf(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 > putEnum(key: String, value: E) = putString(key, value.name) + + protected class Preference( + 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 > enumPreference( + key: String, + defaultValue: E + ) = Preference( + key = key, + defaultValue = defaultValue, + getter = ::getEnum, + setter = ::putEnum + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/model/Manifest.kt b/app/src/main/kotlin/com/aliucord/manager/domain/model/Manifest.kt similarity index 59% rename from app/src/main/kotlin/com/aliucord/manager/model/Manifest.kt rename to app/src/main/kotlin/com/aliucord/manager/domain/model/Manifest.kt index e17e44a..eaa305c 100644 --- a/app/src/main/kotlin/com/aliucord/manager/model/Manifest.kt +++ b/app/src/main/kotlin/com/aliucord/manager/domain/model/Manifest.kt @@ -3,9 +3,10 @@ * Licensed under the Open Software License version 3.0 */ -package com.aliucord.manager.model +package com.aliucord.manager.domain.model import kotlinx.serialization.Serializable +import kotlinx.serialization.json.JsonObject @Serializable data class Manifest( @@ -15,8 +16,13 @@ data class Manifest( val version: String, val updateUrl: String, val changelog: String?, - val changelogMedia: String? + val changelogMedia: String?, + val links: JsonObject ) { + val repositoryUrl = updateUrl + .replace("raw.githubusercontent.com", "github.com") + .replaceFirst("/builds.*".toRegex(), "") + @Serializable data class Author(val name: String, val id: Long) } diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/model/Plugin.kt b/app/src/main/kotlin/com/aliucord/manager/domain/model/Plugin.kt new file mode 100644 index 0000000..5c797bb --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/domain/model/Plugin.kt @@ -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) } + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt b/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt new file mode 100644 index 0000000..adda499 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/domain/repository/GithubRepository.kt @@ -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(service.getVersion()) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/service/InstallService.kt b/app/src/main/kotlin/com/aliucord/manager/installer/service/InstallService.kt index e232f02..6a78fda 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/service/InstallService.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/service/InstallService.kt @@ -6,28 +6,30 @@ import android.content.pm.PackageInstaller import android.os.IBinder import android.util.Log import android.widget.Toast +import com.aliucord.manager.BuildConfig class InstallService : Service() { override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int { when (val statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) { PackageInstaller.STATUS_PENDING_USER_ACTION -> { - val confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT) - confirmationIntent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + @Suppress("DEPRECATION") // No. + val confirmationIntent = intent.getParcelableExtra(Intent.EXTRA_INTENT)!! + .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) startActivity(confirmationIntent) } PackageInstaller.STATUS_SUCCESS -> Toast.makeText( - this, - "Successfully installed Aliucord!", - Toast.LENGTH_LONG + /* context = */ this, + /* text = */ "Successfully installed Aliucord!", + /* duration = */ Toast.LENGTH_LONG ).show() PackageInstaller.STATUS_FAILURE_ABORTED -> Toast.makeText( - this, - "Aborted installation", - Toast.LENGTH_LONG + /* context = */ this, + /* text = */ "Aborted installation", + /* duration = */ Toast.LENGTH_LONG ).show() else -> { - Log.i("Installer", "Installation failed: error code $statusCode") + Log.i(BuildConfig.TAG, "Installation failed: error code $statusCode") Toast.makeText(this, "Failed: Error $statusCode", Toast.LENGTH_LONG).show() } } diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/util/DownloadUtils.kt b/app/src/main/kotlin/com/aliucord/manager/installer/util/DownloadUtils.kt deleted file mode 100644 index 6855bd4..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/installer/util/DownloadUtils.kt +++ /dev/null @@ -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) - } - } - } -} diff --git a/app/src/main/kotlin/com/aliucord/manager/installer/util/ManifestPatcher.kt b/app/src/main/kotlin/com/aliucord/manager/installer/util/ManifestPatcher.kt index 518b11d..d15d60f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/installer/util/ManifestPatcher.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/util/ManifestPatcher.kt @@ -5,7 +5,6 @@ package com.aliucord.manager.installer.util -import com.aliucord.manager.preferences.Prefs import pxb.android.axml.* object ManifestPatcher { @@ -15,8 +14,12 @@ object ManifestPatcher { private const val DEBUGGABLE = "debuggable" private const val LABEL = "label" - fun patchManifest(manifestBytes: ByteArray): ByteArray { - val packageName = Prefs.packageName.get() + fun patchManifest( + manifestBytes: ByteArray, + packageName: String, + appName: String, + debuggable: Boolean + ): ByteArray { val reader = AxmlReader(manifestBytes) val writer = AxmlWriter() @@ -38,12 +41,12 @@ object ManifestPatcher { "application" -> object : ReplaceAttrsVisitor( nv, mapOf( - LABEL to Prefs.appName.get(), - DEBUGGABLE to Prefs.debuggable.get(), + LABEL to appName, + DEBUGGABLE to debuggable, USES_CLEARTEXT_TRAFFIC to true ) ) { - private var addDebuggable = Prefs.debuggable.get() + private var addDebuggable = debuggable private var addUseClearTextTraffic = true override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) { @@ -56,12 +59,15 @@ object ManifestPatcher { val visitor = super.child(ns, name) return when (name) { - "activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to Prefs.appName.get())) + "activity" -> ReplaceAttrsVisitor(visitor, mapOf("label" to appName)) "provider" -> object : NodeVisitor(visitor) { override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) { super.attr( - ns, name, resourceId, type, - if (name == "authorities") (value as String).replace("com.discord", packageName) else value + /* ns = */ ns, + /* name = */ name, + /* resourceId = */ resourceId, + /* type = */ type, + /* obj = */ if (name == "authorities") (value as String).replace("com.discord", packageName) else value ) } } @@ -91,16 +97,20 @@ object ManifestPatcher { return writer.toByteArray() } - fun renamePackage(manifestBytes: ByteArray): ByteArray { - val packageName = Prefs.packageName.get() + fun renamePackage( + manifestBytes: ByteArray, + packageName: String + ): ByteArray { val reader = AxmlReader(manifestBytes) val writer = AxmlWriter() - reader.accept(object : AxmlVisitor(writer) { - override fun child(ns: String?, name: String?): ReplaceAttrsVisitor { - return ReplaceAttrsVisitor(super.child(ns, name), mapOf("package" to packageName)) + reader.accept( + object : AxmlVisitor(writer) { + override fun child(ns: String?, name: String?): ReplaceAttrsVisitor { + return ReplaceAttrsVisitor(super.child(ns, name), mapOf("package" to packageName)) + } } - }) + ) return writer.toByteArray() } diff --git a/app/src/main/kotlin/com/aliucord/manager/util/Signer.kt b/app/src/main/kotlin/com/aliucord/manager/installer/util/Signer.kt similarity index 78% rename from app/src/main/kotlin/com/aliucord/manager/util/Signer.kt rename to app/src/main/kotlin/com/aliucord/manager/installer/util/Signer.kt index c8526b1..2d9e32f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/util/Signer.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/util/Signer.kt @@ -3,8 +3,9 @@ * Licensed under the Open Software License version 3.0 */ -package com.aliucord.manager.util +package com.aliucord.manager.installer.util +import com.aliucord.manager.aliucordDir import com.android.apksig.ApkSigner import org.bouncycastle.asn1.x500.X500Name import org.bouncycastle.asn1.x509.SubjectPublicKeyInfo @@ -34,9 +35,9 @@ object Signer { val certificate = keyStore.getCertificate(alias) as X509Certificate ApkSigner.SignerConfig.Builder( - "Aliucord Manager signer", - keyStore.getKey(alias, password) as PrivateKey, - listOf(certificate), + /* name = */ "Aliucord Manager signer", + /* privateKey = */ keyStore.getKey(alias, password) as PrivateKey, + /* certificates = */ listOf(certificate), ).build() } @@ -62,13 +63,13 @@ object Signer { generateKeyPair() } val builder = X509v3CertificateBuilder( - x500Name, - serialNumber, - Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L), - Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L), - Locale.ENGLISH, - x500Name, - SubjectPublicKeyInfo.getInstance(pair.public.encoded) + /* issuer = */ x500Name, + /* serial = */ serialNumber, + /* notBefore = */ Date(System.currentTimeMillis() - 1000L * 60L * 60L * 24L * 30L), + /* notAfter = */ Date(System.currentTimeMillis() + 1000L * 60L * 60L * 24L * 366L * 30L), + /* dateLocale = */ Locale.ENGLISH, + /* subject = */ x500Name, + /* publicKeyInfo = */ SubjectPublicKeyInfo.getInstance(pair.public.encoded) ) val signer = JcaContentSignerBuilder("SHA1withRSA").build(pair.private) diff --git a/app/src/main/kotlin/com/aliucord/manager/util/ZipUtils.kt b/app/src/main/kotlin/com/aliucord/manager/installer/util/ZipUtils.kt similarity index 90% rename from app/src/main/kotlin/com/aliucord/manager/util/ZipUtils.kt rename to app/src/main/kotlin/com/aliucord/manager/installer/util/ZipUtils.kt index 0a7f4f2..961f5b8 100644 --- a/app/src/main/kotlin/com/aliucord/manager/util/ZipUtils.kt +++ b/app/src/main/kotlin/com/aliucord/manager/installer/util/ZipUtils.kt @@ -3,7 +3,7 @@ * Licensed under the Open Software License version 3.0 */ -package com.aliucord.manager.util +package com.aliucord.manager.installer.util import com.aliucord.libzip.Zip import java.io.InputStream diff --git a/app/src/main/kotlin/com/aliucord/manager/model/Plugin.kt b/app/src/main/kotlin/com/aliucord/manager/model/Plugin.kt deleted file mode 100644 index ce2ef84..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/model/Plugin.kt +++ /dev/null @@ -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 { - 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 } - } - } -} diff --git a/app/src/main/kotlin/com/aliucord/manager/model/github/Commit.kt b/app/src/main/kotlin/com/aliucord/manager/network/dto/Commit.kt similarity index 65% rename from app/src/main/kotlin/com/aliucord/manager/model/github/Commit.kt rename to app/src/main/kotlin/com/aliucord/manager/network/dto/Commit.kt index 01212fc..8521794 100644 --- a/app/src/main/kotlin/com/aliucord/manager/model/github/Commit.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/dto/Commit.kt @@ -3,7 +3,7 @@ * Licensed under the Open Software License version 3.0 */ -package com.aliucord.manager.model.github +package com.aliucord.manager.network.dto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable @@ -16,6 +16,8 @@ data class Commit( val commit: Commit, val author: Author ) { - @Serializable data class Author(@SerialName("login") val name: String) - @Serializable data class Commit(val message: String) + @Serializable + data class Author(@SerialName("login") val name: String) + @Serializable + data class Commit(val message: String) } diff --git a/app/src/main/kotlin/com/aliucord/manager/model/github/GithubUser.kt b/app/src/main/kotlin/com/aliucord/manager/network/dto/GithubUser.kt similarity index 87% rename from app/src/main/kotlin/com/aliucord/manager/model/github/GithubUser.kt rename to app/src/main/kotlin/com/aliucord/manager/network/dto/GithubUser.kt index 62b52ad..331544f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/model/github/GithubUser.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/dto/GithubUser.kt @@ -3,7 +3,7 @@ * Licensed under the Open Software License version 3.0 */ -package com.aliucord.manager.model.github +package com.aliucord.manager.network.dto import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable diff --git a/app/src/main/kotlin/com/aliucord/manager/model/github/Version.kt b/app/src/main/kotlin/com/aliucord/manager/network/dto/Version.kt similarity index 86% rename from app/src/main/kotlin/com/aliucord/manager/model/github/Version.kt rename to app/src/main/kotlin/com/aliucord/manager/network/dto/Version.kt index 2c0e69e..fdf0b14 100644 --- a/app/src/main/kotlin/com/aliucord/manager/model/github/Version.kt +++ b/app/src/main/kotlin/com/aliucord/manager/network/dto/Version.kt @@ -3,7 +3,7 @@ * Licensed under the Open Software License version 3.0 */ -package com.aliucord.manager.model.github +package com.aliucord.manager.network.dto import kotlinx.serialization.Serializable diff --git a/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt b/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt new file mode 100644 index 0000000..7915f70 --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/network/service/GithubService.kt @@ -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 = 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 = 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" + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/preferences/Preferences.kt b/app/src/main/kotlin/com/aliucord/manager/preferences/Preferences.kt deleted file mode 100644 index 16fb3d8..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/preferences/Preferences.kt +++ /dev/null @@ -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( - 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 -) diff --git a/app/src/main/kotlin/com/aliucord/manager/preferences/PreferencesHolder.kt b/app/src/main/kotlin/com/aliucord/manager/preferences/PreferencesHolder.kt deleted file mode 100644 index f9ad786..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/preferences/PreferencesHolder.kt +++ /dev/null @@ -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") -} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/about/ContributorsCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/ContributorsCard.kt similarity index 72% rename from app/src/main/kotlin/com/aliucord/manager/ui/component/about/ContributorsCard.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/component/ContributorsCard.kt index f8ee871..f61a604 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/about/ContributorsCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/component/ContributorsCard.kt @@ -3,26 +3,27 @@ * Licensed under the Open Software License version 3.0 */ -package com.aliucord.manager.ui.component.about +package com.aliucord.manager.ui.component +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.grid.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage import com.aliucord.manager.R -import com.aliucord.manager.model.github.GithubUser +import com.aliucord.manager.network.dto.GithubUser @Composable fun ContributorsCard(contributors: List) { - ElevatedCard( - modifier = Modifier - .wrapContentHeight() - .fillMaxWidth() - ) { + ElevatedCard { Column( modifier = Modifier .padding(16.dp) @@ -56,3 +57,17 @@ fun ContributorsCard(contributors: List) { } } } + +@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 + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/GrantPermission.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/GrantPermission.kt index b16590e..98fccc9 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/GrantPermission.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/component/GrantPermission.kt @@ -32,7 +32,7 @@ fun GrantPermission(permissionState: PermissionState) { horizontalAlignment = Alignment.CenterHorizontally ) { Text( - stringResource(R.string.permission_required), + text = stringResource(R.string.permission_required), style = MaterialTheme.typography.headlineMedium, textAlign = TextAlign.Center ) @@ -42,7 +42,7 @@ fun GrantPermission(permissionState: PermissionState) { Row { val context = LocalContext.current - Button(onClick = { permissionState.launchPermissionRequest() }) { + Button(onClick = permissionState::launchPermissionRequest) { Text(stringResource(R.string.permission_grant)) } @@ -52,15 +52,11 @@ fun GrantPermission(permissionState: PermissionState) { onClick = { context.startActivity( Intent( - Settings.ACTION_APPLICATION_DETAILS_SETTINGS, - Uri.fromParts("package", BuildConfig.APPLICATION_ID, null) + /* action = */ Settings.ACTION_APPLICATION_DETAILS_SETTINGS, + /* uri = */ Uri.fromParts("package", BuildConfig.APPLICATION_ID, null) ) ) - }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer - ), + } ) { Text(stringResource(R.string.open_settings)) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallerDialog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/InstallerDialog.kt similarity index 77% rename from app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallerDialog.kt rename to app/src/main/kotlin/com/aliucord/manager/ui/component/InstallerDialog.kt index 6e270b1..8f40ed4 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/installer/InstallerDialog.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/component/InstallerDialog.kt @@ -3,15 +3,17 @@ * Licensed under the Open Software License version 3.0 */ -package com.aliucord.manager.ui.component.installer +package com.aliucord.manager.ui.component import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.Download import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import com.aliucord.manager.R @@ -22,34 +24,27 @@ fun InstallerDialog( onDismissRequest: () -> Unit, onConfirm: (DownloadMethod) -> Unit ) { - var selectedMethod by remember { mutableStateOf(DownloadMethod.DOWNLOAD) } + var selectedMethod by rememberSaveable { mutableStateOf(DownloadMethod.DOWNLOAD) } AlertDialog( icon = { Icon( - painter = painterResource(id = R.drawable.ic_download_24dp), + imageVector = Icons.Default.Download, contentDescription = stringResource(R.string.download_method) ) }, title = { - Row( - modifier = Modifier.fillMaxWidth(), - verticalAlignment = Alignment.CenterVertically - ) { - Text(stringResource(R.string.download_method)) - } + Text(stringResource(R.string.download_method)) }, text = { Column { mapOf( DownloadMethod.DOWNLOAD to stringResource(R.string.download_apk), - DownloadMethod.SELECT to stringResource(R.string.select_apk) + // DownloadMethod.SELECT to stringResource(R.string.select_apk) ).forEach { (method, name) -> Column { Row( - modifier = Modifier.clickable { - selectedMethod = method - }, + modifier = Modifier.clickable { selectedMethod = method }, verticalAlignment = Alignment.CenterVertically ) { Text( diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/about/ContributorEntry.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/about/ContributorEntry.kt deleted file mode 100644 index 9ac9792..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/about/ContributorEntry.kt +++ /dev/null @@ -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) - ) -} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/about/TeamCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/about/TeamCard.kt deleted file mode 100644 index 242e3df..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/about/TeamCard.kt +++ /dev/null @@ -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") - } - } -} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/about/UserEntry.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/about/UserEntry.kt deleted file mode 100644 index 09eed86..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/about/UserEntry.kt +++ /dev/null @@ -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) - } - } -} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/Changelog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/Changelog.kt index f555592..57c3cce 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/Changelog.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/Changelog.kt @@ -5,38 +5,32 @@ package com.aliucord.manager.ui.component.plugins -import android.annotation.SuppressLint import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.items import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.text.ClickableText import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Person +import androidx.compose.material.icons.filled.History import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.clip -import androidx.compose.ui.graphics.vector.rememberVectorPainter -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.style.TextDecoration import androidx.compose.ui.unit.dp import coil.compose.AsyncImage -import coil.request.ImageRequest import com.aliucord.manager.R -import com.aliucord.manager.model.Plugin +import com.aliucord.manager.domain.model.Plugin private val hyperLinkPattern = Regex("\\[(.+?)]\\((.+?\\))") @Suppress("RegExpRedundantEscape") // It is very much not redundant and causes a crash lol private val headerStylePattern = Regex("\\{(improved|added|fixed)( marginTop)?\\}") -@SuppressLint("ComposableNaming") // Can't use MaterialTheme without Composable, but this is not a component @Composable private fun AnnotatedString.Builder.hyperlink(content: String) { var idx = 0 @@ -51,7 +45,7 @@ private fun AnnotatedString.Builder.hyperlink(content: String) { append(content.substring(idx, start)) withStyle( - style = SpanStyle( + SpanStyle( color = MaterialTheme.colorScheme.primary, textDecoration = TextDecoration.Underline ) @@ -77,9 +71,8 @@ fun Changelog( onDismissRequest = onDismiss, icon = { Icon( - painter = painterResource(R.drawable.ic_history_white_24dp), - contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name), - tint = MaterialTheme.colorScheme.onSurface + imageVector = Icons.Default.History, + contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name) ) }, title = { Text(plugin.manifest.name) }, @@ -91,17 +84,12 @@ fun Changelog( .fillMaxWidth() .heightIn(max = 90.dp) .clip(RoundedCornerShape(14.dp)), - model = ImageRequest.Builder(LocalContext.current) - .data(mediaUrl) - .build(), - placeholder = rememberVectorPainter(image = Icons.Default.Person), + model = mediaUrl, contentDescription = stringResource(R.string.changelog_media) ) } - LazyColumn( - modifier = Modifier.wrapContentHeight() - ) { + LazyColumn { items(plugin.manifest.changelog!!.lines()) { var line = it.trim() @@ -113,16 +101,17 @@ fun Changelog( } while (line.startsWith("#")) Text( - line.trimStart(), + text = line.trimStart(), fontWeight = FontWeight.Bold, modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) ) } '*' -> { LinkText( - buildAnnotatedString { + modifier = Modifier.padding(bottom = 2.dp), + annotatedString = buildAnnotatedString { withStyle( - style = SpanStyle( + SpanStyle( color = MaterialTheme.colorScheme.primary, fontWeight = FontWeight.Bold ) @@ -131,26 +120,25 @@ fun Changelog( } hyperlink(line.substring(1)) - }, - Modifier.padding(bottom = 2.dp) + } ) } else -> { when { line.endsWith("marginTop}") -> { val color = MaterialTheme.colorScheme.onSurface + Text( - line, + text = line, fontWeight = FontWeight.Bold, color = color, modifier = Modifier.padding(top = 16.dp, bottom = 6.dp) ) } - line.all { c -> c == '=' } -> { - } // Discord ignores ======= + line.all { c -> c == '=' } -> { } // Discord ignores ======= else -> { LinkText( - buildAnnotatedString { + annotatedString = buildAnnotatedString { hyperlink(line) } ) @@ -164,9 +152,7 @@ fun Changelog( } }, confirmButton = { - Button( - onClick = onDismiss - ) { + Button(onClick = onDismiss) { Text(stringResource(R.string.close)) } } @@ -174,17 +160,20 @@ fun Changelog( } @Composable -private fun LinkText(annotatedString: AnnotatedString, modifier: Modifier = Modifier) { +private fun LinkText( + modifier: Modifier = Modifier, + annotatedString: AnnotatedString +) { val urlHandler = LocalUriHandler.current ClickableText( + modifier = modifier, text = annotatedString, style = TextStyle(color = LocalContentColor.current), onClick = { offset -> annotatedString.getStringAnnotations(offset, offset).firstOrNull()?.let { urlHandler.openUri(it.item) } - }, - modifier = modifier + } ) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/PluginCard.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/PluginCard.kt index 1dbe804..5239aa3 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/PluginCard.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/component/plugins/PluginCard.kt @@ -5,13 +5,15 @@ package com.aliucord.manager.ui.component.plugins +import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* -import androidx.compose.foundation.selection.toggleable import androidx.compose.foundation.text.ClickableText import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.History import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.ui.Modifier import androidx.compose.ui.draw.alpha import androidx.compose.ui.platform.LocalUriHandler @@ -21,29 +23,26 @@ import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp import com.aliucord.manager.R -import com.aliucord.manager.model.Plugin +import com.aliucord.manager.domain.model.Plugin @Composable fun PluginCard( plugin: Plugin, - onDelete: () -> Unit, - onShowChangelog: () -> Unit + onClickDelete: () -> Unit, + onClickShowChangelog: () -> Unit ) { - val uriHandler = LocalUriHandler.current - - ElevatedCard( - modifier = Modifier.fillMaxWidth() - ) { + ElevatedCard { Column( - modifier = Modifier.padding(16.dp) + modifier = Modifier + .fillMaxWidth() + .padding(16.dp) ) { - var isEnabled by remember { mutableStateOf(true) } + val uriHandler = LocalUriHandler.current + var isEnabled by rememberSaveable { mutableStateOf(true) } // Header Row( - modifier = Modifier.toggleable(value = isEnabled) { - isEnabled = it - } + modifier = Modifier.clickable { isEnabled = !isEnabled } ) { Column { // Name @@ -79,16 +78,14 @@ fun PluginCard( } } } + ClickableText( text = authors, style = MaterialTheme.typography.labelMedium, - onClick = { - authors.getStringAnnotations("authorId", it, it) + onClick = { offset -> + authors.getStringAnnotations("authorId", offset, offset) .firstOrNull() - ?.let { (id) -> - val url = "https://discord.com/users/$id" - uriHandler.openUri(url) - } + ?.let { (id) -> uriHandler.openUri("https://discord.com/users/$id") } } ) } @@ -102,15 +99,15 @@ fun PluginCard( ) } - Divider(modifier = Modifier.alpha(0.1f)) + Divider(modifier = Modifier.alpha(0.3f)) // Description Text( - text = plugin.manifest.description, - style = MaterialTheme.typography.bodyMedium, modifier = Modifier .heightIn(max = 150.dp, min = 40.dp) .padding(top = 10.dp), + text = plugin.manifest.description, + style = MaterialTheme.typography.bodyMedium ) Row( @@ -118,23 +115,19 @@ fun PluginCard( ) { IconButton( onClick = { - uriHandler.openUri( - plugin.manifest.updateUrl - .replace("raw.githubusercontent.com", "github.com") - .replaceFirst("/builds.*".toRegex(), "") - ) + uriHandler.openUri(plugin.manifest.repositoryUrl) } ) { Icon( painter = painterResource(R.drawable.ic_account_github_white_24dp), - contentDescription = "GitHub" + contentDescription = stringResource(R.string.github) ) } if (plugin.manifest.changelog != null) { - IconButton(onClick = onShowChangelog) { + IconButton(onClick = onClickShowChangelog) { Icon( - painter = painterResource(R.drawable.ic_history_white_24dp), + imageVector = Icons.Default.History, contentDescription = stringResource(R.string.view_plugin_changelog, plugin.manifest.name) ) } @@ -142,12 +135,10 @@ fun PluginCard( Spacer(Modifier.weight(1f, true)) - IconButton( - onClick = onDelete - ) { + IconButton(onClick = onClickDelete) { Icon( imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.uninstall_plugin, plugin.manifest.name), + contentDescription = stringResource(R.string.uninstall), tint = MaterialTheme.colorScheme.error ) } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/settings/Setting.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/settings/Setting.kt deleted file mode 100644 index 0dc3c10..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/settings/Setting.kt +++ /dev/null @@ -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 -) = ListItem( - modifier = Modifier.clickable { - preference.set(!preference.get()) - }, - leadingContent = icon, - headlineText = title, - supportingText = description, - trailingContent = { - Switch( - checked = preference.get(), - onCheckedChange = preference::set - ) - } -) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/settings/ThemeDialog.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/settings/ThemeDialog.kt deleted file mode 100644 index 737f7b4..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/settings/ThemeDialog.kt +++ /dev/null @@ -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)) - } - } - ) -} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/component/settings/ThemeSetting.kt b/app/src/main/kotlin/com/aliucord/manager/ui/component/settings/ThemeSetting.kt deleted file mode 100644 index 9d26afb..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/component/settings/ThemeSetting.kt +++ /dev/null @@ -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) - } - } -) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/navigation/AppDestination.kt b/app/src/main/kotlin/com/aliucord/manager/ui/navigation/AppDestination.kt index 59ac561..0abe16d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/navigation/AppDestination.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/navigation/AppDestination.kt @@ -23,9 +23,6 @@ sealed interface AppDestination : Destination { @Parcelize object Install : AppDestination - @Parcelize - object Store : AppDestination - @Parcelize object About : AppDestination diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screen/AboutScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screen/AboutScreen.kt index 4718d9b..ad15ee5 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screen/AboutScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screen/AboutScreen.kt @@ -6,24 +6,30 @@ package com.aliucord.manager.ui.screen import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.NavigateBefore import androidx.compose.material3.* import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import coil.compose.AsyncImage import com.aliucord.manager.R -import com.aliucord.manager.ui.component.about.ContributorsCard -import com.aliucord.manager.ui.component.about.TeamCard +import com.aliucord.manager.ui.component.ContributorsCard import com.aliucord.manager.ui.viewmodel.AboutViewModel +import org.koin.androidx.compose.getViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun AboutScreen(onClickBack: () -> Unit) { +fun AboutScreen( + viewModel: AboutViewModel = getViewModel(), + onClickBack: () -> Unit +) { Scaffold( topBar = { MediumTopAppBar( @@ -40,14 +46,35 @@ fun AboutScreen(onClickBack: () -> Unit) { } ) { paddingValues -> Column( - verticalArrangement = Arrangement.spacedBy(8.dp), modifier = Modifier .padding(paddingValues) - .padding(horizontal = 12.dp, vertical = 14.dp) + .padding(horizontal = 12.dp, vertical = 14.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { val uriHandler = LocalUriHandler.current - val viewModel: AboutViewModel = viewModel() - TeamCard() + + ElevatedCard { + Column( + modifier = Modifier + .padding(16.dp) + .fillMaxWidth(), + verticalArrangement = Arrangement.spacedBy(8.dp) + ) { + Text( + text = stringResource(R.string.team), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary + ) + + Column( + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + UserEntry("Juby210", "Fox") + UserEntry("Vendicated", "the ven") + } + } + } + ContributorsCard(viewModel.contributors) Spacer(Modifier.weight(1f, true)) @@ -58,11 +85,40 @@ fun AboutScreen(onClickBack: () -> Unit) { ) { Icon( painter = painterResource(R.drawable.ic_account_github_white_24dp), - contentDescription = "GitHub" + contentDescription = stringResource(R.string.github) ) - Spacer(Modifier.padding(4.dp)) + + Spacer(Modifier.width(ButtonDefaults.IconSpacing)) + Text(stringResource(R.string.github)) } } } } + +@Composable +private fun UserEntry(name: String, roles: String) { + Column { + Row( + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(8.dp) + ) { + AsyncImage( + modifier = Modifier + .size(54.dp) + .clip(CircleShape), + model = "https://github.com/$name.png", + contentDescription = name + ) + + Column { + Text( + text = name, + style = MaterialTheme.typography.titleLarge + ) + + Text(roles) + } + } + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeRootScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeRootScreen.kt index 7369678..201edbe 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeRootScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeRootScreen.kt @@ -1,42 +1,35 @@ package com.aliucord.manager.ui.screen -import android.content.res.Configuration import androidx.compose.animation.* -import androidx.compose.foundation.layout.* +import androidx.compose.foundation.layout.padding import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.* +import androidx.compose.material.icons.filled.Info +import androidx.compose.material.icons.filled.Settings import androidx.compose.material3.* -import androidx.compose.runtime.* -import androidx.compose.ui.Alignment +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalConfiguration import androidx.compose.ui.platform.LocalUriHandler import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource -import androidx.compose.ui.unit.DpOffset -import androidx.compose.ui.unit.dp import com.aliucord.manager.BuildConfig import com.aliucord.manager.R -import com.aliucord.manager.ui.navigation.AppDestination import com.aliucord.manager.ui.navigation.HomeDestination -import com.xinto.taxi.* +import com.xinto.taxi.Taxi +import com.xinto.taxi.rememberNavigator @OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class) @Composable -fun MainRootScreen(navigator: BackstackNavigator) { +fun MainRootScreen( + onClickInstall: () -> Unit, + onClickAbout: () -> Unit, + onClickSettings: () -> Unit +) { val mainRootNavigator = rememberNavigator(HomeDestination.HOME) - val currentDestination = mainRootNavigator.currentDestination - val orientation = LocalConfiguration.current.orientation - var isMenuExpanded by remember { mutableStateOf(false) } Scaffold( - modifier = Modifier - .fillMaxSize(), topBar = { MediumTopAppBar( - title = { - Text(stringResource(mainRootNavigator.currentDestination.label)) - }, + title = { Text(stringResource(mainRootNavigator.currentDestination.label)) }, actions = { val localUriHandler = LocalUriHandler.current @@ -47,125 +40,64 @@ fun MainRootScreen(navigator: BackstackNavigator) { ) { Icon( painter = painterResource(R.drawable.ic_discord), - contentDescription = stringResource(R.string.support_server), - tint = MaterialTheme.colorScheme.onBackground + contentDescription = stringResource(R.string.support_server) ) } - Box( - modifier = Modifier.wrapContentSize(Alignment.TopEnd) - ) { - IconButton( - onClick = { isMenuExpanded = !isMenuExpanded } - ) { - Icon( - imageVector = Icons.Default.MoreVert, - contentDescription = stringResource(R.string.show_menu), - tint = MaterialTheme.colorScheme.onBackground - ) - } + IconButton(onClick = onClickAbout) { + Icon( + imageVector = Icons.Default.Info, + contentDescription = stringResource(R.string.about) + ) + } - DropdownMenu( - offset = DpOffset(x = 12.dp, y = 0.dp), - expanded = isMenuExpanded, - onDismissRequest = { isMenuExpanded = false } - ) { - DropdownMenuItem( - onClick = { - isMenuExpanded = false - navigator.push(AppDestination.About) - }, - leadingIcon = { - Icon( - imageVector = Icons.Default.Info, - contentDescription = "About" - ) - }, - text = { Text(stringResource(R.string.about)) } - ) - - DropdownMenuItem( - onClick = { - isMenuExpanded = false - navigator.push(AppDestination.Settings) - }, - leadingIcon = { - Icon( - imageVector = Icons.Default.Settings, - contentDescription = "Settings" - ) - }, - text = { Text(stringResource(R.string.settings)) } - ) - } + IconButton(onClick = onClickSettings) { + Icon( + imageVector = Icons.Default.Settings, + contentDescription = stringResource(R.string.settings) + ) } } ) }, bottomBar = { - if (orientation == Configuration.ORIENTATION_PORTRAIT) { - NavigationBar { + val currentDestination = mainRootNavigator.currentDestination - HomeDestination.values().forEach { destination -> - NavigationBarItem( - selected = currentDestination == destination, - icon = { - if (currentDestination == destination) Icon( - destination.selectedIcon, - stringResource(destination.label) - ) else Icon(destination.unselectedIcon, stringResource(destination.label)) - }, - label = { Text(stringResource(destination.label)) }, - onClick = { mainRootNavigator.replace(destination) } - ) - } + NavigationBar { + HomeDestination.values().forEach { destination -> + NavigationBarItem( + selected = currentDestination == destination, + icon = { + if (currentDestination == destination) { + Icon( + imageVector = destination.selectedIcon, + contentDescription = stringResource(destination.label) + ) + } else { + Icon( + imageVector = destination.unselectedIcon, + contentDescription = stringResource(destination.label) + ) + } + }, + label = { Text(stringResource(destination.label)) }, + onClick = { mainRootNavigator.replace(destination) } + ) } } } ) { paddingValues -> - Row( - modifier = Modifier - .padding(paddingValues) - .padding(horizontal = 12.dp, vertical = 14.dp) - ) { - if (orientation == Configuration.ORIENTATION_LANDSCAPE) { - NavRail( - currentDestination = currentDestination, - onClickDestination = mainRootNavigator::replace + Taxi( + modifier = Modifier.padding(paddingValues), + navigator = mainRootNavigator, + transitionSpec = { fadeIn() with fadeOut() } + ) { destination -> + when (destination) { + HomeDestination.HOME -> HomeScreen( + onClickInstall = onClickInstall ) - } - - Taxi( - modifier = Modifier.weight(1f, true), - navigator = mainRootNavigator, - transitionSpec = { fadeIn() with fadeOut() } - ) { destination -> - when (destination) { - HomeDestination.HOME -> HomeScreen(navigator = navigator) - HomeDestination.PLUGINS -> PluginsScreen(navigator = navigator) - } + HomeDestination.PLUGINS -> PluginsScreen() } } - - } -} - -@Composable -fun NavRail( - currentDestination: HomeDestination, - onClickDestination: (HomeDestination) -> Unit -) = NavigationRail { - HomeDestination.values().forEach { destination -> - NavigationRailItem( - selected = currentDestination == destination, - icon = { - if (currentDestination == destination) Icon( - destination.selectedIcon, - stringResource(destination.label) - ) else Icon(destination.unselectedIcon, stringResource(destination.label)) - }, - label = { Text(stringResource(destination.label)) }, - onClick = { onClickDestination(destination) } - ) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeScreen.kt index 9ccd3a6..edeb9e9 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screen/HomeScreen.kt @@ -12,63 +12,56 @@ import androidx.compose.foundation.clickable import androidx.compose.foundation.layout.* import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.material.icons.Icons -import androidx.compose.material.icons.filled.Delete +import androidx.compose.material.icons.filled.* import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalUriHandler -import androidx.compose.ui.res.painterResource import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.* import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel -import androidx.paging.* +import androidx.paging.LoadState import androidx.paging.compose.collectAsLazyPagingItems import androidx.paging.compose.items import com.aliucord.manager.R -import com.aliucord.manager.model.github.Commit -import com.aliucord.manager.preferences.Prefs -import com.aliucord.manager.ui.component.installer.DownloadMethod -import com.aliucord.manager.ui.component.installer.InstallerDialog -import com.aliucord.manager.ui.navigation.AppDestination +import com.aliucord.manager.ui.component.DownloadMethod +import com.aliucord.manager.ui.component.InstallerDialog import com.aliucord.manager.ui.viewmodel.HomeViewModel -import com.aliucord.manager.util.Github -import com.xinto.taxi.BackstackNavigator +import org.koin.androidx.compose.getViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun HomeScreen(navigator: BackstackNavigator) { - val viewModel: HomeViewModel = viewModel() - +fun HomeScreen( + viewModel: HomeViewModel = getViewModel(), + onClickInstall: () -> Unit +) { var showOptionsDialog by remember { mutableStateOf(false) } if (showOptionsDialog) { val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri -> if (uri == null) return@rememberLauncherForActivityResult - navigator.push(AppDestination.Install) + onClickInstall() } InstallerDialog( onDismissRequest = { showOptionsDialog = false }, onConfirm = { method -> - if (method == DownloadMethod.DOWNLOAD) { - navigator.push(AppDestination.Install) - } else { - filePicker.launch(arrayOf("application/octet-stream")) + when (method) { + DownloadMethod.DOWNLOAD -> onClickInstall() + DownloadMethod.SELECT -> filePicker.launch(arrayOf("application/octet-stream")) } } ) } Column( - verticalArrangement = Arrangement.spacedBy(12.dp) + modifier = Modifier.padding(horizontal = 16.dp), + verticalArrangement = Arrangement.spacedBy(8.dp) ) { - ElevatedCard( - modifier = Modifier.wrapContentHeight() - ) { + ElevatedCard { Column( modifier = Modifier .padding(16.dp) @@ -77,52 +70,53 @@ fun HomeScreen(navigator: BackstackNavigator) { ) { Column { Text( - "Aliucord${Prefs.packageName.get().let { if (it != "com.aliucord") " ($it)" else "" }}", + text = "Aliucord${viewModel.preferences.packageName.let { if (it != "com.aliucord") " ($it)" else "" }}", style = MaterialTheme.typography.titleLarge, color = MaterialTheme.colorScheme.primary ) - Text(buildAnnotatedString { - append("Supported version: ") + Text( + buildAnnotatedString { + append("Supported version: ") - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(viewModel.supportedVersion) + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(viewModel.supportedVersion) + } + + append("\nInstalled version: ") + + withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { + append(viewModel.installedVersion) + } } - - append("\nInstalled version: ") - - withStyle(SpanStyle(fontWeight = FontWeight.Bold)) { - append(viewModel.installedVersion) - } - }) - } - - val (drawable, description) = when (viewModel.installedVersion) { - "-" -> R.drawable.ic_download_24dp to R.string.install - viewModel.supportedVersion -> R.drawable.ic_reinstall_24dp to R.string.reinstall - else -> R.drawable.ic_update_24dp to R.string.update + ) } Column( - modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(12.dp) ) { + val (icon, description) = when (viewModel.installedVersion) { + "-" -> Icons.Default.Download to R.string.install + viewModel.supportedVersion -> Icons.Default.Update to R.string.reinstall + else -> Icons.Default.Update to R.string.update + } + Row( horizontalArrangement = Arrangement.spacedBy(12.dp) ) { Button( modifier = Modifier.weight(1f, true), onClick = { - if (Prefs.devMode.get()) { + if (viewModel.preferences.devMode) { showOptionsDialog = true } else { - navigator.push(AppDestination.Install) + onClickInstall() } } ) { Icon( modifier = Modifier.padding(8.dp), - painter = painterResource(drawable), + imageVector = icon, contentDescription = stringResource(description), tint = MaterialTheme.colorScheme.onPrimary ) @@ -132,9 +126,7 @@ fun HomeScreen(navigator: BackstackNavigator) { } if (viewModel.installedVersion != "-") { - Row( - modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(12.dp) ) { CompositionLocalProvider( @@ -142,14 +134,12 @@ fun HomeScreen(navigator: BackstackNavigator) { ) { FilledTonalButton( modifier = Modifier.weight(1f), - onClick = { - viewModel.uninstallAliucord() - } + onClick = viewModel::uninstallAliucord ) { Icon( modifier = Modifier.padding(8.dp), imageVector = Icons.Default.Delete, - contentDescription = "Uninstall" + contentDescription = stringResource(R.string.uninstall) ) } @@ -159,8 +149,8 @@ fun HomeScreen(navigator: BackstackNavigator) { ) { Icon( modifier = Modifier.padding(8.dp), - painter = painterResource(R.drawable.ic_launch_24dp), - contentDescription = "Launch" + imageVector = Icons.Default.Launch, + contentDescription = stringResource(R.string.launch) ) } } @@ -169,89 +159,60 @@ fun HomeScreen(navigator: BackstackNavigator) { } } } - val pager = remember { - Pager( - PagingConfig( - pageSize = 30, - enablePlaceholders = true, - maxSize = 200 - ) - ) { - object : PagingSource() { - override suspend fun load(params: LoadParams): LoadResult { - 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) = state.anchorPosition?.let { - state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1) - } - } - } - } ElevatedCard( - Modifier - .fillMaxWidth() - .weight(1f) + modifier = Modifier.padding(bottom = 16.dp) ) { Column( - modifier = Modifier.padding(16.dp, 16.dp, 16.dp), + modifier = Modifier + .padding(16.dp) + .weight(1f, true) + .fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - Row( - verticalAlignment = Alignment.CenterVertically - ) { - Text( - text = stringResource(R.string.commits), - style = MaterialTheme.typography.titleLarge, - color = MaterialTheme.colorScheme.primary - ) + val lazyPagingItems = viewModel.commits.collectAsLazyPagingItems() + + Text( + text = stringResource(R.string.commits), + style = MaterialTheme.typography.titleLarge, + color = MaterialTheme.colorScheme.primary + ) - Spacer(Modifier.weight(1f, true)) - } - val lazyPagingItems = pager.flow.collectAsLazyPagingItems() LazyColumn( - modifier = Modifier - .fillMaxWidth(), + modifier = Modifier.fillMaxWidth(), verticalArrangement = Arrangement.spacedBy(14.dp) ) { - if (lazyPagingItems.loadState.refresh == LoadState.Loading) item { - Text( - text = stringResource(R.string.paging_initial_load), - modifier = Modifier - .fillMaxWidth() - .wrapContentWidth(Alignment.CenterHorizontally) - ) + if (lazyPagingItems.loadState.refresh == LoadState.Loading) { + item { + Text( + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally), + text = stringResource(R.string.paging_initial_load) + ) + } } items(lazyPagingItems) { commitData -> if (commitData == null) return@items - val localUriHandler = LocalUriHandler.current + val uriHandler = LocalUriHandler.current ListItem( - modifier = Modifier.clickable { localUriHandler.openUri(commitData.htmlUrl) }, + modifier = Modifier.clickable { uriHandler.openUri(commitData.htmlUrl) }, overlineText = { Text(commitData.sha.substring(0, 7)) }, headlineText = { Text("${commitData.commit.message.split("\n").first()} - ${commitData.author.name}") } ) } - if (lazyPagingItems.loadState.append == LoadState.Loading) item { - CircularProgressIndicator( - modifier = Modifier - .fillMaxWidth() - .wrapContentWidth(Alignment.CenterHorizontally) - ) + if (lazyPagingItems.loadState.append == LoadState.Loading) { + item { + CircularProgressIndicator( + modifier = Modifier + .fillMaxWidth() + .wrapContentWidth(Alignment.CenterHorizontally) + ) + } } } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screen/InstallScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screen/InstallScreen.kt index 5acd937..c054876 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screen/InstallScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screen/InstallScreen.kt @@ -5,8 +5,6 @@ package com.aliucord.manager.ui.screen -import android.os.Build -import androidx.annotation.RequiresApi import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding @@ -16,25 +14,21 @@ import androidx.compose.material.icons.filled.NavigateBefore import androidx.compose.material3.* import androidx.compose.runtime.* import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel +import com.aliucord.manager.R import com.aliucord.manager.ui.viewmodel.InstallViewModel +import org.koin.androidx.compose.getViewModel -@RequiresApi(Build.VERSION_CODES.O) @OptIn(ExperimentalFoundationApi::class, ExperimentalMaterial3Api::class) @Composable fun InstallerScreen( + viewModel: InstallViewModel = getViewModel(), onClickBack: () -> Unit ) { - val viewModel: InstallViewModel = viewModel() - val navigateMain by viewModel.returnToHome.collectAsState(initial = false) - if (navigateMain) onClickBack - - LaunchedEffect(Unit) { - viewModel.startInstallation() - } + if (navigateMain) onClickBack() Scaffold( topBar = { @@ -44,7 +38,7 @@ fun InstallerScreen( IconButton(onClick = onClickBack) { Icon( imageVector = Icons.Default.NavigateBefore, - contentDescription = "Back" + contentDescription = stringResource(R.string.back) ) } } @@ -62,6 +56,7 @@ fun InstallerScreen( modifier = Modifier.fillParentMaxWidth() ) } + item { Text( text = viewModel.log, diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screen/PluginsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screen/PluginsScreen.kt index 1ba8f1d..ad7115d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screen/PluginsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screen/PluginsScreen.kt @@ -14,50 +14,100 @@ import androidx.compose.foundation.text.KeyboardOptions import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.* import androidx.compose.material3.* -import androidx.compose.runtime.* +import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalFocusManager import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.input.ImeAction import androidx.compose.ui.unit.dp -import androidx.compose.ui.window.Dialog import com.aliucord.manager.R -import com.aliucord.manager.model.Plugin import com.aliucord.manager.ui.component.plugins.Changelog import com.aliucord.manager.ui.component.plugins.PluginCard -import com.aliucord.manager.ui.navigation.AppDestination -import com.xinto.taxi.BackstackNavigator +import com.aliucord.manager.ui.viewmodel.PluginsViewModel +import org.koin.androidx.compose.getViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun PluginsScreen(navigator: BackstackNavigator) { +fun PluginsScreen( + viewModel: PluginsViewModel = getViewModel() +) { + if (viewModel.showUninstallDialog) { + val plugin = viewModel.selectedPlugin!! + + AlertDialog( + onDismissRequest = viewModel::hideUninstallDialog, + icon = { + Icon( + imageVector = Icons.Default.Delete, + contentDescription = stringResource(R.string.uninstall) + ) + }, + title = { + Text("${stringResource(R.string.uninstall)} ${plugin.manifest.name}") + }, + text = { + Text(stringResource(R.string.delete_plugin_body)) + }, + confirmButton = { + Button( + onClick = { viewModel.uninstallPlugin(plugin) } + ) { + Text(stringResource(android.R.string.ok)) + } + }, + dismissButton = { + Button( + onClick = viewModel::hideUninstallDialog, + colors = ButtonDefaults.buttonColors( + contentColor = MaterialTheme.colorScheme.onSecondaryContainer, + containerColor = MaterialTheme.colorScheme.secondaryContainer + ) + ) { + Text(stringResource(android.R.string.cancel)) + } + } + ) + } + + if (viewModel.showChangelogDialog) { + Changelog( + plugin = viewModel.selectedPlugin!!, + onDismiss = viewModel::hideChangelogDialog + ) + } + Column( - modifier = Modifier.fillMaxSize(), + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 16.dp), verticalArrangement = Arrangement.spacedBy(8.dp) ) { - var plugins by remember { mutableStateOf(Plugin.loadAll()) } - val focusManager = LocalFocusManager.current - var search by remember { mutableStateOf("") } + val search = viewModel.search + + if (viewModel.plugins.isNotEmpty()) { + val focusManager = LocalFocusManager.current - if (plugins.isNotEmpty()) { OutlinedTextField( - maxLines = 1, - value = search, - onValueChange = { search = it }, - singleLine = true, modifier = Modifier.fillMaxWidth(), + maxLines = 1, + value = viewModel.search, + onValueChange = viewModel::search, + singleLine = true, shape = RoundedCornerShape(12.dp), label = { Text(stringResource(R.string.search)) }, trailingIcon = { - if (search.isNotBlank()) { - IconButton(onClick = { search = "" }) { + if (viewModel.search.isNotBlank()) { + IconButton(onClick = viewModel::clearSearch) { Icon( imageVector = Icons.Default.Close, contentDescription = stringResource(R.string.clear) ) } } else { - Icon(Icons.Default.Search, stringResource(R.string.search_plugins)) + Icon( + imageVector = Icons.Default.Search, + contentDescription = stringResource(R.string.search_plugins) + ) } }, keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Search), @@ -67,59 +117,19 @@ fun PluginsScreen(navigator: BackstackNavigator) { LazyColumn( verticalArrangement = Arrangement.spacedBy(8.dp) ) { - items(plugins.filter { plugin -> - plugin.manifest.run { - name.contains(search, true) - || description.contains(search, true) - || authors.any { a -> a.name.contains(search, true) } - } - }) { plugin -> - var showDeleteDialog by remember { mutableStateOf(false) } - var showChangelogDialog by remember { mutableStateOf(false) } - - if (showDeleteDialog) AlertDialog( - onDismissRequest = { showDeleteDialog = false }, - icon = { - Icon( - imageVector = Icons.Default.Delete, - contentDescription = stringResource(R.string.uninstall_plugin, plugin.manifest.name) - ) - }, - title = { Text(stringResource(R.string.uninstall_plugin, plugin.manifest.name)) }, - text = { Text(stringResource(R.string.delete_plugin_body)) }, - confirmButton = { - Button( - onClick = { - showDeleteDialog = false - plugin.file.delete() - plugins = plugins.filter { it != plugin } - } - ) { - Text(stringResource(android.R.string.ok)) - } - }, - dismissButton = { - Button( - onClick = { showDeleteDialog = false }, - colors = ButtonDefaults.buttonColors( - containerColor = MaterialTheme.colorScheme.secondaryContainer, - contentColor = MaterialTheme.colorScheme.onSecondaryContainer - ) - ) { - Text(stringResource(android.R.string.cancel)) - } + items( + viewModel.plugins.filter { plugin -> + plugin.manifest.run { + name.contains(search, true) + || description.contains(search, true) + || authors.any { (name) -> name.contains(search, true) } } - ) - - if (showChangelogDialog) Dialog( - onDismissRequest = { showChangelogDialog = false }, - content = { Changelog(plugin, onDismiss = { showChangelogDialog = false }) } - ) - + } + ) { plugin -> PluginCard( plugin = plugin, - onDelete = { showDeleteDialog = true }, - onShowChangelog = { showChangelogDialog = true } + onClickDelete = { viewModel.showUninstallDialog(plugin) }, + onClickShowChangelog = { viewModel.showChangelogDialog(plugin) } ) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screen/SettingsScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screen/SettingsScreen.kt index fbeb3d0..b44c30a 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screen/SettingsScreen.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/screen/SettingsScreen.kt @@ -5,28 +5,30 @@ package com.aliucord.manager.ui.screen -import androidx.compose.animation.AnimatedVisibility -import androidx.compose.animation.shrinkVertically +import androidx.compose.animation.* +import androidx.compose.foundation.* import androidx.compose.foundation.layout.* -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.NavigateBefore +import androidx.compose.material.icons.filled.Style import androidx.compose.material3.* import androidx.compose.runtime.* +import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.style.TextAlign import androidx.compose.ui.unit.dp -import androidx.lifecycle.viewmodel.compose.viewModel import com.aliucord.manager.R -import com.aliucord.manager.preferences.Prefs -import com.aliucord.manager.ui.component.settings.* +import com.aliucord.manager.ui.theme.Theme import com.aliucord.manager.ui.viewmodel.SettingsViewModel +import org.koin.androidx.compose.getViewModel @OptIn(ExperimentalMaterial3Api::class) @Composable -fun SettingsScreen(onClickBack: () -> Unit) { +fun SettingsScreen( + viewModel: SettingsViewModel = getViewModel(), + onClickBack: () -> Unit +) { Scaffold( topBar = { MediumTopAppBar( @@ -49,23 +51,25 @@ fun SettingsScreen(onClickBack: () -> Unit) { .verticalScroll(state = rememberScrollState()), verticalArrangement = Arrangement.spacedBy(12.dp) ) { - val viewModifier: SettingsViewModel = viewModel() + val preferences = viewModel.preferences - var showThemeDialog by remember { mutableStateOf(false) } - - if (showThemeDialog) { + if (viewModel.showThemeDialog) { ThemeDialog( - onDismissRequest = { showThemeDialog = false }, - onConfirm = { Prefs.theme.set(it.ordinal) } + currentTheme = preferences.theme, + onDismissRequest = viewModel::hideThemeDialog, + onConfirm = viewModel::setTheme ) } - ThemeSetting(onClick = { showThemeDialog = true }) - - PreferenceItem( - title = { Text(stringResource(R.string.use_black)) }, - description = { Text(stringResource(R.string.use_black_description)) }, - preference = Prefs.useBlack + ListItem( + modifier = Modifier.clickable(onClick = viewModel::showThemeDialog), + headlineText = { Text(stringResource(R.string.theme)) }, + supportingText = { Text(stringResource(R.string.theme_setting_description)) }, + trailingContent = { + FilledTonalButton(onClick = viewModel::showThemeDialog) { + Text(preferences.theme.displayName) + } + } ) Divider() @@ -74,27 +78,28 @@ fun SettingsScreen(onClickBack: () -> Unit) { modifier = Modifier .fillMaxWidth() .padding(horizontal = 10.dp), - value = Prefs.appName.get(), - onValueChange = Prefs.appName::set, + value = preferences.appName, + onValueChange = viewModel::setAppName, label = { Text(stringResource(R.string.app_name_setting)) }, placeholder = { Text("Aliucord") }, singleLine = true ) - PreferenceItem( + SwitchSetting( + checked = preferences.replaceBg, title = { Text(stringResource(R.string.replace_bg)) }, - preference = Prefs.replaceBg + onCheckedChange = { preferences.replaceBg = it } ) - val devModeOn = Prefs.devMode.get() - - PreferenceItem( - title = { Text(stringResource(R.string.developer_mode)) }, - preference = Prefs.devMode + SwitchSetting( + checked = preferences.devMode, + title = { Text(stringResource(R.string.advanced_options)) }, + onCheckedChange = { preferences.devMode = it } ) AnimatedVisibility( - visible = devModeOn, + visible = preferences.devMode, + enter = expandVertically(), exit = shrinkVertically() ) { Column( @@ -103,18 +108,29 @@ fun SettingsScreen(onClickBack: () -> Unit) { OutlinedTextField( modifier = Modifier .fillMaxWidth() - .padding(horizontal = 8.dp), - value = Prefs.packageName.get(), - onValueChange = Prefs.packageName::set, + .padding(horizontal = 10.dp), + value = preferences.packageName, + onValueChange = viewModel::setPackageName, label = { Text(stringResource(R.string.package_name)) }, placeholder = { Text("com.aliucord") }, singleLine = true ) - PreferenceItem( + OutlinedTextField( + modifier = Modifier + .fillMaxWidth() + .padding(horizontal = 10.dp), + value = preferences.version, + onValueChange = viewModel::setVersion, + label = { Text(stringResource(R.string.version)) }, + singleLine = true + ) + + SwitchSetting( + checked = preferences.debuggable, title = { Text(stringResource(R.string.debuggable)) }, description = { Text(stringResource(R.string.debuggable_description)) }, - preference = Prefs.debuggable + onCheckedChange = { preferences.debuggable = it } ) } } @@ -123,10 +139,88 @@ fun SettingsScreen(onClickBack: () -> Unit) { modifier = Modifier .fillMaxWidth() .padding(8.dp), - onClick = viewModifier::clearCacheDir + onClick = viewModel::clearCacheDir ) { - Text(stringResource(R.string.clear_files_cache), textAlign = TextAlign.Center) + Text( + text = stringResource(R.string.clear_files_cache), + textAlign = TextAlign.Center + ) } } } } + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun SwitchSetting( + checked: Boolean, + icon: @Composable (() -> Unit)? = null, + title: @Composable () -> Unit, + description: @Composable (() -> Unit)? = null, + onCheckedChange: (Boolean) -> Unit, +) { + ListItem( + modifier = Modifier.clickable { onCheckedChange(!checked) }, + leadingContent = icon, + headlineText = title, + supportingText = description, + trailingContent = { + Switch( + checked = checked, + onCheckedChange = onCheckedChange + ) + } + ) +} + +@Composable +fun ThemeDialog( + currentTheme: Theme, + onDismissRequest: () -> Unit, + onConfirm: (Theme) -> Unit +) { + var selectedTheme by remember { mutableStateOf(currentTheme) } + + AlertDialog( + onDismissRequest = onDismissRequest, + icon = { + Icon( + imageVector = Icons.Default.Style, + contentDescription = stringResource(R.string.theme) + ) + }, + title = { Text(stringResource(R.string.theme)) }, + text = { + Column { + Theme.values().forEach { theme -> + Row( + modifier = Modifier.clickable { selectedTheme = theme }, + verticalAlignment = Alignment.CenterVertically + ) { + Text( + text = theme.displayName, + style = MaterialTheme.typography.labelLarge + ) + + Spacer(Modifier.weight(1f, true)) + + RadioButton( + selected = theme == selectedTheme, + onClick = { selectedTheme = theme } + ) + } + } + } + }, + confirmButton = { + Button( + onClick = { + onConfirm(selectedTheme) + onDismissRequest() + } + ) { + Text(stringResource(R.string.apply)) + } + } + ) +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/screen/StoreScreen.kt b/app/src/main/kotlin/com/aliucord/manager/ui/screen/StoreScreen.kt deleted file mode 100644 index 0a03f59..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/screen/StoreScreen.kt +++ /dev/null @@ -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 -) { - @Serializable - data class File( - val mode: String, - val sha: String - ) -} - -@OptIn(ExperimentalMaterial3Api::class) -@Composable -fun StoreScreen(onClickBack: () -> Unit) { - val plugins = remember { mutableStateListOf() } - // 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() - - plugins.addAll(content.tree.filter { it.mode == "160000" }) - } - } - } else { - LazyColumn { - items(plugins) { item -> - Text("SHA: ${item.sha}", style = MaterialTheme.typography.labelSmall) - } - } - } - } - } -} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/theme/Color.kt b/app/src/main/kotlin/com/aliucord/manager/ui/theme/Color.kt deleted file mode 100644 index a5d9ecb..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/theme/Color.kt +++ /dev/null @@ -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 diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/theme/Theme.kt b/app/src/main/kotlin/com/aliucord/manager/ui/theme/Theme.kt index 2961cc7..020a6db 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/theme/Theme.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/theme/Theme.kt @@ -10,27 +10,11 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.material3.* import androidx.compose.runtime.Composable import androidx.compose.runtime.SideEffect -import androidx.compose.ui.graphics.Color import androidx.compose.ui.platform.LocalContext import com.google.accompanist.systemuicontroller.rememberSystemUiController -private val LightColorScheme = lightColorScheme() -private val DarkColorScheme = darkColorScheme() -private val BlackColorScheme = darkColorScheme( - background = Color.Black, - surface = Color.Black, - primary = Color.LightGray, - onPrimary = Color.DarkGray, - secondary = Color.Gray, - onSecondary = Color.LightGray, - secondaryContainer = Color.DarkGray, - onSecondaryContainer = Color.White, - outline = Color.LightGray -) - @Composable fun ManagerTheme( - isBlack: Boolean = false, isDarkTheme: Boolean = isSystemInDarkTheme(), isDynamicColor: Boolean = true, content: @Composable () -> Unit @@ -39,8 +23,8 @@ fun ManagerTheme( val colorScheme = when { dynamicColor && isDarkTheme -> dynamicDarkColorScheme(LocalContext.current) dynamicColor && !isDarkTheme -> dynamicLightColorScheme(LocalContext.current) - isDarkTheme -> if (isBlack) BlackColorScheme else DarkColorScheme - else -> LightColorScheme + isDarkTheme -> darkColorScheme() + else -> lightColorScheme() } val systemUiController = rememberSystemUiController() @@ -54,19 +38,12 @@ fun ManagerTheme( MaterialTheme( colorScheme = colorScheme, - typography = Typography, content = content ) } -enum class Theme( - val displayName: String -) { +enum class Theme(val displayName: String) { SYSTEM("System"), LIGHT("Light"), - DARK("Dark"); - - companion object { - fun from(index: Int) = values()[index] - } + DARK("Dark") } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/theme/Type.kt b/app/src/main/kotlin/com/aliucord/manager/ui/theme/Type.kt deleted file mode 100644 index 5788f38..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/ui/theme/Type.kt +++ /dev/null @@ -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() diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/AboutViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/AboutViewModel.kt index 37a8c6b..a75241f 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/AboutViewModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/AboutViewModel.kt @@ -3,29 +3,18 @@ package com.aliucord.manager.ui.viewmodel import androidx.compose.runtime.mutableStateListOf import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.aliucord.manager.model.github.GithubUser -import com.aliucord.manager.util.Github -import com.aliucord.manager.util.httpClient -import io.ktor.client.call.* -import io.ktor.client.request.* +import com.aliucord.manager.network.dto.GithubUser +import com.aliucord.manager.domain.repository.GithubRepository import kotlinx.coroutines.launch -class AboutViewModel : ViewModel() { - +class AboutViewModel( + private val githubRepository: GithubRepository +) : ViewModel() { val contributors = mutableStateListOf() - fun load() { + init { viewModelScope.launch { - val githubContributors = httpClient.get(Github.contributorsUrl).body>() - .sortedByDescending { - it.contributions - } - contributors.addAll(githubContributors) + contributors.addAll(githubRepository.getContributors()) } } - - init { - load() - } - } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/HomeViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/HomeViewModel.kt index 693f267..8f4938d 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/HomeViewModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/HomeViewModel.kt @@ -5,18 +5,21 @@ import android.content.Intent import android.net.Uri import android.widget.Toast import androidx.compose.runtime.* -import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope -import com.aliucord.manager.model.github.Version -import com.aliucord.manager.preferences.Prefs -import com.aliucord.manager.util.* -import io.ktor.client.request.* -import io.ktor.client.statement.* -import kotlinx.coroutines.* -import kotlinx.serialization.decodeFromString +import androidx.paging.* +import com.aliucord.manager.domain.manager.PreferencesManager +import com.aliucord.manager.domain.repository.GithubRepository +import com.aliucord.manager.network.dto.Commit +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.launch -class HomeViewModel(application: Application) : AndroidViewModel(application) { - private val packageManager = getApplication().packageManager +class HomeViewModel( + private val application: Application, + private val githubRepository: GithubRepository, + val preferences: PreferencesManager +) : ViewModel() { + private val packageManager = application.packageManager var supportedVersion by mutableStateOf("") private set @@ -24,43 +27,64 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) { var installedVersion by mutableStateOf("-") private set - private fun load() { - viewModelScope.launch(Dispatchers.IO) { - val version = json.decodeFromString(httpClient.get(Github.dataUrl).bodyAsText()) + val commits = Pager(PagingConfig(pageSize = 30)) { + object : PagingSource() { + override suspend fun load(params: LoadParams): LoadResult { + val page = params.key ?: 0 - withContext(Dispatchers.Main) { - supportedVersion = "${version.versionName} - " + when (version.versionCode[3]) { - '0' -> "Stable" - '1' -> "Beta" - '2' -> "Alpha" - else -> throw NoWhenBranchMatchedException() - } - installedVersion = try { - packageManager.getPackageInfo(Prefs.packageName.get(), 0).versionName - } catch (th: Throwable) { - "-" - } + val response = githubRepository.getCommits(page) + val prevKey = if (page > 0) page - 1 else null + val nextKey = if (response.isNotEmpty()) page + 1 else null + + return LoadResult.Page( + data = response, + prevKey = prevKey, + nextKey = nextKey + ) + } + + override fun getRefreshKey(state: PagingState) = state.anchorPosition?.let { + state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1) + } + } + }.flow.cachedIn(viewModelScope) + + init { + viewModelScope.launch(Dispatchers.IO) { + val version = githubRepository.getVersion() + + supportedVersion = "${version.versionName} - " + when (version.versionCode[3]) { + '0' -> "Stable" + '1' -> "Beta" + '2' -> "Alpha" + else -> throw NoWhenBranchMatchedException() + } + + installedVersion = try { + @Suppress("DEPRECATION") + packageManager.getPackageInfo(preferences.packageName, 0).versionName + } catch (th: Throwable) { + "-" } } } fun launchAliucord() { - packageManager.getLaunchIntentForPackage(Prefs.packageName.get())?.let { - getApplication().startActivity(it) - } ?: Toast.makeText(getApplication(), "Failed to launch Aliucord", Toast.LENGTH_LONG) - .show() + val launchIntent = packageManager.getLaunchIntentForPackage(preferences.packageName) + + if (launchIntent != null) { + application.startActivity(launchIntent) + } else { + Toast.makeText(application, "Failed to launch Aliucord", Toast.LENGTH_LONG).show() + } } fun uninstallAliucord() { - val packageURI = Uri.parse("package:${Prefs.packageName.get()}") + val packageURI = Uri.parse("package:${preferences.packageName}") val uninstallIntent = Intent(Intent.ACTION_DELETE, packageURI).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) } - getApplication().startActivity(uninstallIntent) - } - - init { - load() + application.startActivity(uninstallIntent) } } diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt index e1994d2..2ba1090 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/InstallViewModel.kt @@ -8,21 +8,29 @@ import android.content.pm.PackageInstaller import android.os.Build import androidx.annotation.RequiresApi import androidx.compose.runtime.* -import androidx.lifecycle.AndroidViewModel +import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import com.aliucord.manager.domain.manager.DownloadManager +import com.aliucord.manager.domain.manager.PreferencesManager import com.aliucord.manager.installer.service.InstallService -import com.aliucord.manager.installer.util.DownloadUtils import com.aliucord.manager.installer.util.ManifestPatcher -import com.aliucord.manager.util.Signer +import com.aliucord.manager.installer.util.Signer import com.android.zipflinger.* -import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.* import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.asSharedFlow -import kotlinx.coroutines.launch import java.io.File import java.util.zip.Deflater -class InstallViewModel(application: Application) : AndroidViewModel(application) { +class InstallViewModel( + private val application: Application, + private val downloadManager: DownloadManager, + private val preferences: PreferencesManager +) : ViewModel() { + private val assetManager = application.assets + private val externalCacheDir = application.externalCacheDir!! + private val packageInstaller = application.packageManager.packageInstaller + private val _returnToHome = MutableSharedFlow() val returnToHome = _returnToHome.asSharedFlow() @@ -31,39 +39,39 @@ class InstallViewModel(application: Application) : AndroidViewModel(application) var log by mutableStateOf("") private set + init { + viewModelScope.launch { + startInstallation() + } + } + @RequiresApi(Build.VERSION_CODES.O) - fun startInstallation() { + suspend fun startInstallation() { if (installationRunning) return - viewModelScope.launch(Dispatchers.IO) { + withContext(Dispatchers.IO) { installationRunning = true - val context = getApplication().applicationContext - val assetManager = context.assets - val externalCacheDir = context.externalCacheDir!! val arch = Build.SUPPORTED_ABIS.first() - val supportedVersion = "124206" + val supportedVersion = preferences.version val baseApk = externalCacheDir.resolve("base-${supportedVersion}.apk").also { file -> - val archiveInfo = context.packageManager.getPackageArchiveInfo(file.path, 0) + @Suppress("DEPRECATION") + val archiveInfo = application.packageManager.getPackageArchiveInfo(file.path, 0) - if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also run { log += "Using cached base APK\n"} + if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also run { log += "Using cached base APK\n" } log += "Downloading Discord APK... " - - DownloadUtils.downloadDiscordApk(context, supportedVersion) - + downloadManager.downloadDiscordApk(supportedVersion) log += "Done\n" } val libArch = arch.replace("-v", "_v") val libsApk = externalCacheDir.resolve("config.$libArch-${supportedVersion}.apk").also { file -> - if (file.exists()) return@also run { log += "Used cached libs APK\n" } + if (file.exists()) return@also run { log += "Using cached libs APK\n" } log += "Downloading libs APK... " - - DownloadUtils.downloadSplit(context, supportedVersion, "config.$libArch") - + downloadManager.downloadSplit(supportedVersion, "config.$libArch") log += "Done\n" } @@ -71,23 +79,19 @@ class InstallViewModel(application: Application) : AndroidViewModel(application) if (file.exists()) return@also run { log += "Using cached locale APK\n" } log += "Downloading locale APK... " - - DownloadUtils.downloadSplit(context, supportedVersion, "config.en") - + downloadManager.downloadSplit(supportedVersion, "config.en") log += "Done\n" } val xxhdpiApk = externalCacheDir.resolve("config.xxhdpi-${supportedVersion}.apk").also { file -> - if (file.exists()) return@also run { log += "Using cached drawables APK\n"} + if (file.exists()) return@also run { log += "Using cached drawables APK\n" } log += "Downloading drawables APK... " - - DownloadUtils.downloadSplit(context, supportedVersion, "config.xxhdpi") - + downloadManager.downloadSplit(supportedVersion, "config.xxhdpi") log += "Done\n" } - val apks = listOf(baseApk, libsApk, localeApk, xxhdpiApk) + val apks = arrayOf(baseApk, libsApk, localeApk, xxhdpiApk) log += "Patching manifest...\n" @@ -97,7 +101,14 @@ class InstallViewModel(application: Application) : AndroidViewModel(application) .array() ZipArchive(baseApk.toPath()).use { zip -> - val source = BytesSource(ManifestPatcher.patchManifest(byteArray), "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION) + val patchedBytesArray = ManifestPatcher.patchManifest( + manifestBytes = byteArray, + packageName = preferences.packageName, + appName = preferences.appName, + debuggable = preferences.debuggable, + ) + + val source = BytesSource(patchedBytesArray, "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION) zip.delete("AndroidManifest.xml") zip.add(source) @@ -109,15 +120,18 @@ class InstallViewModel(application: Application) : AndroidViewModel(application) .array() ZipArchive(apk.toPath()).use { zip -> - val source = BytesSource(ManifestPatcher.renamePackage(byteArray), "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION) + val patchedBytes = ManifestPatcher.renamePackage(byteArray, preferences.packageName) + val source = BytesSource(patchedBytes, "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION) zip.delete("AndroidManifest.xml") zip.add(source) } } } catch (e: Exception) { - log += "An error has occurred" + log += "An error has occurred: ${e.message}\n" e.printStackTrace() + + return@withContext } log += "Patching libraries...\n" @@ -141,7 +155,7 @@ class InstallViewModel(application: Application) : AndroidViewModel(application) log += "Done\n" - installApks(*apks.toTypedArray()) + installApks(*apks) _returnToHome.emit(true) installationRunning = false @@ -149,9 +163,7 @@ class InstallViewModel(application: Application) : AndroidViewModel(application) } private fun installApks(vararg apks: File) { - val context = getApplication().applicationContext val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL) - val packageInstaller = context.packageManager.packageInstaller val sessionId = packageInstaller.createSession(params) val session = packageInstaller.openSession(sessionId) @@ -163,13 +175,13 @@ class InstallViewModel(application: Application) : AndroidViewModel(application) } } - val callbackIntent = Intent(context, InstallService::class.java) + val callbackIntent = Intent(application, InstallService::class.java) @SuppressLint("UnspecifiedImmutableFlag") val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - PendingIntent.getService(context, 0, callbackIntent, PendingIntent.FLAG_MUTABLE) + PendingIntent.getService(application, 0, callbackIntent, PendingIntent.FLAG_MUTABLE) } else { - PendingIntent.getService(context, 0, callbackIntent, 0) + PendingIntent.getService(application, 0, callbackIntent, 0) } session.commit(contentIntent.intentSender) diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/PluginsViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/PluginsViewModel.kt new file mode 100644 index 0000000..eda6f9a --- /dev/null +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/PluginsViewModel.kt @@ -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(null) + private set + + val plugins = mutableStateListOf() + + 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 } + ) + } +} diff --git a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/SettingsViewModel.kt b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/SettingsViewModel.kt index 5d1c2ba..accb3ca 100644 --- a/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/SettingsViewModel.kt +++ b/app/src/main/kotlin/com/aliucord/manager/ui/viewmodel/SettingsViewModel.kt @@ -1,12 +1,43 @@ package com.aliucord.manager.ui.viewmodel import android.app.Application -import androidx.lifecycle.AndroidViewModel +import androidx.compose.runtime.* +import androidx.lifecycle.ViewModel +import com.aliucord.manager.domain.manager.PreferencesManager +import com.aliucord.manager.ui.theme.Theme -class SettingsViewModel(application: Application) : AndroidViewModel(application) { +class SettingsViewModel( + private val application: Application, + val preferences: PreferencesManager +) : ViewModel() { + var showThemeDialog by mutableStateOf(false) + private set - fun clearCacheDir() { - getApplication().cacheDir.deleteRecursively() + fun showThemeDialog() { + showThemeDialog = true } + fun hideThemeDialog() { + showThemeDialog = false + } + + fun setPackageName(packageName: String) { + if (packageName.isNotBlank()) preferences.packageName = packageName + } + + fun setAppName(appName: String) { + if (appName.isNotBlank()) preferences.appName = appName + } + + fun setTheme(theme: Theme) { + preferences.theme = theme + } + + fun setVersion(version: String) { + if (version.isNotBlank()) preferences.version = version + } + + fun clearCacheDir() { + application.cacheDir.deleteRecursively() + } } diff --git a/app/src/main/kotlin/com/aliucord/manager/util/Const.kt b/app/src/main/kotlin/com/aliucord/manager/util/Const.kt deleted file mode 100644 index 65e2d19..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/util/Const.kt +++ /dev/null @@ -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") diff --git a/app/src/main/kotlin/com/aliucord/manager/util/Github.kt b/app/src/main/kotlin/com/aliucord/manager/util/Github.kt deleted file mode 100644 index 5fef249..0000000 --- a/app/src/main/kotlin/com/aliucord/manager/util/Github.kt +++ /dev/null @@ -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>() - - suspend fun getCommits(vararg params: Pair): List { - 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>() - - commitsCache[query] = res - - return res - } -} diff --git a/app/src/main/res/drawable/ic_account_github_white_24dp.xml b/app/src/main/res/drawable/ic_account_github_white_24dp.xml index bb914a0..2d4f0b4 100644 --- a/app/src/main/res/drawable/ic_account_github_white_24dp.xml +++ b/app/src/main/res/drawable/ic_account_github_white_24dp.xml @@ -1,11 +1,11 @@ + 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" /> diff --git a/app/src/main/res/drawable/ic_download_24dp.xml b/app/src/main/res/drawable/ic_download_24dp.xml deleted file mode 100644 index 103827b..0000000 --- a/app/src/main/res/drawable/ic_download_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_history_white_24dp.xml b/app/src/main/res/drawable/ic_history_white_24dp.xml deleted file mode 100644 index f3e3f07..0000000 --- a/app/src/main/res/drawable/ic_history_white_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_launch_24dp.xml b/app/src/main/res/drawable/ic_launch_24dp.xml deleted file mode 100644 index 3fcd087..0000000 --- a/app/src/main/res/drawable/ic_launch_24dp.xml +++ /dev/null @@ -1,10 +0,0 @@ - - - - diff --git a/app/src/main/res/drawable/ic_plugin_24dp.xml b/app/src/main/res/drawable/ic_plugin_24dp.xml deleted file mode 100644 index 986ff93..0000000 --- a/app/src/main/res/drawable/ic_plugin_24dp.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - diff --git a/app/src/main/res/drawable/ic_reinstall_24dp.xml b/app/src/main/res/drawable/ic_reinstall_24dp.xml deleted file mode 100644 index f986635..0000000 --- a/app/src/main/res/drawable/ic_reinstall_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/drawable/ic_update_24dp.xml b/app/src/main/res/drawable/ic_update_24dp.xml deleted file mode 100644 index d51c785..0000000 --- a/app/src/main/res/drawable/ic_update_24dp.xml +++ /dev/null @@ -1,9 +0,0 @@ - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 50ee3b4..40fe31e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -6,16 +6,15 @@ Open settings You need to grant storage permission to use this app Grant permission - Replace app icon with Aliucord logo + Replace app icon with Aliucord variant Settings Show menu Theme Use Injector.dex from storage Plugins - Uninstall %1$s Search View changelog for %1$s - Are you sure you want to uninstall this plugin? This action cannot be undone. + Are you sure you want to uninstall this plugin? Close Support Server Back @@ -23,7 +22,7 @@ Install Reinstall Update - Developer Mode + Show advanced options Debuggable Enable debuggable flag About @@ -32,11 +31,8 @@ Search Plugins GitHub Installer - Use black - Use black colors for dark mode Manager appearance No plugins installed! - Store Loading… Contributor Apply @@ -49,4 +45,7 @@ App Name Package Name Clear + Launch + Uninstall + Version diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 1a7c308..176dc58 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ -#Mon Jul 04 12:13:32 EDT 2022 +#Sun Aug 14 21:28:22 EDT 2022 distributionBase=GRADLE_USER_HOME -distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip distributionPath=wrapper/dists zipStorePath=wrapper/dists zipStoreBase=GRADLE_USER_HOME