Add Koin and other minor changes
This commit is contained in:
parent
68a56e880d
commit
7a34c03981
65 changed files with 1172 additions and 1373 deletions
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -21,12 +21,8 @@
|
|||
android:name="android.permission.QUERY_ALL_PACKAGES"
|
||||
tools:ignore="QueryAllPackagesPermission" />
|
||||
|
||||
<queries>
|
||||
<package android:name="com.aliucord" />
|
||||
<package android:name="com.discord" />
|
||||
</queries>
|
||||
|
||||
<application
|
||||
android:name=".ManagerApplication"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
android:roundIcon="@mipmap/ic_launcher_round"
|
||||
|
|
|
|||
|
|
@ -3,10 +3,8 @@
|
|||
* Licensed under the Open Software License version 3.0
|
||||
*/
|
||||
|
||||
|
||||
package com.aliucord.manager
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.os.*
|
||||
import android.provider.Settings
|
||||
|
|
@ -19,24 +17,26 @@ import androidx.compose.foundation.layout.fillMaxSize
|
|||
import androidx.compose.ui.Modifier
|
||||
import androidx.core.net.toUri
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import com.aliucord.manager.preferences.Prefs
|
||||
import com.aliucord.manager.preferences.sharedPreferences
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.ui.navigation.AppDestination
|
||||
import com.aliucord.manager.ui.screen.*
|
||||
import com.aliucord.manager.ui.theme.ManagerTheme
|
||||
import com.aliucord.manager.ui.theme.Theme
|
||||
import com.xinto.taxi.Taxi
|
||||
import com.xinto.taxi.rememberBackstackNavigator
|
||||
import org.koin.android.ext.android.inject
|
||||
|
||||
val aliucordDir = Environment.getExternalStorageDirectory().resolve("Aliucord")
|
||||
|
||||
class MainActivity : ComponentActivity() {
|
||||
private val preferences: PreferencesManager by inject()
|
||||
|
||||
@OptIn(ExperimentalAnimationApi::class)
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
installSplashScreen()
|
||||
|
||||
super.onCreate(savedInstanceState)
|
||||
|
||||
sharedPreferences = getPreferences(Context.MODE_PRIVATE)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && !Environment.isExternalStorageManager()) {
|
||||
val intent = Intent(Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION)
|
||||
.setData("package:com.aliucord.manager".toUri())
|
||||
|
|
@ -46,17 +46,12 @@ class MainActivity : ComponentActivity() {
|
|||
|
||||
setContent {
|
||||
ManagerTheme(
|
||||
isBlack = Prefs.useBlack.get(),
|
||||
isDarkTheme = run {
|
||||
val theme = Theme.from(Prefs.theme.get())
|
||||
|
||||
theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK
|
||||
}
|
||||
isDarkTheme = preferences.theme == Theme.DARK || preferences.theme == Theme.SYSTEM && isSystemInDarkTheme(),
|
||||
) {
|
||||
val navigator = rememberBackstackNavigator<AppDestination>(AppDestination.Home)
|
||||
|
||||
BackHandler {
|
||||
navigator.pop()
|
||||
if (!navigator.pop()) finish()
|
||||
}
|
||||
|
||||
Taxi(
|
||||
|
|
@ -66,11 +61,11 @@ class MainActivity : ComponentActivity() {
|
|||
) { destination ->
|
||||
when (destination) {
|
||||
is AppDestination.Home -> MainRootScreen(
|
||||
navigator = navigator
|
||||
)
|
||||
is AppDestination.Plugins -> PluginsScreen(
|
||||
navigator = navigator
|
||||
onClickInstall = { navigator.push(AppDestination.Install) },
|
||||
onClickAbout = { navigator.push(AppDestination.About) },
|
||||
onClickSettings = { navigator.push(AppDestination.Settings) }
|
||||
)
|
||||
is AppDestination.Plugins -> PluginsScreen()
|
||||
is AppDestination.Install -> InstallerScreen(
|
||||
onClickBack = navigator::pop,
|
||||
)
|
||||
|
|
@ -80,9 +75,6 @@ class MainActivity : ComponentActivity() {
|
|||
is AppDestination.About -> AboutScreen(
|
||||
onClickBack = navigator::pop
|
||||
)
|
||||
is AppDestination.Store -> StoreScreen(
|
||||
onClickBack = navigator::pop
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
14
app/src/main/kotlin/com/aliucord/manager/di/AppModule.kt
Normal file
14
app/src/main/kotlin/com/aliucord/manager/di/AppModule.kt
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
package com.aliucord.manager.di
|
||||
|
||||
import kotlinx.serialization.json.Json
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val appModule = module {
|
||||
fun provideJson(): Json = Json {
|
||||
ignoreUnknownKeys = true
|
||||
isLenient = true
|
||||
}
|
||||
|
||||
singleOf(::provideJson)
|
||||
}
|
||||
18
app/src/main/kotlin/com/aliucord/manager/di/ManagerModule.kt
Normal file
18
app/src/main/kotlin/com/aliucord/manager/di/ManagerModule.kt
Normal file
|
|
@ -0,0 +1,18 @@
|
|||
package com.aliucord.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import com.aliucord.manager.domain.manager.DownloadManager
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val managerModule = module {
|
||||
fun providePreferences(context: Context): PreferencesManager {
|
||||
return PreferencesManager(context.getSharedPreferences("preferences", Context.MODE_PRIVATE))
|
||||
}
|
||||
|
||||
fun provideDownloadManager(context: Context) = DownloadManager(context)
|
||||
|
||||
singleOf(::providePreferences)
|
||||
singleOf(::provideDownloadManager)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -0,0 +1,52 @@
|
|||
package com.aliucord.manager.domain.manager
|
||||
|
||||
import android.app.DownloadManager
|
||||
import android.content.*
|
||||
import androidx.core.content.getSystemService
|
||||
import androidx.core.net.toUri
|
||||
import java.io.File
|
||||
import kotlin.coroutines.resume
|
||||
import kotlin.coroutines.suspendCoroutine
|
||||
|
||||
class DownloadManager(
|
||||
private val context: Context
|
||||
) {
|
||||
private val downloadManager = context.getSystemService<DownloadManager>()!!
|
||||
|
||||
suspend fun downloadDiscordApk(version: String): File {
|
||||
return download("$backendHost/download/discord?v=$version", "base-$version.apk")
|
||||
}
|
||||
|
||||
suspend fun downloadSplit(version: String, split: String): File {
|
||||
return download("$backendHost/download/discord?v=$version&split=$split", "$split-$version.apk")
|
||||
}
|
||||
|
||||
private suspend fun download(url: String, fileName: String): File {
|
||||
return suspendCoroutine { continuation ->
|
||||
val receiver = object : BroadcastReceiver() {
|
||||
var downloadId = 0L
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
val id = intent.getLongExtra(DownloadManager.EXTRA_DOWNLOAD_ID, -1)
|
||||
|
||||
if (downloadId == id) {
|
||||
context.unregisterReceiver(this)
|
||||
continuation.resume(context.filesDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
context.registerReceiver(receiver, IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE))
|
||||
|
||||
receiver.downloadId = DownloadManager.Request(url.toUri()).run {
|
||||
setNotificationVisibility(DownloadManager.Request.VISIBILITY_HIDDEN)
|
||||
setDestinationUri(context.externalCacheDir!!.resolve(fileName).toUri())
|
||||
downloadManager.enqueue(this)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val backendHost = "https://aliucord.com/"
|
||||
}
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -0,0 +1,90 @@
|
|||
package com.aliucord.manager.domain.manager.base
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.core.content.edit
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
abstract class BasePreferenceManager(
|
||||
private val prefs: SharedPreferences
|
||||
) {
|
||||
protected fun getString(key: String, defaultValue: String) = prefs.getString(key, defaultValue) ?: defaultValue
|
||||
private fun getBoolean(key: String, defaultValue: Boolean) = prefs.getBoolean(key, defaultValue)
|
||||
private fun getInt(key: String, defaultValue: Int) = prefs.getInt(key, defaultValue)
|
||||
private fun getFloat(key: String, defaultValue: Float) = prefs.getFloat(key, defaultValue)
|
||||
protected inline fun <reified E : Enum<E>> getEnum(key: String, defaultValue: E) =
|
||||
enumValueOf<E>(getString(key, defaultValue.name))
|
||||
|
||||
protected fun putString(key: String, value: String?) = prefs.edit { putString(key, value) }
|
||||
private fun putBoolean(key: String, value: Boolean) = prefs.edit { putBoolean(key, value) }
|
||||
private fun putInt(key: String, value: Int) = prefs.edit { putInt(key, value) }
|
||||
private fun putFloat(key: String, value: Float) = prefs.edit { putFloat(key, value) }
|
||||
protected inline fun <reified E : Enum<E>> putEnum(key: String, value: E) = putString(key, value.name)
|
||||
|
||||
protected class Preference<T>(
|
||||
private val key: String,
|
||||
defaultValue: T,
|
||||
getter: (key: String, defaultValue: T) -> T,
|
||||
private val setter: (key: String, newValue: T) -> Unit
|
||||
) {
|
||||
@Suppress("RedundantSetter")
|
||||
var value by mutableStateOf(getter(key, defaultValue))
|
||||
private set
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
|
||||
value = newValue
|
||||
setter(key, newValue)
|
||||
}
|
||||
}
|
||||
|
||||
protected fun stringPreference(
|
||||
key: String,
|
||||
defaultValue: String
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getString,
|
||||
setter = ::putString
|
||||
)
|
||||
|
||||
protected fun booleanPreference(
|
||||
key: String,
|
||||
defaultValue: Boolean
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getBoolean,
|
||||
setter = ::putBoolean
|
||||
)
|
||||
|
||||
protected fun intPreference(
|
||||
key: String,
|
||||
defaultValue: Int
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getInt,
|
||||
setter = ::putInt
|
||||
)
|
||||
|
||||
protected fun floatPreference(
|
||||
key: String,
|
||||
defaultValue: Float
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getFloat,
|
||||
setter = ::putFloat
|
||||
)
|
||||
|
||||
protected inline fun <reified E : Enum<E>> enumPreference(
|
||||
key: String,
|
||||
defaultValue: E
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getEnum,
|
||||
setter = ::putEnum
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
package com.aliucord.manager.domain.repository
|
||||
|
||||
import com.aliucord.manager.network.dto.GithubUser
|
||||
import com.aliucord.manager.network.dto.Version
|
||||
import com.aliucord.manager.network.service.GithubService
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class GithubRepository(
|
||||
private val json: Json,
|
||||
private val service: GithubService
|
||||
) {
|
||||
suspend fun getCommits(page: Int) = service.getCommits(page)
|
||||
|
||||
suspend fun getContributors() = service.getContributors()
|
||||
.sortedByDescending(GithubUser::contributions)
|
||||
|
||||
suspend fun getVersion() = json.decodeFromString<Version>(service.getVersion())
|
||||
}
|
||||
|
|
@ -6,28 +6,30 @@ import android.content.pm.PackageInstaller
|
|||
import android.os.IBinder
|
||||
import android.util.Log
|
||||
import android.widget.Toast
|
||||
import com.aliucord.manager.BuildConfig
|
||||
|
||||
class InstallService : Service() {
|
||||
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
|
||||
when (val statusCode = intent.getIntExtra(PackageInstaller.EXTRA_STATUS, -999)) {
|
||||
PackageInstaller.STATUS_PENDING_USER_ACTION -> {
|
||||
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)
|
||||
confirmationIntent!!.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
@Suppress("DEPRECATION") // No.
|
||||
val confirmationIntent = intent.getParcelableExtra<Intent>(Intent.EXTRA_INTENT)!!
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
|
||||
startActivity(confirmationIntent)
|
||||
}
|
||||
PackageInstaller.STATUS_SUCCESS -> Toast.makeText(
|
||||
this,
|
||||
"Successfully installed Aliucord!",
|
||||
Toast.LENGTH_LONG
|
||||
/* context = */ this,
|
||||
/* text = */ "Successfully installed Aliucord!",
|
||||
/* duration = */ Toast.LENGTH_LONG
|
||||
).show()
|
||||
PackageInstaller.STATUS_FAILURE_ABORTED -> Toast.makeText(
|
||||
this,
|
||||
"Aborted installation",
|
||||
Toast.LENGTH_LONG
|
||||
/* context = */ this,
|
||||
/* text = */ "Aborted installation",
|
||||
/* duration = */ Toast.LENGTH_LONG
|
||||
).show()
|
||||
else -> {
|
||||
Log.i("Installer", "Installation failed: error code $statusCode")
|
||||
Log.i(BuildConfig.TAG, "Installation failed: error code $statusCode")
|
||||
Toast.makeText(this, "Failed: Error $statusCode", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
@ -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
|
||||
|
|
@ -1,57 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Juby210 & zt
|
||||
* Licensed under the Open Software License version 3.0
|
||||
*/
|
||||
|
||||
package com.aliucord.manager.model
|
||||
|
||||
import android.util.Log
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.util.aliucordDir
|
||||
import com.aliucord.manager.util.json
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import java.io.File
|
||||
import java.util.zip.ZipException
|
||||
import java.util.zip.ZipFile
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
data class Plugin(val file: File) {
|
||||
val manifest: Manifest
|
||||
|
||||
init {
|
||||
ZipFile(file).use { zipFile ->
|
||||
val entry = zipFile.getEntry("manifest.json")
|
||||
?: throw ZipException("Plugin ${file.nameWithoutExtension} has no manifest.")
|
||||
|
||||
manifest = zipFile.getInputStream(entry).use { zis -> json.decodeFromStream(zis) }
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private val pluginsDir = aliucordDir.resolve("plugins")
|
||||
|
||||
fun loadAll(): List<Plugin> {
|
||||
if (!pluginsDir.exists() && !pluginsDir.mkdirs()) {
|
||||
Log.e(BuildConfig.TAG, "Failed to create plugins dir. Missing Permissions?")
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
val files = pluginsDir.listFiles() ?: return emptyList()
|
||||
|
||||
return files.mapNotNull { file ->
|
||||
if (file.extension == "zip") {
|
||||
try {
|
||||
Plugin(file)
|
||||
} catch (th: Throwable) {
|
||||
Log.e(BuildConfig.TAG, "Failed to load plugin ${file.nameWithoutExtension}", th)
|
||||
null
|
||||
}
|
||||
} else {
|
||||
Log.w(BuildConfig.TAG, "Found non zip ${file.name} in plugins folder.")
|
||||
null
|
||||
}
|
||||
}.sortedBy { it.manifest.name }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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
|
||||
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
package com.aliucord.manager.network.service
|
||||
|
||||
import com.aliucord.manager.network.dto.Commit
|
||||
import com.aliucord.manager.network.dto.GithubUser
|
||||
import io.ktor.client.*
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.engine.cio.*
|
||||
import io.ktor.client.plugins.contentnegotiation.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import io.ktor.serialization.kotlinx.json.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import kotlinx.serialization.json.Json
|
||||
|
||||
class GithubService(
|
||||
private val json: Json
|
||||
) {
|
||||
private val httpClient = HttpClient(CIO) {
|
||||
install(ContentNegotiation) {
|
||||
json(json)
|
||||
}
|
||||
}
|
||||
|
||||
suspend fun getCommits(page: Int): List<Commit> = withContext(Dispatchers.IO) {
|
||||
httpClient.get(commitsUrl) {
|
||||
parameter("page", page)
|
||||
}.body()
|
||||
}
|
||||
|
||||
suspend fun getVersion(): String = withContext(Dispatchers.IO) {
|
||||
httpClient.get(dataUrl).bodyAsText()
|
||||
}
|
||||
|
||||
suspend fun getContributors(): List<GithubUser> = withContext(Dispatchers.IO) {
|
||||
httpClient.get(contributorsUrl).body()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val org = "Aliucord"
|
||||
private const val repo = "Aliucord"
|
||||
|
||||
private const val commitsUrl = "https://api.github.com/repos/$org/$repo/commits"
|
||||
private const val contributorsUrl = "https://api.github.com/repos/$org/$repo/contributors"
|
||||
private const val dataUrl = "https://raw.githubusercontent.com/$org/$repo/builds/data.json"
|
||||
}
|
||||
}
|
||||
|
|
@ -1,67 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Juby210 & zt
|
||||
* Licensed under the Open Software License version 3.0
|
||||
*/
|
||||
|
||||
|
||||
package com.aliucord.manager.preferences
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.core.content.edit
|
||||
import kotlin.reflect.KProperty
|
||||
|
||||
lateinit var sharedPreferences: SharedPreferences
|
||||
|
||||
class Preference<T>(
|
||||
private val key: String,
|
||||
defaultValue: T,
|
||||
getter: SharedPreferences.(key: String, defaultValue: T) -> T?,
|
||||
private val setter: SharedPreferences.Editor.(key: String, newValue: T) -> Unit
|
||||
) {
|
||||
private val value = mutableStateOf(sharedPreferences.getter(key, defaultValue) ?: defaultValue)
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value.value
|
||||
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) = set(newValue)
|
||||
|
||||
fun get() = value.value
|
||||
|
||||
fun set(newValue: T) {
|
||||
if (value.value == newValue) return
|
||||
|
||||
value.value = newValue
|
||||
|
||||
sharedPreferences.edit { setter(key, newValue) }
|
||||
}
|
||||
}
|
||||
|
||||
fun boolPreference(
|
||||
key: String,
|
||||
defaultValue: Boolean = false
|
||||
) = Preference(
|
||||
key,
|
||||
defaultValue,
|
||||
SharedPreferences::getBoolean,
|
||||
SharedPreferences.Editor::putBoolean
|
||||
)
|
||||
|
||||
fun intPreference(
|
||||
key: String,
|
||||
defaultValue: Int = 0
|
||||
) = Preference(
|
||||
key,
|
||||
defaultValue,
|
||||
SharedPreferences::getInt,
|
||||
SharedPreferences.Editor::putInt
|
||||
)
|
||||
|
||||
fun stringPreference(
|
||||
key: String,
|
||||
defaultValue: String
|
||||
) = Preference(
|
||||
key,
|
||||
defaultValue,
|
||||
SharedPreferences::getString,
|
||||
SharedPreferences.Editor::putString
|
||||
)
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -3,26 +3,27 @@
|
|||
* Licensed under the Open Software License version 3.0
|
||||
*/
|
||||
|
||||
package com.aliucord.manager.ui.component.about
|
||||
package com.aliucord.manager.ui.component
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.grid.*
|
||||
import androidx.compose.foundation.shape.CircleShape
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.draw.clip
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import coil.compose.AsyncImage
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.model.github.GithubUser
|
||||
import com.aliucord.manager.network.dto.GithubUser
|
||||
|
||||
@Composable
|
||||
fun ContributorsCard(contributors: List<GithubUser>) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier
|
||||
.wrapContentHeight()
|
||||
.fillMaxWidth()
|
||||
) {
|
||||
ElevatedCard {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
|
|
@ -56,3 +57,17 @@ fun ContributorsCard(contributors: List<GithubUser>) {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun ContributorEntry(name: String) {
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
AsyncImage(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.clip(CircleShape)
|
||||
.clickable { uriHandler.openUri("https://github.com/$name") },
|
||||
model = "https://github.com/$name.png",
|
||||
contentDescription = name
|
||||
)
|
||||
}
|
||||
|
|
@ -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))
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
@ -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)
|
||||
)
|
||||
}
|
||||
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,34 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Juby210 & zt
|
||||
* Licensed under the Open Software License version 3.0
|
||||
*/
|
||||
|
||||
package com.aliucord.manager.ui.component.settings
|
||||
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import com.aliucord.manager.preferences.Preference
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PreferenceItem(
|
||||
icon: @Composable (() -> Unit)? = null,
|
||||
title: @Composable () -> Unit,
|
||||
description: @Composable (() -> Unit)? = null,
|
||||
preference: Preference<Boolean>
|
||||
) = ListItem(
|
||||
modifier = Modifier.clickable {
|
||||
preference.set(!preference.get())
|
||||
},
|
||||
leadingContent = icon,
|
||||
headlineText = title,
|
||||
supportingText = description,
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = preference.get(),
|
||||
onCheckedChange = preference::set
|
||||
)
|
||||
}
|
||||
)
|
||||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
)
|
||||
|
|
@ -23,9 +23,6 @@ sealed interface AppDestination : Destination {
|
|||
@Parcelize
|
||||
object Install : AppDestination
|
||||
|
||||
@Parcelize
|
||||
object Store : AppDestination
|
||||
|
||||
@Parcelize
|
||||
object About : AppDestination
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,42 +1,35 @@
|
|||
package com.aliucord.manager.ui.screen
|
||||
|
||||
import android.content.res.Configuration
|
||||
import androidx.compose.animation.*
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material.icons.filled.Info
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.DpOffset
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.ui.navigation.AppDestination
|
||||
import com.aliucord.manager.ui.navigation.HomeDestination
|
||||
import com.xinto.taxi.*
|
||||
import com.xinto.taxi.Taxi
|
||||
import com.xinto.taxi.rememberNavigator
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class, ExperimentalAnimationApi::class)
|
||||
@Composable
|
||||
fun MainRootScreen(navigator: BackstackNavigator<AppDestination>) {
|
||||
fun MainRootScreen(
|
||||
onClickInstall: () -> Unit,
|
||||
onClickAbout: () -> Unit,
|
||||
onClickSettings: () -> Unit
|
||||
) {
|
||||
val mainRootNavigator = rememberNavigator(HomeDestination.HOME)
|
||||
val currentDestination = mainRootNavigator.currentDestination
|
||||
val orientation = LocalConfiguration.current.orientation
|
||||
var isMenuExpanded by remember { mutableStateOf(false) }
|
||||
|
||||
Scaffold(
|
||||
modifier = Modifier
|
||||
.fillMaxSize(),
|
||||
topBar = {
|
||||
MediumTopAppBar(
|
||||
title = {
|
||||
Text(stringResource(mainRootNavigator.currentDestination.label))
|
||||
},
|
||||
title = { Text(stringResource(mainRootNavigator.currentDestination.label)) },
|
||||
actions = {
|
||||
val localUriHandler = LocalUriHandler.current
|
||||
|
||||
|
|
@ -47,125 +40,64 @@ fun MainRootScreen(navigator: BackstackNavigator<AppDestination>) {
|
|||
) {
|
||||
Icon(
|
||||
painter = painterResource(R.drawable.ic_discord),
|
||||
contentDescription = stringResource(R.string.support_server),
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
contentDescription = stringResource(R.string.support_server)
|
||||
)
|
||||
}
|
||||
|
||||
Box(
|
||||
modifier = Modifier.wrapContentSize(Alignment.TopEnd)
|
||||
) {
|
||||
IconButton(
|
||||
onClick = { isMenuExpanded = !isMenuExpanded }
|
||||
) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.MoreVert,
|
||||
contentDescription = stringResource(R.string.show_menu),
|
||||
tint = MaterialTheme.colorScheme.onBackground
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onClickAbout) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
contentDescription = stringResource(R.string.about)
|
||||
)
|
||||
}
|
||||
|
||||
DropdownMenu(
|
||||
offset = DpOffset(x = 12.dp, y = 0.dp),
|
||||
expanded = isMenuExpanded,
|
||||
onDismissRequest = { isMenuExpanded = false }
|
||||
) {
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
isMenuExpanded = false
|
||||
navigator.push(AppDestination.About)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Info,
|
||||
contentDescription = "About"
|
||||
)
|
||||
},
|
||||
text = { Text(stringResource(R.string.about)) }
|
||||
)
|
||||
|
||||
DropdownMenuItem(
|
||||
onClick = {
|
||||
isMenuExpanded = false
|
||||
navigator.push(AppDestination.Settings)
|
||||
},
|
||||
leadingIcon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = "Settings"
|
||||
)
|
||||
},
|
||||
text = { Text(stringResource(R.string.settings)) }
|
||||
)
|
||||
}
|
||||
IconButton(onClick = onClickSettings) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Settings,
|
||||
contentDescription = stringResource(R.string.settings)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
},
|
||||
bottomBar = {
|
||||
if (orientation == Configuration.ORIENTATION_PORTRAIT) {
|
||||
NavigationBar {
|
||||
val currentDestination = mainRootNavigator.currentDestination
|
||||
|
||||
HomeDestination.values().forEach { destination ->
|
||||
NavigationBarItem(
|
||||
selected = currentDestination == destination,
|
||||
icon = {
|
||||
if (currentDestination == destination) Icon(
|
||||
destination.selectedIcon,
|
||||
stringResource(destination.label)
|
||||
) else Icon(destination.unselectedIcon, stringResource(destination.label))
|
||||
},
|
||||
label = { Text(stringResource(destination.label)) },
|
||||
onClick = { mainRootNavigator.replace(destination) }
|
||||
)
|
||||
}
|
||||
NavigationBar {
|
||||
HomeDestination.values().forEach { destination ->
|
||||
NavigationBarItem(
|
||||
selected = currentDestination == destination,
|
||||
icon = {
|
||||
if (currentDestination == destination) {
|
||||
Icon(
|
||||
imageVector = destination.selectedIcon,
|
||||
contentDescription = stringResource(destination.label)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
imageVector = destination.unselectedIcon,
|
||||
contentDescription = stringResource(destination.label)
|
||||
)
|
||||
}
|
||||
},
|
||||
label = { Text(stringResource(destination.label)) },
|
||||
onClick = { mainRootNavigator.replace(destination) }
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
) { paddingValues ->
|
||||
Row(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
.padding(horizontal = 12.dp, vertical = 14.dp)
|
||||
) {
|
||||
if (orientation == Configuration.ORIENTATION_LANDSCAPE) {
|
||||
NavRail(
|
||||
currentDestination = currentDestination,
|
||||
onClickDestination = mainRootNavigator::replace
|
||||
Taxi(
|
||||
modifier = Modifier.padding(paddingValues),
|
||||
navigator = mainRootNavigator,
|
||||
transitionSpec = { fadeIn() with fadeOut() }
|
||||
) { destination ->
|
||||
when (destination) {
|
||||
HomeDestination.HOME -> HomeScreen(
|
||||
onClickInstall = onClickInstall
|
||||
)
|
||||
}
|
||||
|
||||
Taxi(
|
||||
modifier = Modifier.weight(1f, true),
|
||||
navigator = mainRootNavigator,
|
||||
transitionSpec = { fadeIn() with fadeOut() }
|
||||
) { destination ->
|
||||
when (destination) {
|
||||
HomeDestination.HOME -> HomeScreen(navigator = navigator)
|
||||
HomeDestination.PLUGINS -> PluginsScreen(navigator = navigator)
|
||||
}
|
||||
HomeDestination.PLUGINS -> PluginsScreen()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun NavRail(
|
||||
currentDestination: HomeDestination,
|
||||
onClickDestination: (HomeDestination) -> Unit
|
||||
) = NavigationRail {
|
||||
HomeDestination.values().forEach { destination ->
|
||||
NavigationRailItem(
|
||||
selected = currentDestination == destination,
|
||||
icon = {
|
||||
if (currentDestination == destination) Icon(
|
||||
destination.selectedIcon,
|
||||
stringResource(destination.label)
|
||||
) else Icon(destination.unselectedIcon, stringResource(destination.label))
|
||||
},
|
||||
label = { Text(stringResource(destination.label)) },
|
||||
onClick = { onClickDestination(destination) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,63 +12,56 @@ import androidx.compose.foundation.clickable
|
|||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Delete
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalUriHandler
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.*
|
||||
import androidx.compose.ui.text.font.FontWeight
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.viewmodel.compose.viewModel
|
||||
import androidx.paging.*
|
||||
import androidx.paging.LoadState
|
||||
import androidx.paging.compose.collectAsLazyPagingItems
|
||||
import androidx.paging.compose.items
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.model.github.Commit
|
||||
import com.aliucord.manager.preferences.Prefs
|
||||
import com.aliucord.manager.ui.component.installer.DownloadMethod
|
||||
import com.aliucord.manager.ui.component.installer.InstallerDialog
|
||||
import com.aliucord.manager.ui.navigation.AppDestination
|
||||
import com.aliucord.manager.ui.component.DownloadMethod
|
||||
import com.aliucord.manager.ui.component.InstallerDialog
|
||||
import com.aliucord.manager.ui.viewmodel.HomeViewModel
|
||||
import com.aliucord.manager.util.Github
|
||||
import com.xinto.taxi.BackstackNavigator
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
|
||||
val viewModel: HomeViewModel = viewModel()
|
||||
|
||||
fun HomeScreen(
|
||||
viewModel: HomeViewModel = getViewModel(),
|
||||
onClickInstall: () -> Unit
|
||||
) {
|
||||
var showOptionsDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showOptionsDialog) {
|
||||
val filePicker = rememberLauncherForActivityResult(ActivityResultContracts.OpenDocument()) { uri ->
|
||||
if (uri == null) return@rememberLauncherForActivityResult
|
||||
|
||||
navigator.push(AppDestination.Install)
|
||||
onClickInstall()
|
||||
}
|
||||
|
||||
InstallerDialog(
|
||||
onDismissRequest = { showOptionsDialog = false },
|
||||
onConfirm = { method ->
|
||||
if (method == DownloadMethod.DOWNLOAD) {
|
||||
navigator.push(AppDestination.Install)
|
||||
} else {
|
||||
filePicker.launch(arrayOf("application/octet-stream"))
|
||||
when (method) {
|
||||
DownloadMethod.DOWNLOAD -> onClickInstall()
|
||||
DownloadMethod.SELECT -> filePicker.launch(arrayOf("application/octet-stream"))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
modifier = Modifier.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
ElevatedCard(
|
||||
modifier = Modifier.wrapContentHeight()
|
||||
) {
|
||||
ElevatedCard {
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
|
|
@ -77,52 +70,53 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
|
|||
) {
|
||||
Column {
|
||||
Text(
|
||||
"Aliucord${Prefs.packageName.get().let { if (it != "com.aliucord") " ($it)" else "" }}",
|
||||
text = "Aliucord${viewModel.preferences.packageName.let { if (it != "com.aliucord") " ($it)" else "" }}",
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Text(buildAnnotatedString {
|
||||
append("Supported version: ")
|
||||
Text(
|
||||
buildAnnotatedString {
|
||||
append("Supported version: ")
|
||||
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(viewModel.supportedVersion)
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(viewModel.supportedVersion)
|
||||
}
|
||||
|
||||
append("\nInstalled version: ")
|
||||
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(viewModel.installedVersion)
|
||||
}
|
||||
}
|
||||
|
||||
append("\nInstalled version: ")
|
||||
|
||||
withStyle(SpanStyle(fontWeight = FontWeight.Bold)) {
|
||||
append(viewModel.installedVersion)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
val (drawable, description) = when (viewModel.installedVersion) {
|
||||
"-" -> R.drawable.ic_download_24dp to R.string.install
|
||||
viewModel.supportedVersion -> R.drawable.ic_reinstall_24dp to R.string.reinstall
|
||||
else -> R.drawable.ic_update_24dp to R.string.update
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
val (icon, description) = when (viewModel.installedVersion) {
|
||||
"-" -> Icons.Default.Download to R.string.install
|
||||
viewModel.supportedVersion -> Icons.Default.Update to R.string.reinstall
|
||||
else -> Icons.Default.Update to R.string.update
|
||||
}
|
||||
|
||||
Row(
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
Button(
|
||||
modifier = Modifier.weight(1f, true),
|
||||
onClick = {
|
||||
if (Prefs.devMode.get()) {
|
||||
if (viewModel.preferences.devMode) {
|
||||
showOptionsDialog = true
|
||||
} else {
|
||||
navigator.push(AppDestination.Install)
|
||||
onClickInstall()
|
||||
}
|
||||
}
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
painter = painterResource(drawable),
|
||||
imageVector = icon,
|
||||
contentDescription = stringResource(description),
|
||||
tint = MaterialTheme.colorScheme.onPrimary
|
||||
)
|
||||
|
|
@ -132,9 +126,7 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
|
|||
}
|
||||
|
||||
if (viewModel.installedVersion != "-") {
|
||||
|
||||
Row(
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
horizontalArrangement = Arrangement.spacedBy(12.dp)
|
||||
) {
|
||||
CompositionLocalProvider(
|
||||
|
|
@ -142,14 +134,12 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
|
|||
) {
|
||||
FilledTonalButton(
|
||||
modifier = Modifier.weight(1f),
|
||||
onClick = {
|
||||
viewModel.uninstallAliucord()
|
||||
}
|
||||
onClick = viewModel::uninstallAliucord
|
||||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = "Uninstall"
|
||||
contentDescription = stringResource(R.string.uninstall)
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -159,8 +149,8 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
|
|||
) {
|
||||
Icon(
|
||||
modifier = Modifier.padding(8.dp),
|
||||
painter = painterResource(R.drawable.ic_launch_24dp),
|
||||
contentDescription = "Launch"
|
||||
imageVector = Icons.Default.Launch,
|
||||
contentDescription = stringResource(R.string.launch)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -169,89 +159,60 @@ fun HomeScreen(navigator: BackstackNavigator<AppDestination>) {
|
|||
}
|
||||
}
|
||||
}
|
||||
val pager = remember {
|
||||
Pager(
|
||||
PagingConfig(
|
||||
pageSize = 30,
|
||||
enablePlaceholders = true,
|
||||
maxSize = 200
|
||||
)
|
||||
) {
|
||||
object : PagingSource<Int, Commit>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Commit> {
|
||||
val pageNumber = params.key ?: 0
|
||||
|
||||
val response = Github.getCommits("page" to pageNumber.toString())
|
||||
val prevKey = if (pageNumber > 0) pageNumber - 1 else null
|
||||
val nextKey = if (response.isNotEmpty()) pageNumber + 1 else null
|
||||
|
||||
return LoadResult.Page(
|
||||
data = response,
|
||||
prevKey = prevKey,
|
||||
nextKey = nextKey
|
||||
)
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, Commit>) = state.anchorPosition?.let {
|
||||
state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ElevatedCard(
|
||||
Modifier
|
||||
.fillMaxWidth()
|
||||
.weight(1f)
|
||||
modifier = Modifier.padding(bottom = 16.dp)
|
||||
) {
|
||||
Column(
|
||||
modifier = Modifier.padding(16.dp, 16.dp, 16.dp),
|
||||
modifier = Modifier
|
||||
.padding(16.dp)
|
||||
.weight(1f, true)
|
||||
.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
Row(
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
Text(
|
||||
text = stringResource(R.string.commits),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
val lazyPagingItems = viewModel.commits.collectAsLazyPagingItems()
|
||||
|
||||
Text(
|
||||
text = stringResource(R.string.commits),
|
||||
style = MaterialTheme.typography.titleLarge,
|
||||
color = MaterialTheme.colorScheme.primary
|
||||
)
|
||||
|
||||
Spacer(Modifier.weight(1f, true))
|
||||
}
|
||||
val lazyPagingItems = pager.flow.collectAsLazyPagingItems()
|
||||
LazyColumn(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth(),
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
verticalArrangement = Arrangement.spacedBy(14.dp)
|
||||
) {
|
||||
if (lazyPagingItems.loadState.refresh == LoadState.Loading) item {
|
||||
Text(
|
||||
text = stringResource(R.string.paging_initial_load),
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
)
|
||||
if (lazyPagingItems.loadState.refresh == LoadState.Loading) {
|
||||
item {
|
||||
Text(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally),
|
||||
text = stringResource(R.string.paging_initial_load)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
items(lazyPagingItems) { commitData ->
|
||||
if (commitData == null) return@items
|
||||
|
||||
val localUriHandler = LocalUriHandler.current
|
||||
val uriHandler = LocalUriHandler.current
|
||||
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { localUriHandler.openUri(commitData.htmlUrl) },
|
||||
modifier = Modifier.clickable { uriHandler.openUri(commitData.htmlUrl) },
|
||||
overlineText = { Text(commitData.sha.substring(0, 7)) },
|
||||
headlineText = { Text("${commitData.commit.message.split("\n").first()} - ${commitData.author.name}") }
|
||||
)
|
||||
}
|
||||
|
||||
if (lazyPagingItems.loadState.append == LoadState.Loading) item {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
)
|
||||
if (lazyPagingItems.loadState.append == LoadState.Loading) {
|
||||
item {
|
||||
CircularProgressIndicator(
|
||||
modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
.wrapContentWidth(Alignment.CenterHorizontally)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -14,50 +14,100 @@ import androidx.compose.foundation.text.KeyboardOptions
|
|||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.*
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.platform.LocalFocusManager
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.input.ImeAction
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.compose.ui.window.Dialog
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.model.Plugin
|
||||
import com.aliucord.manager.ui.component.plugins.Changelog
|
||||
import com.aliucord.manager.ui.component.plugins.PluginCard
|
||||
import com.aliucord.manager.ui.navigation.AppDestination
|
||||
import com.xinto.taxi.BackstackNavigator
|
||||
import com.aliucord.manager.ui.viewmodel.PluginsViewModel
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun PluginsScreen(navigator: BackstackNavigator<AppDestination>) {
|
||||
fun PluginsScreen(
|
||||
viewModel: PluginsViewModel = getViewModel()
|
||||
) {
|
||||
if (viewModel.showUninstallDialog) {
|
||||
val plugin = viewModel.selectedPlugin!!
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = viewModel::hideUninstallDialog,
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(R.string.uninstall)
|
||||
)
|
||||
},
|
||||
title = {
|
||||
Text("${stringResource(R.string.uninstall)} ${plugin.manifest.name}")
|
||||
},
|
||||
text = {
|
||||
Text(stringResource(R.string.delete_plugin_body))
|
||||
},
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = { viewModel.uninstallPlugin(plugin) }
|
||||
) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(
|
||||
onClick = viewModel::hideUninstallDialog,
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer
|
||||
)
|
||||
) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
if (viewModel.showChangelogDialog) {
|
||||
Changelog(
|
||||
plugin = viewModel.selectedPlugin!!,
|
||||
onDismiss = viewModel::hideChangelogDialog
|
||||
)
|
||||
}
|
||||
|
||||
Column(
|
||||
modifier = Modifier.fillMaxSize(),
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(horizontal = 16.dp),
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
var plugins by remember { mutableStateOf(Plugin.loadAll()) }
|
||||
val focusManager = LocalFocusManager.current
|
||||
var search by remember { mutableStateOf("") }
|
||||
val search = viewModel.search
|
||||
|
||||
if (viewModel.plugins.isNotEmpty()) {
|
||||
val focusManager = LocalFocusManager.current
|
||||
|
||||
if (plugins.isNotEmpty()) {
|
||||
OutlinedTextField(
|
||||
maxLines = 1,
|
||||
value = search,
|
||||
onValueChange = { search = it },
|
||||
singleLine = true,
|
||||
modifier = Modifier.fillMaxWidth(),
|
||||
maxLines = 1,
|
||||
value = viewModel.search,
|
||||
onValueChange = viewModel::search,
|
||||
singleLine = true,
|
||||
shape = RoundedCornerShape(12.dp),
|
||||
label = { Text(stringResource(R.string.search)) },
|
||||
trailingIcon = {
|
||||
if (search.isNotBlank()) {
|
||||
IconButton(onClick = { search = "" }) {
|
||||
if (viewModel.search.isNotBlank()) {
|
||||
IconButton(onClick = viewModel::clearSearch) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Close,
|
||||
contentDescription = stringResource(R.string.clear)
|
||||
)
|
||||
}
|
||||
} else {
|
||||
Icon(Icons.Default.Search, stringResource(R.string.search_plugins))
|
||||
Icon(
|
||||
imageVector = Icons.Default.Search,
|
||||
contentDescription = stringResource(R.string.search_plugins)
|
||||
)
|
||||
}
|
||||
},
|
||||
keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Search),
|
||||
|
|
@ -67,59 +117,19 @@ fun PluginsScreen(navigator: BackstackNavigator<AppDestination>) {
|
|||
LazyColumn(
|
||||
verticalArrangement = Arrangement.spacedBy(8.dp)
|
||||
) {
|
||||
items(plugins.filter { plugin ->
|
||||
plugin.manifest.run {
|
||||
name.contains(search, true)
|
||||
|| description.contains(search, true)
|
||||
|| authors.any { a -> a.name.contains(search, true) }
|
||||
}
|
||||
}) { plugin ->
|
||||
var showDeleteDialog by remember { mutableStateOf(false) }
|
||||
var showChangelogDialog by remember { mutableStateOf(false) }
|
||||
|
||||
if (showDeleteDialog) AlertDialog(
|
||||
onDismissRequest = { showDeleteDialog = false },
|
||||
icon = {
|
||||
Icon(
|
||||
imageVector = Icons.Default.Delete,
|
||||
contentDescription = stringResource(R.string.uninstall_plugin, plugin.manifest.name)
|
||||
)
|
||||
},
|
||||
title = { Text(stringResource(R.string.uninstall_plugin, plugin.manifest.name)) },
|
||||
text = { Text(stringResource(R.string.delete_plugin_body)) },
|
||||
confirmButton = {
|
||||
Button(
|
||||
onClick = {
|
||||
showDeleteDialog = false
|
||||
plugin.file.delete()
|
||||
plugins = plugins.filter { it != plugin }
|
||||
}
|
||||
) {
|
||||
Text(stringResource(android.R.string.ok))
|
||||
}
|
||||
},
|
||||
dismissButton = {
|
||||
Button(
|
||||
onClick = { showDeleteDialog = false },
|
||||
colors = ButtonDefaults.buttonColors(
|
||||
containerColor = MaterialTheme.colorScheme.secondaryContainer,
|
||||
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
)
|
||||
) {
|
||||
Text(stringResource(android.R.string.cancel))
|
||||
}
|
||||
items(
|
||||
viewModel.plugins.filter { plugin ->
|
||||
plugin.manifest.run {
|
||||
name.contains(search, true)
|
||||
|| description.contains(search, true)
|
||||
|| authors.any { (name) -> name.contains(search, true) }
|
||||
}
|
||||
)
|
||||
|
||||
if (showChangelogDialog) Dialog(
|
||||
onDismissRequest = { showChangelogDialog = false },
|
||||
content = { Changelog(plugin, onDismiss = { showChangelogDialog = false }) }
|
||||
)
|
||||
|
||||
}
|
||||
) { plugin ->
|
||||
PluginCard(
|
||||
plugin = plugin,
|
||||
onDelete = { showDeleteDialog = true },
|
||||
onShowChangelog = { showChangelogDialog = true }
|
||||
onClickDelete = { viewModel.showUninstallDialog(plugin) },
|
||||
onClickShowChangelog = { viewModel.showChangelogDialog(plugin) }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,89 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Juby210 & zt
|
||||
* Licensed under the Open Software License version 3.0
|
||||
*/
|
||||
|
||||
package com.aliucord.manager.ui.screen
|
||||
|
||||
import androidx.compose.foundation.layout.*
|
||||
import androidx.compose.foundation.lazy.LazyColumn
|
||||
import androidx.compose.foundation.lazy.items
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.NavigateBefore
|
||||
import androidx.compose.material3.*
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.unit.dp
|
||||
import com.aliucord.manager.R
|
||||
import com.aliucord.manager.util.httpClient
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.serialization.Serializable
|
||||
|
||||
@Serializable
|
||||
private data class Tree(
|
||||
val tree: List<File>
|
||||
) {
|
||||
@Serializable
|
||||
data class File(
|
||||
val mode: String,
|
||||
val sha: String
|
||||
)
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@Composable
|
||||
fun StoreScreen(onClickBack: () -> Unit) {
|
||||
val plugins = remember { mutableStateListOf<Tree.File>() }
|
||||
// TODO: Display cards with option to install plugin
|
||||
Scaffold(
|
||||
topBar = {
|
||||
MediumTopAppBar(
|
||||
title = { Text(stringResource(R.string.store)) },
|
||||
navigationIcon = {
|
||||
IconButton(onClick = onClickBack) {
|
||||
Icon(
|
||||
imageVector = Icons.Default.NavigateBefore,
|
||||
contentDescription = stringResource(R.string.back)
|
||||
)
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
) { paddingValues ->
|
||||
Column(
|
||||
modifier = Modifier
|
||||
.padding(paddingValues)
|
||||
) {
|
||||
if (plugins.isEmpty()) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.fillMaxSize()
|
||||
.padding(12.dp),
|
||||
contentAlignment = Alignment.Center,
|
||||
content = { CircularProgressIndicator() }
|
||||
)
|
||||
|
||||
LaunchedEffect(Unit) {
|
||||
launch(Dispatchers.IO) {
|
||||
val content = httpClient.get("https://api.github.com/repos/DiamondMiner88/AllACPlugins/git/trees/master") {
|
||||
header("accept", "application/json")
|
||||
}.body<Tree>()
|
||||
|
||||
plugins.addAll(content.tree.filter { it.mode == "160000" })
|
||||
}
|
||||
}
|
||||
} else {
|
||||
LazyColumn {
|
||||
items(plugins) { item ->
|
||||
Text("SHA: ${item.sha}", style = MaterialTheme.typography.labelSmall)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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
|
||||
|
|
@ -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")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
@ -3,29 +3,18 @@ package com.aliucord.manager.ui.viewmodel
|
|||
import androidx.compose.runtime.mutableStateListOf
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aliucord.manager.model.github.GithubUser
|
||||
import com.aliucord.manager.util.Github
|
||||
import com.aliucord.manager.util.httpClient
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import com.aliucord.manager.network.dto.GithubUser
|
||||
import com.aliucord.manager.domain.repository.GithubRepository
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class AboutViewModel : ViewModel() {
|
||||
|
||||
class AboutViewModel(
|
||||
private val githubRepository: GithubRepository
|
||||
) : ViewModel() {
|
||||
val contributors = mutableStateListOf<GithubUser>()
|
||||
|
||||
fun load() {
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
val githubContributors = httpClient.get(Github.contributorsUrl).body<List<GithubUser>>()
|
||||
.sortedByDescending {
|
||||
it.contributions
|
||||
}
|
||||
contributors.addAll(githubContributors)
|
||||
contributors.addAll(githubRepository.getContributors())
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
load()
|
||||
}
|
||||
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,18 +5,21 @@ import android.content.Intent
|
|||
import android.net.Uri
|
||||
import android.widget.Toast
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aliucord.manager.model.github.Version
|
||||
import com.aliucord.manager.preferences.Prefs
|
||||
import com.aliucord.manager.util.*
|
||||
import io.ktor.client.request.*
|
||||
import io.ktor.client.statement.*
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.serialization.decodeFromString
|
||||
import androidx.paging.*
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.domain.repository.GithubRepository
|
||||
import com.aliucord.manager.network.dto.Commit
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
||||
private val packageManager = getApplication<Application>().packageManager
|
||||
class HomeViewModel(
|
||||
private val application: Application,
|
||||
private val githubRepository: GithubRepository,
|
||||
val preferences: PreferencesManager
|
||||
) : ViewModel() {
|
||||
private val packageManager = application.packageManager
|
||||
|
||||
var supportedVersion by mutableStateOf("")
|
||||
private set
|
||||
|
|
@ -24,43 +27,64 @@ class HomeViewModel(application: Application) : AndroidViewModel(application) {
|
|||
var installedVersion by mutableStateOf("-")
|
||||
private set
|
||||
|
||||
private fun load() {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val version = json.decodeFromString<Version>(httpClient.get(Github.dataUrl).bodyAsText())
|
||||
val commits = Pager(PagingConfig(pageSize = 30)) {
|
||||
object : PagingSource<Int, Commit>() {
|
||||
override suspend fun load(params: LoadParams<Int>): LoadResult<Int, Commit> {
|
||||
val page = params.key ?: 0
|
||||
|
||||
withContext(Dispatchers.Main) {
|
||||
supportedVersion = "${version.versionName} - " + when (version.versionCode[3]) {
|
||||
'0' -> "Stable"
|
||||
'1' -> "Beta"
|
||||
'2' -> "Alpha"
|
||||
else -> throw NoWhenBranchMatchedException()
|
||||
}
|
||||
installedVersion = try {
|
||||
packageManager.getPackageInfo(Prefs.packageName.get(), 0).versionName
|
||||
} catch (th: Throwable) {
|
||||
"-"
|
||||
}
|
||||
val response = githubRepository.getCommits(page)
|
||||
val prevKey = if (page > 0) page - 1 else null
|
||||
val nextKey = if (response.isNotEmpty()) page + 1 else null
|
||||
|
||||
return LoadResult.Page(
|
||||
data = response,
|
||||
prevKey = prevKey,
|
||||
nextKey = nextKey
|
||||
)
|
||||
}
|
||||
|
||||
override fun getRefreshKey(state: PagingState<Int, Commit>) = state.anchorPosition?.let {
|
||||
state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1)
|
||||
}
|
||||
}
|
||||
}.flow.cachedIn(viewModelScope)
|
||||
|
||||
init {
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
val version = githubRepository.getVersion()
|
||||
|
||||
supportedVersion = "${version.versionName} - " + when (version.versionCode[3]) {
|
||||
'0' -> "Stable"
|
||||
'1' -> "Beta"
|
||||
'2' -> "Alpha"
|
||||
else -> throw NoWhenBranchMatchedException()
|
||||
}
|
||||
|
||||
installedVersion = try {
|
||||
@Suppress("DEPRECATION")
|
||||
packageManager.getPackageInfo(preferences.packageName, 0).versionName
|
||||
} catch (th: Throwable) {
|
||||
"-"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun launchAliucord() {
|
||||
packageManager.getLaunchIntentForPackage(Prefs.packageName.get())?.let {
|
||||
getApplication<Application>().startActivity(it)
|
||||
} ?: Toast.makeText(getApplication(), "Failed to launch Aliucord", Toast.LENGTH_LONG)
|
||||
.show()
|
||||
val launchIntent = packageManager.getLaunchIntentForPackage(preferences.packageName)
|
||||
|
||||
if (launchIntent != null) {
|
||||
application.startActivity(launchIntent)
|
||||
} else {
|
||||
Toast.makeText(application, "Failed to launch Aliucord", Toast.LENGTH_LONG).show()
|
||||
}
|
||||
}
|
||||
|
||||
fun uninstallAliucord() {
|
||||
val packageURI = Uri.parse("package:${Prefs.packageName.get()}")
|
||||
val packageURI = Uri.parse("package:${preferences.packageName}")
|
||||
val uninstallIntent = Intent(Intent.ACTION_DELETE, packageURI).apply {
|
||||
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
}
|
||||
|
||||
getApplication<Application>().startActivity(uninstallIntent)
|
||||
}
|
||||
|
||||
init {
|
||||
load()
|
||||
application.startActivity(uninstallIntent)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,21 +8,29 @@ import android.content.pm.PackageInstaller
|
|||
import android.os.Build
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aliucord.manager.domain.manager.DownloadManager
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.installer.service.InstallService
|
||||
import com.aliucord.manager.installer.util.DownloadUtils
|
||||
import com.aliucord.manager.installer.util.ManifestPatcher
|
||||
import com.aliucord.manager.util.Signer
|
||||
import com.aliucord.manager.installer.util.Signer
|
||||
import com.android.zipflinger.*
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.*
|
||||
import kotlinx.coroutines.flow.MutableSharedFlow
|
||||
import kotlinx.coroutines.flow.asSharedFlow
|
||||
import kotlinx.coroutines.launch
|
||||
import java.io.File
|
||||
import java.util.zip.Deflater
|
||||
|
||||
class InstallViewModel(application: Application) : AndroidViewModel(application) {
|
||||
class InstallViewModel(
|
||||
private val application: Application,
|
||||
private val downloadManager: DownloadManager,
|
||||
private val preferences: PreferencesManager
|
||||
) : ViewModel() {
|
||||
private val assetManager = application.assets
|
||||
private val externalCacheDir = application.externalCacheDir!!
|
||||
private val packageInstaller = application.packageManager.packageInstaller
|
||||
|
||||
private val _returnToHome = MutableSharedFlow<Boolean>()
|
||||
val returnToHome = _returnToHome.asSharedFlow()
|
||||
|
||||
|
|
@ -31,39 +39,39 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
|
|||
var log by mutableStateOf("")
|
||||
private set
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
startInstallation()
|
||||
}
|
||||
}
|
||||
|
||||
@RequiresApi(Build.VERSION_CODES.O)
|
||||
fun startInstallation() {
|
||||
suspend fun startInstallation() {
|
||||
if (installationRunning) return
|
||||
|
||||
viewModelScope.launch(Dispatchers.IO) {
|
||||
withContext(Dispatchers.IO) {
|
||||
installationRunning = true
|
||||
|
||||
val context = getApplication<Application>().applicationContext
|
||||
val assetManager = context.assets
|
||||
val externalCacheDir = context.externalCacheDir!!
|
||||
val arch = Build.SUPPORTED_ABIS.first()
|
||||
val supportedVersion = "124206"
|
||||
val supportedVersion = preferences.version
|
||||
|
||||
val baseApk = externalCacheDir.resolve("base-${supportedVersion}.apk").also { file ->
|
||||
val archiveInfo = context.packageManager.getPackageArchiveInfo(file.path, 0)
|
||||
@Suppress("DEPRECATION")
|
||||
val archiveInfo = application.packageManager.getPackageArchiveInfo(file.path, 0)
|
||||
|
||||
if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also run { log += "Using cached base APK\n"}
|
||||
if (archiveInfo?.versionCode.toString().startsWith(supportedVersion)) return@also run { log += "Using cached base APK\n" }
|
||||
|
||||
log += "Downloading Discord APK... "
|
||||
|
||||
DownloadUtils.downloadDiscordApk(context, supportedVersion)
|
||||
|
||||
downloadManager.downloadDiscordApk(supportedVersion)
|
||||
log += "Done\n"
|
||||
}
|
||||
|
||||
val libArch = arch.replace("-v", "_v")
|
||||
val libsApk = externalCacheDir.resolve("config.$libArch-${supportedVersion}.apk").also { file ->
|
||||
if (file.exists()) return@also run { log += "Used cached libs APK\n" }
|
||||
if (file.exists()) return@also run { log += "Using cached libs APK\n" }
|
||||
|
||||
log += "Downloading libs APK... "
|
||||
|
||||
DownloadUtils.downloadSplit(context, supportedVersion, "config.$libArch")
|
||||
|
||||
downloadManager.downloadSplit(supportedVersion, "config.$libArch")
|
||||
log += "Done\n"
|
||||
}
|
||||
|
||||
|
|
@ -71,23 +79,19 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
|
|||
if (file.exists()) return@also run { log += "Using cached locale APK\n" }
|
||||
|
||||
log += "Downloading locale APK... "
|
||||
|
||||
DownloadUtils.downloadSplit(context, supportedVersion, "config.en")
|
||||
|
||||
downloadManager.downloadSplit(supportedVersion, "config.en")
|
||||
log += "Done\n"
|
||||
}
|
||||
|
||||
val xxhdpiApk = externalCacheDir.resolve("config.xxhdpi-${supportedVersion}.apk").also { file ->
|
||||
if (file.exists()) return@also run { log += "Using cached drawables APK\n"}
|
||||
if (file.exists()) return@also run { log += "Using cached drawables APK\n" }
|
||||
|
||||
log += "Downloading drawables APK... "
|
||||
|
||||
DownloadUtils.downloadSplit(context, supportedVersion, "config.xxhdpi")
|
||||
|
||||
downloadManager.downloadSplit(supportedVersion, "config.xxhdpi")
|
||||
log += "Done\n"
|
||||
}
|
||||
|
||||
val apks = listOf(baseApk, libsApk, localeApk, xxhdpiApk)
|
||||
val apks = arrayOf(baseApk, libsApk, localeApk, xxhdpiApk)
|
||||
|
||||
log += "Patching manifest...\n"
|
||||
|
||||
|
|
@ -97,7 +101,14 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
|
|||
.array()
|
||||
|
||||
ZipArchive(baseApk.toPath()).use { zip ->
|
||||
val source = BytesSource(ManifestPatcher.patchManifest(byteArray), "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION)
|
||||
val patchedBytesArray = ManifestPatcher.patchManifest(
|
||||
manifestBytes = byteArray,
|
||||
packageName = preferences.packageName,
|
||||
appName = preferences.appName,
|
||||
debuggable = preferences.debuggable,
|
||||
)
|
||||
|
||||
val source = BytesSource(patchedBytesArray, "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION)
|
||||
|
||||
zip.delete("AndroidManifest.xml")
|
||||
zip.add(source)
|
||||
|
|
@ -109,15 +120,18 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
|
|||
.array()
|
||||
|
||||
ZipArchive(apk.toPath()).use { zip ->
|
||||
val source = BytesSource(ManifestPatcher.renamePackage(byteArray), "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION)
|
||||
val patchedBytes = ManifestPatcher.renamePackage(byteArray, preferences.packageName)
|
||||
val source = BytesSource(patchedBytes, "AndroidManifest.xml", Deflater.DEFAULT_COMPRESSION)
|
||||
|
||||
zip.delete("AndroidManifest.xml")
|
||||
zip.add(source)
|
||||
}
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
log += "An error has occurred"
|
||||
log += "An error has occurred: ${e.message}\n"
|
||||
e.printStackTrace()
|
||||
|
||||
return@withContext
|
||||
}
|
||||
|
||||
log += "Patching libraries...\n"
|
||||
|
|
@ -141,7 +155,7 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
|
|||
|
||||
log += "Done\n"
|
||||
|
||||
installApks(*apks.toTypedArray())
|
||||
installApks(*apks)
|
||||
|
||||
_returnToHome.emit(true)
|
||||
installationRunning = false
|
||||
|
|
@ -149,9 +163,7 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
|
|||
}
|
||||
|
||||
private fun installApks(vararg apks: File) {
|
||||
val context = getApplication<Application>().applicationContext
|
||||
val params = PackageInstaller.SessionParams(PackageInstaller.SessionParams.MODE_FULL_INSTALL)
|
||||
val packageInstaller = context.packageManager.packageInstaller
|
||||
val sessionId = packageInstaller.createSession(params)
|
||||
val session = packageInstaller.openSession(sessionId)
|
||||
|
||||
|
|
@ -163,13 +175,13 @@ class InstallViewModel(application: Application) : AndroidViewModel(application)
|
|||
}
|
||||
}
|
||||
|
||||
val callbackIntent = Intent(context, InstallService::class.java)
|
||||
val callbackIntent = Intent(application, InstallService::class.java)
|
||||
|
||||
@SuppressLint("UnspecifiedImmutableFlag")
|
||||
val contentIntent = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
PendingIntent.getService(context, 0, callbackIntent, PendingIntent.FLAG_MUTABLE)
|
||||
PendingIntent.getService(application, 0, callbackIntent, PendingIntent.FLAG_MUTABLE)
|
||||
} else {
|
||||
PendingIntent.getService(context, 0, callbackIntent, 0)
|
||||
PendingIntent.getService(application, 0, callbackIntent, 0)
|
||||
}
|
||||
|
||||
session.commit(contentIntent.intentSender)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,85 @@
|
|||
package com.aliucord.manager.ui.viewmodel
|
||||
|
||||
import android.util.Log
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import com.aliucord.manager.BuildConfig
|
||||
import com.aliucord.manager.aliucordDir
|
||||
import com.aliucord.manager.domain.model.Plugin
|
||||
import kotlinx.coroutines.*
|
||||
|
||||
class PluginsViewModel : ViewModel() {
|
||||
var showChangelogDialog by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var showUninstallDialog by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
var search by mutableStateOf("")
|
||||
private set
|
||||
|
||||
var selectedPlugin by mutableStateOf<Plugin?>(null)
|
||||
private set
|
||||
|
||||
val plugins = mutableStateListOf<Plugin>()
|
||||
|
||||
init {
|
||||
viewModelScope.launch {
|
||||
loadPlugins()
|
||||
}
|
||||
}
|
||||
|
||||
fun search(search: String) {
|
||||
this.search = search
|
||||
}
|
||||
|
||||
fun clearSearch() {
|
||||
search = ""
|
||||
}
|
||||
|
||||
fun showChangelogDialog(plugin: Plugin) {
|
||||
showChangelogDialog = true
|
||||
selectedPlugin = plugin
|
||||
}
|
||||
|
||||
fun hideChangelogDialog() {
|
||||
showChangelogDialog = false
|
||||
}
|
||||
|
||||
fun showUninstallDialog(plugin: Plugin) {
|
||||
showUninstallDialog = true
|
||||
selectedPlugin = plugin
|
||||
}
|
||||
|
||||
fun hideUninstallDialog() {
|
||||
showUninstallDialog = false
|
||||
}
|
||||
|
||||
fun uninstallPlugin(plugin: Plugin) {
|
||||
plugin.file.delete()
|
||||
plugins.remove(plugin)
|
||||
}
|
||||
|
||||
private suspend fun loadPlugins(): Unit = withContext(Dispatchers.IO) {
|
||||
val pluginsDir = aliucordDir.resolve("plugins")
|
||||
|
||||
if (!pluginsDir.exists() && !pluginsDir.mkdirs()) {
|
||||
Log.e(BuildConfig.TAG, "Failed to create plugins dir. Missing Permissions?")
|
||||
return@withContext
|
||||
}
|
||||
|
||||
val files = pluginsDir.listFiles { file -> file.extension == "zip" } ?: return@withContext
|
||||
|
||||
plugins.addAll(
|
||||
files.mapNotNull { file ->
|
||||
try {
|
||||
Plugin(file)
|
||||
} catch (e: Exception) {
|
||||
Log.e(BuildConfig.TAG, "Failed to load plugin ${file.nameWithoutExtension}", e)
|
||||
null
|
||||
}
|
||||
}.sortedBy { it.manifest.name }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,43 @@
|
|||
package com.aliucord.manager.ui.viewmodel
|
||||
|
||||
import android.app.Application
|
||||
import androidx.lifecycle.AndroidViewModel
|
||||
import androidx.compose.runtime.*
|
||||
import androidx.lifecycle.ViewModel
|
||||
import com.aliucord.manager.domain.manager.PreferencesManager
|
||||
import com.aliucord.manager.ui.theme.Theme
|
||||
|
||||
class SettingsViewModel(application: Application) : AndroidViewModel(application) {
|
||||
class SettingsViewModel(
|
||||
private val application: Application,
|
||||
val preferences: PreferencesManager
|
||||
) : ViewModel() {
|
||||
var showThemeDialog by mutableStateOf(false)
|
||||
private set
|
||||
|
||||
fun clearCacheDir() {
|
||||
getApplication<Application>().cacheDir.deleteRecursively()
|
||||
fun showThemeDialog() {
|
||||
showThemeDialog = true
|
||||
}
|
||||
|
||||
fun hideThemeDialog() {
|
||||
showThemeDialog = false
|
||||
}
|
||||
|
||||
fun setPackageName(packageName: String) {
|
||||
if (packageName.isNotBlank()) preferences.packageName = packageName
|
||||
}
|
||||
|
||||
fun setAppName(appName: String) {
|
||||
if (appName.isNotBlank()) preferences.appName = appName
|
||||
}
|
||||
|
||||
fun setTheme(theme: Theme) {
|
||||
preferences.theme = theme
|
||||
}
|
||||
|
||||
fun setVersion(version: String) {
|
||||
if (version.isNotBlank()) preferences.version = version
|
||||
}
|
||||
|
||||
fun clearCacheDir() {
|
||||
application.cacheDir.deleteRecursively()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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")
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2022 Juby210 & zt
|
||||
* Licensed under the Open Software License version 3.0
|
||||
*/
|
||||
|
||||
package com.aliucord.manager.util
|
||||
|
||||
import com.aliucord.manager.model.github.Commit
|
||||
import com.aliucord.manager.model.github.Version
|
||||
import io.ktor.client.call.*
|
||||
import io.ktor.client.request.*
|
||||
import kotlinx.serialization.ExperimentalSerializationApi
|
||||
import kotlinx.serialization.json.decodeFromStream
|
||||
import java.net.URL
|
||||
import kotlin.collections.set
|
||||
|
||||
object Github {
|
||||
private const val org = "Aliucord"
|
||||
private const val repo = "Aliucord"
|
||||
|
||||
private const val commitsUrl = "https://api.github.com/repos/$org/$repo/commits"
|
||||
const val contributorsUrl = "https://api.github.com/repos/$org/$repo/contributors"
|
||||
const val dataUrl = "https://raw.githubusercontent.com/$org/$repo/builds/data.json"
|
||||
|
||||
fun checkForUpdates() {
|
||||
// TODO
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalSerializationApi::class)
|
||||
val version: Version by lazy {
|
||||
json.decodeFromStream(URL(dataUrl).openStream())
|
||||
}
|
||||
|
||||
private val commitsCache = HashMap<String, List<Commit>>()
|
||||
|
||||
suspend fun getCommits(vararg params: Pair<String, String>): List<Commit> {
|
||||
val query = params.joinToString("&", "?") { it.first + "=" + it.second }
|
||||
|
||||
if (commitsCache.containsKey(query)) return commitsCache[query] ?: emptyList()
|
||||
|
||||
val res = httpClient.get(commitsUrl) {
|
||||
params.forEach { (key, value) -> parameter(key, value) }
|
||||
}.body<List<Commit>>()
|
||||
|
||||
commitsCache[query] = res
|
||||
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
|
@ -1,11 +1,11 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M12.0001,2C9.6237,1.9976 7.3241,2.8415 5.5135,4.3806C3.7028,5.9196 2.4994,8.0532 2.1189,10.3989C1.7384,12.7446 2.2057,15.1491 3.4371,17.1816C4.6684,19.2141 6.5833,20.7416 8.8386,21.4905C9.342,21.5826 9.52,21.2695 9.52,21.0055V19.3051C6.7269,19.9128 6.1376,17.9607 6.1376,17.9607C5.9523,17.3557 5.5586,16.8359 5.0265,16.4936C4.1241,15.8797 5.1001,15.8797 5.1001,15.8797C5.4164,15.9244 5.7185,16.0404 5.9833,16.2191C6.2481,16.3978 6.4688,16.6344 6.6287,16.911C6.7645,17.1573 6.9476,17.3743 7.1675,17.5497C7.3874,17.725 7.6398,17.8552 7.9102,17.9328C8.1805,18.0103 8.4635,18.0337 8.7429,18.0017C9.0223,17.9696 9.2927,17.8827 9.5384,17.7459C9.577,17.2409 9.7947,16.7662 10.1523,16.4076C7.9301,16.1559 5.5974,15.2965 5.5974,11.4966C5.5821,10.504 5.9492,9.5435 6.6225,8.814C6.3226,7.9509 6.3577,7.0067 6.7207,6.1682C6.7207,6.1682 7.5617,5.8981 9.4709,7.1934C11.1103,6.7433 12.8407,6.7433 14.4801,7.1934C16.3892,5.8981 17.2241,6.1682 17.2241,6.1682C17.5922,6.9978 17.6361,7.9353 17.3469,8.7956C18.0203,9.5251 18.3873,10.4856 18.3721,11.4782C18.3721,15.3211 16.0332,16.1621 13.8048,16.3892C14.0438,16.6294 14.2283,16.9181 14.3459,17.2359C14.4634,17.5536 14.5113,17.8929 14.4862,18.2308V20.9748C14.4862,21.3002 14.6643,21.5519 15.1738,21.4537C17.4034,20.685 19.2896,19.1531 20.4992,17.1286C21.7088,15.104 22.164,12.7171 21.7842,10.3895C21.4045,8.0619 20.2143,5.9434 18.424,4.4082C16.6336,2.8731 14.3583,2.0201 12.0001,2Z"
|
||||
android:fillType="evenOdd" />
|
||||
android:fillType="evenOdd"
|
||||
android:pathData="M12.0001,2C9.6237,1.9976 7.3241,2.8415 5.5135,4.3806C3.7028,5.9196 2.4994,8.0532 2.1189,10.3989C1.7384,12.7446 2.2057,15.1491 3.4371,17.1816C4.6684,19.2141 6.5833,20.7416 8.8386,21.4905C9.342,21.5826 9.52,21.2695 9.52,21.0055V19.3051C6.7269,19.9128 6.1376,17.9607 6.1376,17.9607C5.9523,17.3557 5.5586,16.8359 5.0265,16.4936C4.1241,15.8797 5.1001,15.8797 5.1001,15.8797C5.4164,15.9244 5.7185,16.0404 5.9833,16.2191C6.2481,16.3978 6.4688,16.6344 6.6287,16.911C6.7645,17.1573 6.9476,17.3743 7.1675,17.5497C7.3874,17.725 7.6398,17.8552 7.9102,17.9328C8.1805,18.0103 8.4635,18.0337 8.7429,18.0017C9.0223,17.9696 9.2927,17.8827 9.5384,17.7459C9.577,17.2409 9.7947,16.7662 10.1523,16.4076C7.9301,16.1559 5.5974,15.2965 5.5974,11.4966C5.5821,10.504 5.9492,9.5435 6.6225,8.814C6.3226,7.9509 6.3577,7.0067 6.7207,6.1682C6.7207,6.1682 7.5617,5.8981 9.4709,7.1934C11.1103,6.7433 12.8407,6.7433 14.4801,7.1934C16.3892,5.8981 17.2241,6.1682 17.2241,6.1682C17.5922,6.9978 17.6361,7.9353 17.3469,8.7956C18.0203,9.5251 18.3873,10.4856 18.3721,11.4782C18.3721,15.3211 16.0332,16.1621 13.8048,16.3892C14.0438,16.6294 14.2283,16.9181 14.3459,17.2359C14.4634,17.5536 14.5113,17.8929 14.4862,18.2308V20.9748C14.4862,21.3002 14.6643,21.5519 15.1738,21.4537C17.4034,20.685 19.2896,19.1531 20.4992,17.1286C21.7088,15.104 22.164,12.7171 21.7842,10.3895C21.4045,8.0619 20.2143,5.9434 18.424,4.4082C16.6336,2.8731 14.3583,2.0201 12.0001,2Z" />
|
||||
</vector>
|
||||
|
|
|
|||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M19,9h-4V3H9v6H5l7,7 7,-7zM5,18v2h14v-2H5z" />
|
||||
</vector>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M13,3c-4.97,0 -9,4.03 -9,9L1,12l3.89,3.89 0.07,0.14L9,12L6,12c0,-3.87 3.13,-7 7,-7s7,3.13 7,7 -3.13,7 -7,7c-1.93,0 -3.68,-0.79 -4.94,-2.06l-1.42,1.42C8.27,19.99 10.51,21 13,21c4.97,0 9,-4.03 9,-9s-4.03,-9 -9,-9zM12,8v5l4.28,2.54 0.72,-1.21 -3.5,-2.08L13.5,8L12,8z" />
|
||||
</vector>
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:height="24dp"
|
||||
android:width="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#fff"
|
||||
android:pathData="M19,19H5V5h7V3H5c-1.11,0 -2,0.9 -2,2v14c0,1.1 0.89,2 2,2h14c1.1,0 2,-0.9 2,-2v-7h-2v7zM14,3v2h3.59l-9.83,9.83 1.41,1.41L19,6.41V10h2V3h-7z" />
|
||||
</vector>
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
|
||||
<group>
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M 20.835 11.512 L 19.37 11.512 L 19.37 7.607 C 19.37 6.533 18.492 5.655 17.418 5.655 L 13.513 5.655 L 13.513 4.19 C 13.513 2.824 12.439 1.75 11.073 1.75 C 9.706 1.75 8.632 2.824 8.632 4.19 L 8.632 5.655 L 4.727 5.655 C 3.654 5.655 2.775 6.533 2.775 7.607 L 2.775 11.317 L 4.239 11.317 C 5.704 11.317 6.875 12.488 6.875 13.952 C 6.875 15.417 5.704 16.588 4.239 16.588 L 2.775 16.588 L 2.775 20.298 C 2.775 21.371 3.654 22.25 4.727 22.25 L 8.437 22.25 L 8.437 20.786 C 8.437 19.321 9.608 18.15 11.073 18.15 C 12.537 18.15 13.708 19.321 13.708 20.786 L 13.708 22.25 L 17.418 22.25 C 18.492 22.25 19.37 21.371 19.37 20.298 L 19.37 16.393 L 20.835 16.393 C 22.201 16.393 23.275 15.319 23.275 13.952 C 23.275 12.586 22.201 11.512 20.835 11.512 Z" />
|
||||
<group></group>
|
||||
</group>
|
||||
</vector>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M12,6v3l4,-4 -4,-4v3c-4.42,0 -8,3.58 -8,8 0,1.57 0.46,3.03 1.24,4.26L6.7,14.8c-0.45,-0.83 -0.7,-1.79 -0.7,-2.8 0,-3.31 2.69,-6 6,-6zM18.76,7.74L17.3,9.2c0.44,0.84 0.7,1.79 0.7,2.8 0,3.31 -2.69,6 -6,6v-3l-4,4 4,4v-3c4.42,0 8,-3.58 8,-8 0,-1.57 -0.46,-3.03 -1.24,-4.26z" />
|
||||
</vector>
|
||||
|
|
@ -1,9 +0,0 @@
|
|||
<vector xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:width="24dp"
|
||||
android:height="24dp"
|
||||
android:viewportWidth="24"
|
||||
android:viewportHeight="24">
|
||||
<path
|
||||
android:fillColor="#ffffff"
|
||||
android:pathData="M11,8v5l4.25,2.52l0.77,-1.28l-3.52,-2.09V8H11zM21,10V3l-2.64,2.64C16.74,4.01 14.49,3 12,3c-4.97,0 -9,4.03 -9,9s4.03,9 9,9s9,-4.03 9,-9h-2c0,3.86 -3.14,7 -7,7s-7,-3.14 -7,-7s3.14,-7 7,-7c1.93,0 3.68,0.79 4.95,2.05L14,10H21z" />
|
||||
</vector>
|
||||
|
|
@ -6,16 +6,15 @@
|
|||
<string name="open_settings">Open settings</string>
|
||||
<string name="permission_required">You need to grant storage permission to use this app</string>
|
||||
<string name="permission_grant">Grant permission</string>
|
||||
<string name="replace_bg">Replace app icon with Aliucord logo</string>
|
||||
<string name="replace_bg">Replace app icon with Aliucord variant</string>
|
||||
<string name="settings">Settings</string>
|
||||
<string name="show_menu">Show menu</string>
|
||||
<string name="theme">Theme</string>
|
||||
<string name="use_dex_from_storage">Use Injector.dex from storage</string>
|
||||
<string name="plugins">Plugins</string>
|
||||
<string name="uninstall_plugin">Uninstall %1$s</string>
|
||||
<string name="search">Search</string>
|
||||
<string name="view_plugin_changelog">View changelog for %1$s</string>
|
||||
<string name="delete_plugin_body">Are you sure you want to uninstall this plugin? This action cannot be undone.</string>
|
||||
<string name="delete_plugin_body">Are you sure you want to uninstall this plugin?</string>
|
||||
<string name="close">Close</string>
|
||||
<string name="support_server">Support Server</string>
|
||||
<string name="back">Back</string>
|
||||
|
|
@ -23,7 +22,7 @@
|
|||
<string name="install">Install</string>
|
||||
<string name="reinstall">Reinstall</string>
|
||||
<string name="update">Update</string>
|
||||
<string name="developer_mode">Developer Mode</string>
|
||||
<string name="advanced_options">Show advanced options</string>
|
||||
<string name="debuggable">Debuggable</string>
|
||||
<string name="debuggable_description">Enable debuggable flag</string>
|
||||
<string name="about">About</string>
|
||||
|
|
@ -32,11 +31,8 @@
|
|||
<string name="search_plugins">Search Plugins</string>
|
||||
<string name="github">GitHub</string>
|
||||
<string name="installer">Installer</string>
|
||||
<string name="use_black">Use black</string>
|
||||
<string name="use_black_description">Use black colors for dark mode</string>
|
||||
<string name="theme_setting_description">Manager appearance</string>
|
||||
<string name="no_plugins_installed">No plugins installed!</string>
|
||||
<string name="store">Store</string>
|
||||
<string name="paging_initial_load">Loading…</string>
|
||||
<string name="contributor">Contributor</string>
|
||||
<string name="apply">Apply</string>
|
||||
|
|
@ -49,4 +45,7 @@
|
|||
<string name="app_name_setting">App Name</string>
|
||||
<string name="package_name">Package Name</string>
|
||||
<string name="clear">Clear</string>
|
||||
<string name="launch">Launch</string>
|
||||
<string name="uninstall">Uninstall</string>
|
||||
<string name="version">Version</string>
|
||||
</resources>
|
||||
|
|
|
|||
4
gradle/wrapper/gradle-wrapper.properties
vendored
4
gradle/wrapper/gradle-wrapper.properties
vendored
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue