feat: move to voyager, clean up, remove NavBar

This commit is contained in:
rushiiMachine 2024-01-13 13:01:08 -08:00
parent 0efd525e88
commit 5339459ac8
No known key found for this signature in database
GPG key ID: DCBE5952BB3B6420
55 changed files with 1106 additions and 1120 deletions

View file

@ -1,4 +1,6 @@
# Aliucord Manager [![Crowdin](https://badges.crowdin.net/aliucord-manager/localized.svg)](https://crowdin.com/project/aliucord-manager) # Aliucord Manager [![Crowdin](https://badges.crowdin.net/aliucord-manager/localized.svg)](https://crowdin.com/project/aliucord-manager)
Aliucord Manager - Aliucord Installer rewrite in [Kotlin](https://kotlinlang.org) and [Jetpack Compose](https://developer.android.com/jetpack/compose).
Aliucord Manager - Aliucord Installer rewrite in [Kotlin](https://kotlinlang.org)
and [Jetpack Compose](https://developer.android.com/jetpack/compose).
INFO: This app is not functional yet, if you want to install Aliucord - [read this](https://github.com/Aliucord/Aliucord#-installation) INFO: This app is not functional yet, if you want to install Aliucord - [read this](https://github.com/Aliucord/Aliucord#-installation)

View file

@ -90,7 +90,10 @@ android {
jvmTarget = "11" jvmTarget = "11"
freeCompilerArgs += listOf( freeCompilerArgs += listOf(
"-Xcontext-receivers", "-Xcontext-receivers",
"-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${reportsDir}", "-opt-in=androidx.compose.foundation.ExperimentalFoundationApi",
"-opt-in=androidx.compose.animation.ExperimentalAnimationApi",
"-opt-in=androidx.compose.material3.ExperimentalMaterial3Api",
// "-P", "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=${reportsDir}",
) )
} }
@ -110,6 +113,7 @@ dependencies {
implementation(libs.bundles.compose) implementation(libs.bundles.compose)
implementation(libs.bundles.koin) implementation(libs.bundles.koin)
implementation(libs.bundles.ktor) implementation(libs.bundles.ktor)
implementation(libs.bundles.voyager)
implementation(libs.compose.ui.tooling.preview) implementation(libs.compose.ui.tooling.preview)
debugImplementation(libs.compose.ui.tooling) debugImplementation(libs.compose.ui.tooling)
@ -122,7 +126,6 @@ dependencies {
implementation(libs.axml) implementation(libs.axml)
implementation(libs.bouncycastle) implementation(libs.bouncycastle)
implementation(libs.coil) implementation(libs.coil)
implementation(libs.navigationReimagined)
implementation(variantOf(libs.zip) { artifactType("aar") }) implementation(variantOf(libs.zip) { artifactType("aar") })
} }

View file

@ -12,17 +12,16 @@ import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
import cafe.adriel.voyager.navigator.Navigator
import cafe.adriel.voyager.transitions.FadeTransition
import com.aliucord.manager.domain.manager.PreferencesManager import com.aliucord.manager.domain.manager.PreferencesManager
import com.aliucord.manager.ui.dialog.StoragePermissionsDialog import com.aliucord.manager.ui.components.*
import com.aliucord.manager.ui.dialog.UpdaterDialog import com.aliucord.manager.ui.components.dialogs.StoragePermissionsDialog
import com.aliucord.manager.ui.navigation.* import com.aliucord.manager.ui.screens.home.HomeScreen
import com.aliucord.manager.ui.screen.* import com.aliucord.manager.ui.widgets.updater.UpdaterDialog
import com.aliucord.manager.ui.theme.ManagerTheme
import com.aliucord.manager.ui.theme.Theme
import dev.olshevski.navigation.reimagined.*
import kotlinx.collections.immutable.persistentListOf
import org.koin.android.ext.android.inject import org.koin.android.ext.android.inject
// TODO: move to a path provider in DI
val aliucordDir = Environment.getExternalStorageDirectory().resolve("Aliucord") val aliucordDir = Environment.getExternalStorageDirectory().resolve("Aliucord")
class MainActivity : ComponentActivity() { class MainActivity : ComponentActivity() {
@ -38,12 +37,6 @@ class MainActivity : ComponentActivity() {
isDarkTheme = preferences.theme == Theme.DARK || preferences.theme == Theme.SYSTEM && isSystemInDarkTheme(), isDarkTheme = preferences.theme == Theme.DARK || preferences.theme == Theme.SYSTEM && isSystemInDarkTheme(),
isDynamicColor = preferences.dynamicColor isDynamicColor = preferences.dynamicColor
) { ) {
val navController = rememberNavController<AppDestination>(AppDestination.Home)
BackHandler {
navController.back()
}
StoragePermissionsDialog() StoragePermissionsDialog()
@Suppress("KotlinConstantConditions") @Suppress("KotlinConstantConditions")
@ -55,36 +48,15 @@ class MainActivity : ComponentActivity() {
UpdaterDialog() UpdaterDialog()
} }
NavHost( Navigator(
controller = navController, screen = HomeScreen(),
) { onBackPressed = null,
when (val dest = this.currentHostEntry.destination) { ) { navigator ->
is BaseScreenDestination -> BaseScreen( BackHandler {
currentNavItem = dest, navigator.back(this@MainActivity)
bottomNavItems = persistentListOf(AppDestination.Home, AppDestination.Plugins, AppDestination.Settings),
onNavChanged = { navController.replaceLast(it) }
) {
when (dest) {
is AppDestination.Home -> HomeScreen(
onClickInstall = { data ->
navController.navigate(AppDestination.Install(data))
}
)
is AppDestination.Plugins -> PluginsScreen()
is AppDestination.Settings -> SettingsScreen()
}
}
is AppDestination.Install -> InstallerScreen(
installData = dest.installData,
onBackClick = { navController.back() }
)
is AppDestination.About -> AboutScreen(
onBackClick = { navController.back() }
)
} }
FadeTransition(navigator)
} }
} }
} }

View file

@ -5,10 +5,16 @@ import com.aliucord.manager.di.*
import com.aliucord.manager.domain.repository.AliucordMavenRepository import com.aliucord.manager.domain.repository.AliucordMavenRepository
import com.aliucord.manager.domain.repository.GithubRepository import com.aliucord.manager.domain.repository.GithubRepository
import com.aliucord.manager.network.service.* import com.aliucord.manager.network.service.*
import com.aliucord.manager.ui.viewmodel.* import com.aliucord.manager.ui.screens.about.AboutModel
import com.aliucord.manager.ui.screens.home.HomeModel
import com.aliucord.manager.ui.screens.install.InstallModel
import com.aliucord.manager.ui.screens.plugins.PluginsModel
import com.aliucord.manager.ui.screens.settings.SettingsModel
import com.aliucord.manager.ui.widgets.updater.UpdaterViewModel
import org.koin.android.ext.koin.androidContext import org.koin.android.ext.koin.androidContext
import org.koin.androidx.viewmodel.dsl.viewModelOf import org.koin.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.context.startKoin import org.koin.core.context.startKoin
import org.koin.core.module.dsl.factoryOf
import org.koin.core.module.dsl.singleOf import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module import org.koin.dsl.module
@ -39,13 +45,13 @@ class ManagerApplication : Application() {
singleOf(::AliucordMavenRepository) singleOf(::AliucordMavenRepository)
}) })
// ViewModels // UI Models
modules(module { modules(module {
viewModelOf(::HomeViewModel) factoryOf(::HomeModel)
viewModelOf(::PluginsViewModel) factoryOf(::PluginsModel)
viewModelOf(::AboutViewModel) factoryOf(::AboutModel)
viewModelOf(::InstallViewModel) factoryOf(::InstallModel)
viewModelOf(::SettingsViewModel) factoryOf(::SettingsModel)
viewModelOf(::UpdaterViewModel) viewModelOf(::UpdaterViewModel)
}) })

View file

@ -12,7 +12,7 @@ import java.io.File
import kotlin.coroutines.* import kotlin.coroutines.*
class DownloadManager( class DownloadManager(
private val application: Application private val application: Application,
) { ) {
private val downloadManager = application.getSystemService<DownloadManager>()!! private val downloadManager = application.getSystemService<DownloadManager>()!!

View file

@ -2,7 +2,7 @@ package com.aliucord.manager.domain.manager
import android.content.SharedPreferences import android.content.SharedPreferences
import com.aliucord.manager.domain.manager.base.BasePreferenceManager import com.aliucord.manager.domain.manager.base.BasePreferenceManager
import com.aliucord.manager.ui.theme.Theme import com.aliucord.manager.ui.components.Theme
class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager(preferences) { class PreferencesManager(preferences: SharedPreferences) : BasePreferenceManager(preferences) {
var theme by enumPreference("theme", Theme.SYSTEM) var theme by enumPreference("theme", Theme.SYSTEM)

View file

@ -6,7 +6,7 @@ import androidx.core.content.edit
import kotlin.reflect.KProperty import kotlin.reflect.KProperty
abstract class BasePreferenceManager( abstract class BasePreferenceManager(
private val prefs: SharedPreferences private val prefs: SharedPreferences,
) { ) {
protected fun getString(key: String, defaultValue: String) = prefs.getString(key, defaultValue) ?: defaultValue 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 getBoolean(key: String, defaultValue: Boolean) = prefs.getBoolean(key, defaultValue)
@ -25,7 +25,7 @@ abstract class BasePreferenceManager(
private val key: String, private val key: String,
defaultValue: T, defaultValue: T,
getter: (key: String, defaultValue: T) -> T, getter: (key: String, defaultValue: T) -> T,
private val setter: (key: String, newValue: T) -> Unit private val setter: (key: String, newValue: T) -> Unit,
) { ) {
@Suppress("RedundantSetter") @Suppress("RedundantSetter")
var value by mutableStateOf(getter(key, defaultValue)) var value by mutableStateOf(getter(key, defaultValue))
@ -40,7 +40,7 @@ abstract class BasePreferenceManager(
protected fun stringPreference( protected fun stringPreference(
key: String, key: String,
defaultValue: String defaultValue: String,
) = Preference( ) = Preference(
key = key, key = key,
defaultValue = defaultValue, defaultValue = defaultValue,
@ -50,7 +50,7 @@ abstract class BasePreferenceManager(
protected fun booleanPreference( protected fun booleanPreference(
key: String, key: String,
defaultValue: Boolean defaultValue: Boolean,
) = Preference( ) = Preference(
key = key, key = key,
defaultValue = defaultValue, defaultValue = defaultValue,
@ -60,7 +60,7 @@ abstract class BasePreferenceManager(
protected fun intPreference( protected fun intPreference(
key: String, key: String,
defaultValue: Int defaultValue: Int,
) = Preference( ) = Preference(
key = key, key = key,
defaultValue = defaultValue, defaultValue = defaultValue,
@ -70,7 +70,7 @@ abstract class BasePreferenceManager(
protected fun floatPreference( protected fun floatPreference(
key: String, key: String,
defaultValue: Float defaultValue: Float,
) = Preference( ) = Preference(
key = key, key = key,
defaultValue = defaultValue, defaultValue = defaultValue,
@ -80,7 +80,7 @@ abstract class BasePreferenceManager(
protected inline fun <reified E : Enum<E>> enumPreference( protected inline fun <reified E : Enum<E>> enumPreference(
key: String, key: String,
defaultValue: E defaultValue: E,
) = Preference( ) = Preference(
key = key, key = key,
defaultValue = defaultValue, defaultValue = defaultValue,

View file

@ -10,13 +10,12 @@ import com.github.diamondminer88.zip.ZipReader
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import org.koin.core.context.GlobalContext import org.koin.core.component.KoinComponent
import org.koin.core.component.get
import java.io.File import java.io.File
private val json: Json = GlobalContext.get().get()
@Immutable @Immutable
data class Plugin(val file: File) { data class Plugin(val file: File) : KoinComponent {
val manifest: Manifest val manifest: Manifest
init { init {
@ -25,7 +24,7 @@ data class Plugin(val file: File) {
?: throw Exception("Plugin ${file.nameWithoutExtension} has no manifest.") ?: throw Exception("Plugin ${file.nameWithoutExtension} has no manifest.")
@OptIn(ExperimentalSerializationApi::class) @OptIn(ExperimentalSerializationApi::class)
this.manifest = json.decodeFromStream(manifest.read().inputStream()) this.manifest = get<Json>().decodeFromStream(manifest.read().inputStream())
} }
} }
} }

View file

@ -5,7 +5,7 @@ import com.aliucord.manager.network.utils.*
import io.ktor.http.HttpStatusCode import io.ktor.http.HttpStatusCode
class AliucordMavenRepository( class AliucordMavenRepository(
private val maven: MavenService private val maven: MavenService,
) { ) {
suspend fun getAliuhookVersion(): ApiResponse<String> { suspend fun getAliuhookVersion(): ApiResponse<String> {
return maven.getArtifactMetadata(BASE_URL, ALIUHOOK).transform { return maven.getArtifactMetadata(BASE_URL, ALIUHOOK).transform {

View file

@ -5,7 +5,7 @@ import com.aliucord.manager.network.service.AliucordGithubService
import com.aliucord.manager.network.utils.transform import com.aliucord.manager.network.utils.transform
class GithubRepository( class GithubRepository(
private val service: AliucordGithubService private val service: AliucordGithubService,
) { ) {
suspend fun getCommits(page: Int = 0) = service.getCommits(page) suspend fun getCommits(page: Int = 0) = service.getCommits(page)
suspend fun getContributors() = service.getContributors() suspend fun getContributors() = service.getContributors()

View file

@ -24,7 +24,7 @@ object ManifestPatcher {
manifestBytes: ByteArray, manifestBytes: ByteArray,
packageName: String, packageName: String,
appName: String, appName: String,
debuggable: Boolean debuggable: Boolean,
): ByteArray { ): ByteArray {
val reader = AxmlReader(manifestBytes) val reader = AxmlReader(manifestBytes)
val writer = AxmlWriter() val writer = AxmlWriter()
@ -142,7 +142,7 @@ object ManifestPatcher {
fun renamePackage( fun renamePackage(
manifestBytes: ByteArray, manifestBytes: ByteArray,
packageName: String packageName: String,
): ByteArray { ): ByteArray {
val reader = AxmlReader(manifestBytes) val reader = AxmlReader(manifestBytes)
val writer = AxmlWriter() val writer = AxmlWriter()
@ -160,7 +160,7 @@ object ManifestPatcher {
private open class ReplaceAttrsVisitor( private open class ReplaceAttrsVisitor(
nv: NodeVisitor, nv: NodeVisitor,
private val attrs: Map<String, Any> private val attrs: Map<String, Any>,
) : NodeVisitor(nv) { ) : NodeVisitor(nv) {
override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) { override fun attr(ns: String?, name: String, resourceId: Int, type: Int, value: Any?) {
val replace = attrs.containsKey(name) val replace = attrs.containsKey(name)

View file

@ -14,7 +14,7 @@ data class Commit(
val htmlUrl: String, val htmlUrl: String,
val sha: String, val sha: String,
val commit: Commit, val commit: Commit,
val author: Author? val author: Author?,
) { ) {
@Serializable @Serializable
data class Author(@SerialName("login") val name: String) data class Author(@SerialName("login") val name: String)

View file

@ -9,12 +9,12 @@ data class GithubRelease(
val createdAt: String, val createdAt: String,
val assets: List<GithubReleaseAssets>, val assets: List<GithubReleaseAssets>,
@SerialName("tag_name") @SerialName("tag_name")
val tagName: String val tagName: String,
) { ) {
@Serializable @Serializable
data class GithubReleaseAssets( data class GithubReleaseAssets(
val name: String, val name: String,
@SerialName("browser_download_url") @SerialName("browser_download_url")
val browserDownloadUrl: String val browserDownloadUrl: String,
) )
} }

View file

@ -12,5 +12,5 @@ import kotlinx.serialization.Serializable
data class GithubUser( data class GithubUser(
@SerialName("login") @SerialName("login")
val name: String, val name: String,
val contributions: Int val contributions: Int,
) )

View file

@ -11,5 +11,5 @@ import kotlinx.serialization.Serializable
data class Version( data class Version(
val versionCode: String, val versionCode: String,
val versionName: String, val versionName: String,
val aliucordHash: String val aliucordHash: String,
) )

View file

@ -8,7 +8,7 @@ import kotlinx.coroutines.withContext
class AliucordGithubService( class AliucordGithubService(
val github: GithubService, val github: GithubService,
val http: HttpService val http: HttpService,
) { ) {
suspend fun getDataJson(): ApiResponse<Version> { suspend fun getDataJson(): ApiResponse<Version> {
return withContext(Dispatchers.IO) { return withContext(Dispatchers.IO) {

View file

@ -17,7 +17,7 @@ class ApiFailure(error: Throwable, body: String?) : Error(body, error)
inline fun <T, R> ApiResponse<T>.fold( inline fun <T, R> ApiResponse<T>.fold(
success: (T) -> R, success: (T) -> R,
error: (ApiError) -> R, error: (ApiError) -> R,
failure: (ApiFailure) -> R failure: (ApiFailure) -> R,
): R { ): R {
return when (this) { return when (this) {
is ApiResponse.Success -> success(this.data) is ApiResponse.Success -> success(this.data)

View file

@ -7,7 +7,7 @@ data class SemVer(
val major: Int, val major: Int,
val minor: Int, val minor: Int,
val patch: Int, val patch: Int,
private val vPrefix: Boolean = false private val vPrefix: Boolean = false,
) : Comparable<SemVer> { ) : Comparable<SemVer> {
override fun compareTo(other: SemVer): Int { override fun compareTo(other: SemVer): Int {
val pairs = arrayOf( val pairs = arrayOf(

View file

@ -0,0 +1,50 @@
package com.aliucord.manager.ui.components
import android.app.Activity
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.Navigator
import com.aliucord.manager.R
import com.aliucord.manager.ui.screens.home.HomeScreen
/**
* Standalone back button for interacting with the current navigator.
*/
@Composable
fun BackButton() {
// Support rendering in @Preview
val context = if (LocalInspectionMode.current) null else LocalContext.current as Activity
val navigator = LocalNavigator.current
IconButton(
onClick = { if (context != null) navigator?.back(context) },
) {
Icon(
painter = painterResource(R.drawable.ic_back),
contentDescription = stringResource(R.string.navigation_back),
)
}
}
/**
* Custom back logic for handling special screens differently.
* If on Home, Landing, or Debug then exit immediately otherwise go back one.
*/
fun Navigator.back(currentActivity: Activity?) {
val top = this.lastItemOrNull ?: return
val stackSize = this.items.size
if (stackSize > 1) {
pop()
} else if (top is HomeScreen) {
currentActivity?.finish()
} else {
replaceAll(HomeScreen())
}
}

View file

@ -3,14 +3,13 @@
* Licensed under the Open Software License version 3.0 * Licensed under the Open Software License version 3.0
*/ */
package com.aliucord.manager.ui.component package com.aliucord.manager.ui.components
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.* import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
@ -22,41 +21,15 @@ import coil.compose.AsyncImage
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.network.dto.GithubUser import com.aliucord.manager.network.dto.GithubUser
fun LazyListScope.contributors(contributors: List<GithubUser>) {
item {
Text(
text = stringResource(R.string.contributors),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(start = 16.dp, bottom = 16.dp)
)
}
if (contributors.isNotEmpty()) {
items(contributors) { user ->
ContributorEntry(user = user)
}
} else {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center,
content = { CircularProgressIndicator() }
)
}
}
}
@Composable @Composable
fun ContributorEntry(user: GithubUser) { fun ContributorCommitsItem(
user: GithubUser,
modifier: Modifier = Modifier,
) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current
Row( Row(
modifier = Modifier modifier = modifier
.clickable { .clickable {
uriHandler.openUri("https://github.com/${user.name}") uriHandler.openUri("https://github.com/${user.name}")
} }
@ -85,6 +58,5 @@ fun ContributorEntry(user: GithubUser) {
) )
) )
} }
} }
} }

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.ui.component package com.aliucord.manager.ui.components
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.*

View file

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0 * Licensed under the Open Software License version 3.0
*/ */
package com.aliucord.manager.ui.theme package com.aliucord.manager.ui.components
import android.os.Build import android.os.Build
import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.isSystemInDarkTheme
@ -19,7 +19,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
fun ManagerTheme( fun ManagerTheme(
isDarkTheme: Boolean = isSystemInDarkTheme(), isDarkTheme: Boolean = isSystemInDarkTheme(),
isDynamicColor: Boolean = true, isDynamicColor: Boolean = true,
content: @Composable () -> Unit content: @Composable () -> Unit,
) { ) {
val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when { val colorScheme = when {

View file

@ -3,18 +3,18 @@
* Licensed under the Open Software License version 3.0 * Licensed under the Open Software License version 3.0
*/ */
package com.aliucord.manager.ui.dialog package com.aliucord.manager.ui.components.dialogs
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.saveable.rememberSaveable
import com.aliucord.manager.ui.screen.InstallData import com.aliucord.manager.ui.screens.install.InstallData
enum class DownloadMethod { DOWNLOAD } enum class DownloadMethod { DOWNLOAD }
@Composable @Composable
fun InstallerDialog( fun InstallerDialog(
onDismiss: () -> Unit, onDismiss: () -> Unit,
onConfirm: (InstallData) -> Unit onConfirm: (InstallData) -> Unit,
) { ) {
val downloadMethod by rememberSaveable { mutableStateOf(DownloadMethod.DOWNLOAD) } val downloadMethod by rememberSaveable { mutableStateOf(DownloadMethod.DOWNLOAD) }

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.ui.dialog package com.aliucord.manager.ui.components.dialogs
import android.Manifest import android.Manifest
import android.content.Intent import android.content.Intent
@ -7,6 +7,7 @@ import android.os.Environment
import android.provider.Settings import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.material3.* import androidx.compose.material3.*
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
@ -26,6 +27,7 @@ fun StoragePermissionsDialog() {
} }
@Composable @Composable
@RequiresApi(Build.VERSION_CODES.R)
private fun ManageStorageDialog() { private fun ManageStorageDialog() {
var manageStorageGranted by remember { mutableStateOf(Environment.isExternalStorageManager()) } var manageStorageGranted by remember { mutableStateOf(Environment.isExternalStorageManager()) }
@ -59,8 +61,8 @@ private fun ManageStorageDialog() {
} }
} }
@OptIn(ExperimentalPermissionsApi::class)
@Composable @Composable
@OptIn(ExperimentalPermissionsApi::class)
private fun ExternalStorageDialog() { private fun ExternalStorageDialog() {
val writeStorageState = rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE) val writeStorageState = rememberPermissionState(Manifest.permission.WRITE_EXTERNAL_STORAGE)

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.ui.component.home package com.aliucord.manager.ui.components.home
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
@ -22,7 +22,7 @@ import androidx.paging.compose.items
import coil.compose.AsyncImage import coil.compose.AsyncImage
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.network.dto.Commit import com.aliucord.manager.network.dto.Commit
import com.aliucord.manager.ui.component.LoadFailure import com.aliucord.manager.ui.components.LoadFailure
import com.aliucord.manager.ui.util.annotatingStringResource import com.aliucord.manager.ui.util.annotatingStringResource
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.Flow
@Composable @Composable
fun CommitList( fun CommitList(
commits: Flow<PagingData<Commit>>, commits: Flow<PagingData<Commit>>,
onRetry: () -> Unit onRetry: () -> Unit,
) { ) {
ElevatedCard( ElevatedCard(
modifier = Modifier.padding(bottom = 16.dp) modifier = Modifier.padding(bottom = 16.dp)
@ -113,7 +113,7 @@ fun CommitList(
@Composable @Composable
fun CommitItem( fun CommitItem(
commit: Commit commit: Commit,
) { ) {
val uriHandler = LocalUriHandler.current val uriHandler = LocalUriHandler.current

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.ui.component.home package com.aliucord.manager.ui.components.home
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
@ -23,7 +23,7 @@ fun InfoCard(
currentVersion: DiscordVersion, currentVersion: DiscordVersion,
onDownloadClick: () -> Unit, onDownloadClick: () -> Unit,
onUninstallClick: () -> Unit, onUninstallClick: () -> Unit,
onLaunchClick: () -> Unit onLaunchClick: () -> Unit,
) { ) {
ElevatedCard { ElevatedCard {
Column( Column(

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.ui.component.installer package com.aliucord.manager.ui.components.installer
import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.AnimatedVisibility
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -35,7 +35,7 @@ class InstallStepData(
val nameResId: Int, val nameResId: Int,
status: InstallStatus, status: InstallStatus,
duration: Float = 0f, duration: Float = 0f,
cached: Boolean = false cached: Boolean = false,
) { ) {
var status by mutableStateOf(status) var status by mutableStateOf(status)
var duration by mutableStateOf(duration) var duration by mutableStateOf(duration)
@ -47,7 +47,7 @@ fun InstallGroup(
name: String, name: String,
isCurrent: Boolean, isCurrent: Boolean,
subSteps: ImmutableList<InstallStepData>, subSteps: ImmutableList<InstallStepData>,
onClick: () -> Unit onClick: () -> Unit,
) { ) {
val status = when { val status = when {
subSteps.all { it.status == InstallStatus.QUEUED } -> subSteps.all { it.status == InstallStatus.QUEUED } ->

View file

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0 * Licensed under the Open Software License version 3.0
*/ */
package com.aliucord.manager.ui.component.plugins package com.aliucord.manager.ui.components.plugins
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn import androidx.compose.foundation.lazy.LazyColumn
@ -64,7 +64,7 @@ private fun AnnotatedString.Builder.hyperlink(content: String) {
@Composable @Composable
fun Changelog( fun Changelog(
plugin: Plugin, plugin: Plugin,
onDismiss: () -> Unit onDismiss: () -> Unit,
) { ) {
AlertDialog( AlertDialog(
onDismissRequest = onDismiss, onDismissRequest = onDismiss,
@ -164,7 +164,7 @@ fun Changelog(
@Composable @Composable
private fun LinkText( private fun LinkText(
modifier: Modifier = Modifier, modifier: Modifier = Modifier,
annotatedString: AnnotatedString annotatedString: AnnotatedString,
) { ) {
val urlHandler = LocalUriHandler.current val urlHandler = LocalUriHandler.current

View file

@ -3,7 +3,7 @@
* Licensed under the Open Software License version 3.0 * Licensed under the Open Software License version 3.0
*/ */
package com.aliucord.manager.ui.component.plugins package com.aliucord.manager.ui.components.plugins
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.ui.component.settings package com.aliucord.manager.ui.components.settings
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
@ -9,7 +9,7 @@ import androidx.compose.ui.unit.dp
@Composable @Composable
fun SettingsHeader( fun SettingsHeader(
text: String text: String,
) { ) {
Text( Text(
text = text, text = text,

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.ui.component.settings package com.aliucord.manager.ui.components.settings
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.ui.component.settings package com.aliucord.manager.ui.components.settings
import androidx.compose.foundation.clickable import androidx.compose.foundation.clickable
import androidx.compose.material3.Switch import androidx.compose.material3.Switch

View file

@ -1,12 +1,12 @@
package com.aliucord.manager.ui.component.settings package com.aliucord.manager.ui.components.settings
import androidx.compose.foundation.layout.* import androidx.compose.foundation.layout.*
import androidx.compose.material3.* import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable @Composable
fun SettingsTextField( fun SettingsTextField(
label: String, label: String,

View file

@ -1,62 +0,0 @@
package com.aliucord.manager.ui.navigation
import android.app.Activity
import android.os.Parcelable
import androidx.annotation.DrawableRes
import androidx.annotation.StringRes
import com.aliucord.manager.R
import com.aliucord.manager.ui.screen.InstallData
import dev.olshevski.navigation.reimagined.*
import kotlinx.parcelize.Parcelize
@Parcelize
sealed class BaseScreenDestination(
@DrawableRes val selectedIcon: Int,
@DrawableRes val unselectedIcon: Int,
@StringRes val label: Int
) : AppDestination
@Parcelize
sealed interface AppDestination : Parcelable {
@Parcelize
object Home : BaseScreenDestination(
R.drawable.ic_home,
R.drawable.ic_home, // TODO: get ic_home outline variant
R.string.navigation_home
)
@Parcelize
object Plugins : BaseScreenDestination(
R.drawable.ic_extension,
R.drawable.ic_extension_outlined,
R.string.plugins_title
)
@Parcelize
object Settings : BaseScreenDestination(
R.drawable.ic_settings,
R.drawable.ic_settings_outlined,
R.string.navigation_settings
)
@Parcelize
data class Install(val installData: InstallData) : AppDestination
@Parcelize
object About : AppDestination
}
context(Activity)
fun NavController<AppDestination>.back() {
val topDest = backstack.entries.lastOrNull()?.destination
if (topDest == AppDestination.Home) {
finish()
} else if (topDest is BaseScreenDestination) {
replaceAll(AppDestination.Home)
} else if (backstack.entries.size > 1) {
pop()
} else {
replaceAll(AppDestination.Home)
}
}

View file

@ -1,82 +0,0 @@
@file:OptIn(ExperimentalMaterial3Api::class)
package com.aliucord.manager.ui.navigation
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalUriHandler
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import kotlinx.collections.immutable.ImmutableList
@Composable
fun BaseScreen(
currentNavItem: BaseScreenDestination,
bottomNavItems: ImmutableList<BaseScreenDestination>,
onNavChanged: (AppDestination) -> Unit,
content: @Composable () -> Unit
) {
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(currentNavItem.label)) },
actions = {
val localUriHandler = LocalUriHandler.current
IconButton(
onClick = {
localUriHandler.openUri("https://discord.gg/${BuildConfig.SUPPORT_SERVER}")
}
) {
Icon(
painter = painterResource(R.drawable.ic_discord),
contentDescription = stringResource(R.string.support_server)
)
}
IconButton(onClick = { onNavChanged(AppDestination.About) }) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = stringResource(R.string.navigation_about)
)
}
}
)
},
bottomBar = {
NavigationBar {
for (navItem in bottomNavItems) {
NavigationBarItem(
selected = currentNavItem == navItem,
icon = {
Icon(
painter = if (currentNavItem == navItem) {
painterResource(navItem.selectedIcon)
} else {
painterResource(navItem.unselectedIcon)
},
contentDescription = null,
)
},
label = { Text(stringResource(navItem.label)) },
onClick = { onNavChanged(navItem) }
)
}
}
}
) { paddingValues ->
Box(
modifier = Modifier
.padding(paddingValues)
.fillMaxSize()
) {
content()
}
}
}

View file

@ -1,181 +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.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.compose.ui.unit.sp
import coil.compose.AsyncImage
import com.aliucord.manager.R
import com.aliucord.manager.ui.component.LoadFailure
import com.aliucord.manager.ui.component.contributors
import com.aliucord.manager.ui.viewmodel.AboutViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun AboutScreen(
viewModel: AboutViewModel = getViewModel(),
onBackClick: () -> Unit
) {
val uriHandler = LocalUriHandler.current
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.navigation_about)) },
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
painter = painterResource(R.drawable.ic_back),
contentDescription = stringResource(R.string.navigation_back)
)
}
}
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier
.padding(paddingValues)
) {
item {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
AsyncImage(
model = "https://github.com/Aliucord.png",
contentDescription = stringResource(R.string.aliucord),
modifier = Modifier.size(71.dp)
)
Text(
text = stringResource(R.string.aliucord),
style = MaterialTheme.typography.titleMedium.copy(
fontSize = 26.sp
)
)
Text(
text = stringResource(R.string.app_description),
style = MaterialTheme.typography.titleSmall.copy(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
)
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
TextButton(onClick = { uriHandler.openUri("https://github.com/Aliucord") }) {
Icon(
painter = painterResource(R.drawable.ic_account_github_white_24dp),
contentDescription = stringResource(R.string.github)
)
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
Text(text = stringResource(id = R.string.github))
}
TextButton(onClick = { uriHandler.openUri("https://aliucord.com") }) {
Icon(
painter = painterResource(R.drawable.ic_link),
contentDescription = stringResource(R.string.website)
)
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
Text(text = stringResource(id = R.string.website))
}
}
}
}
if (!viewModel.fetchError) {
item {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier = Modifier
.fillMaxWidth()
.padding(top = 16.dp, bottom = 20.dp)
) {
UserEntry("Vendicated", "the ven")
UserEntry("Juby210", "Fox")
UserEntry("rushii", "explod", "DiamondMiner88")
}
}
contributors(viewModel.contributors)
} else {
item {
Box(
modifier = Modifier.padding(top = 12.dp)
) {
LoadFailure(onRetry = { viewModel.fetchContributors() })
}
}
}
}
}
}
@Composable
private fun UserEntry(name: String, roles: String, username: String = name) {
val uriHandler = LocalUriHandler.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.clickable(
onClick = { uriHandler.openUri("https://github.com/$username") },
indication = rememberRipple(bounded = false, radius = 90.dp),
interactionSource = remember { MutableInteractionSource() }
)
.widthIn(min = 100.dp)
) {
AsyncImage(
modifier = Modifier
.size(71.dp)
.clip(CircleShape),
model = "https://github.com/$username.png",
contentDescription = username
)
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = name,
style = MaterialTheme.typography.titleMedium.copy(
fontSize = 18.sp
)
)
Text(
text = roles,
style = MaterialTheme.typography.titleSmall.copy(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
)
}
}
}

View file

@ -1,60 +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.runtime.*
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.aliucord.manager.ui.component.home.CommitList
import com.aliucord.manager.ui.component.home.InfoCard
import com.aliucord.manager.ui.dialog.InstallerDialog
import com.aliucord.manager.ui.viewmodel.HomeViewModel
import org.koin.androidx.compose.getViewModel
@Composable
fun HomeScreen(
viewModel: HomeViewModel = getViewModel(),
onClickInstall: (InstallData) -> Unit
) {
var showInstallerDialog by remember { mutableStateOf(false) }
LaunchedEffect(viewModel.preferences.packageName) {
viewModel.fetchInstalledVersion()
}
if (showInstallerDialog) {
InstallerDialog(
onDismiss = { showInstallerDialog = false },
onConfirm = { data ->
showInstallerDialog = false
onClickInstall(data)
}
)
}
Column(
modifier = Modifier
.padding(horizontal = 16.dp)
.padding(top = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
InfoCard(
packageName = viewModel.preferences.packageName,
supportedVersion = viewModel.supportedVersion,
currentVersion = viewModel.installedVersion,
onDownloadClick = { showInstallerDialog = true },
onLaunchClick = viewModel::launchAliucord,
onUninstallClick = viewModel::uninstallAliucord
)
CommitList(
commits = viewModel.commits,
onRetry = { viewModel.fetchSupportedVersion() }
)
}
}

View file

@ -1,146 +0,0 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screen
import android.os.Parcelable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.ArrowBack
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
import com.aliucord.manager.ui.component.installer.InstallGroup
import com.aliucord.manager.ui.component.installer.InstallStatus
import com.aliucord.manager.ui.dialog.DownloadMethod
import com.aliucord.manager.ui.viewmodel.InstallViewModel
import com.aliucord.manager.ui.viewmodel.InstallViewModel.InstallStepGroup
import kotlinx.collections.immutable.toImmutableList
import kotlinx.parcelize.Parcelize
import org.koin.androidx.compose.getViewModel
import org.koin.core.parameter.parametersOf
@Immutable // this isn't *really* stable, but this never gets modified after being passed to a composable, so...
@Parcelize
data class InstallData(
val downloadMethod: DownloadMethod,
var baseApk: String? = null,
var splits: List<String>? = null
) : Parcelable
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun InstallerScreen(
installData: InstallData,
onBackClick: () -> Unit,
viewModel: InstallViewModel = getViewModel(parameters = { parametersOf(installData) })
) {
if (viewModel.returnToHome) onBackClick()
var expandedGroup by remember { mutableStateOf<InstallStepGroup?>(null) }
LaunchedEffect(viewModel.currentStep) {
expandedGroup = viewModel.currentStep?.group
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.installer)) },
navigationIcon = {
IconButton(onClick = onBackClick) {
Icon(
imageVector = Icons.Default.ArrowBack,
contentDescription = stringResource(R.string.navigation_back)
)
}
}
)
}
) { paddingValues ->
Column(Modifier.padding(paddingValues)) {
if (viewModel.steps[viewModel.currentStep]?.status == InstallStatus.ONGOING) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(8.dp)
.padding(bottom = 4.dp)
)
}
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(16.dp)
) {
for (group in InstallStepGroup.entries) {
InstallGroup(
name = stringResource(group.nameResId),
isCurrent = expandedGroup == group,
onClick = { expandedGroup = group },
subSteps = viewModel.getSteps(group).toImmutableList(),
)
}
if (viewModel.isFinished && viewModel.stacktrace.isEmpty()) {
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
FilledTonalButton(onClick = viewModel::clearCache) {
Text(stringResource(R.string.setting_clear_cache))
}
}
}
if (viewModel.stacktrace.isNotEmpty()) {
SelectionContainer {
Text(
text = viewModel.stacktrace,
style = MaterialTheme.typography.labelSmall,
fontFamily = FontFamily.Monospace,
softWrap = false,
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(10.dp))
.padding(10.dp)
.horizontalScroll(rememberScrollState())
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End),
modifier = Modifier.fillMaxWidth()
) {
FilledTonalButton(onClick = viewModel::clearCache) {
Text(stringResource(R.string.setting_clear_cache))
}
Spacer(Modifier.weight(1f, true))
OutlinedButton(onClick = viewModel::saveDebugToFile) {
Text(stringResource(R.string.installer_save_file))
}
FilledTonalButton(onClick = viewModel::copyDebugToClipboard) {
Text(stringResource(R.string.action_copy))
}
}
}
}
}
}
}

View file

@ -1,179 +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.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.KeyboardActions
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.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
import com.aliucord.manager.ui.component.plugins.Changelog
import com.aliucord.manager.ui.component.plugins.PluginCard
import com.aliucord.manager.ui.viewmodel.PluginsViewModel
import org.koin.androidx.compose.getViewModel
@OptIn(ExperimentalMaterial3Api::class)
@Composable
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.action_uninstall)
)
},
title = {
Text("${stringResource(R.string.action_uninstall)} ${plugin.manifest.name}")
},
text = {
Text(stringResource(R.string.plugins_delete_plugin_body))
},
confirmButton = {
Button(
onClick = {
viewModel.uninstallPlugin(plugin)
viewModel.hideUninstallDialog()
}
) {
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()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
val focusManager = LocalFocusManager.current
OutlinedTextField(
modifier = Modifier.fillMaxWidth(),
maxLines = 1,
value = viewModel.search,
onValueChange = viewModel::search,
singleLine = true,
shape = RoundedCornerShape(12.dp),
label = { Text(stringResource(R.string.action_search)) },
trailingIcon = {
if (viewModel.search.isNotBlank()) {
IconButton(onClick = viewModel::clearSearch) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.action_clear)
)
}
} else {
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(R.string.plugins_search)
)
}
},
keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Search),
keyboardActions = KeyboardActions { focusManager.clearFocus() }
)
if (viewModel.error) {
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
)
Text(
text = stringResource(R.string.plugins_error),
color = MaterialTheme.colorScheme.error,
)
}
}
} else if (viewModel.plugins.isNotEmpty()) {
LazyColumn(
contentPadding = PaddingValues(bottom = 15.dp, top = 6.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
items(
viewModel.plugins.filter { plugin ->
plugin.manifest.run {
name.contains(viewModel.search, true)
|| description.contains(viewModel.search, true)
|| authors.any { (name) -> name.contains(viewModel.search, true) }
}
}
) { plugin ->
PluginCard(
plugin = plugin,
enabled = viewModel.enabled[plugin.manifest.name] ?: true,
onClickDelete = { viewModel.showUninstallDialog(plugin) },
onClickShowChangelog = { viewModel.showChangelogDialog(plugin) },
onSetEnabled = { viewModel.setPluginEnabled(plugin.manifest.name, it) }
)
}
}
} else {
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
painter = painterResource(R.drawable.ic_extension_off),
contentDescription = null
)
Text(stringResource(R.string.plugins_none_installed))
}
}
}
}
}

View file

@ -1,206 +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.animation.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
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 androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import com.aliucord.manager.R
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
@Composable
fun SettingsScreen(
viewModel: SettingsViewModel = getViewModel()
) {
Column(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
) {
val preferences = viewModel.preferences
if (viewModel.showThemeDialog) {
ThemeDialog(
currentTheme = preferences.theme,
onDismissRequest = viewModel::hideThemeDialog,
onConfirm = viewModel::setTheme
)
}
SettingsHeader(stringResource(R.string.settings_appearance))
SettingsItem(
modifier = Modifier.clickable(onClick = viewModel::showThemeDialog),
icon = { Icon(painterResource(R.drawable.ic_brush), null) },
text = { Text(stringResource(R.string.settings_theme)) }
) {
FilledTonalButton(onClick = viewModel::showThemeDialog) {
Text(preferences.theme.toDisplayName())
}
}
SettingsSwitch(
label = stringResource(R.string.setting_dynamic_color),
pref = preferences.dynamicColor,
icon = { Icon(painterResource(R.drawable.ic_palette), null) }
) {
preferences.dynamicColor = it
}
SettingsHeader(stringResource(R.string.settings_advanced))
SettingsTextField(
label = stringResource(R.string.setting_app_name),
pref = preferences.appName,
onPrefChange = viewModel::setAppName
)
Spacer(modifier = Modifier.height(4.dp))
SettingsSwitch(
label = stringResource(R.string.setting_replace_icon),
secondaryLabel = stringResource(R.string.setting_replace_icon_desc),
pref = preferences.replaceIcon,
icon = { Icon(painterResource(R.drawable.ic_app_shortcut), null) }
) {
preferences.replaceIcon = it
}
SettingsSwitch(
label = stringResource(R.string.setting_keep_patched_apks),
secondaryLabel = stringResource(R.string.setting_keep_patched_apks_desc),
icon = { Icon(painterResource(R.drawable.ic_delete_forever), null) },
pref = preferences.keepPatchedApks,
onPrefChange = { preferences.keepPatchedApks = it },
)
Spacer(modifier = Modifier.height(14.dp))
SettingsSwitch(
label = stringResource(R.string.settings_developer_options),
pref = preferences.devMode,
icon = { Icon(painterResource(R.drawable.ic_code), null) }
) {
preferences.devMode = it
}
AnimatedVisibility(
visible = preferences.devMode,
enter = expandVertically(),
exit = shrinkVertically()
) {
Column(
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
SettingsTextField(
label = stringResource(R.string.setting_package_name),
pref = preferences.packageName,
onPrefChange = viewModel::setPackageName,
error = viewModel.packageNameError
)
SettingsTextField(
label = stringResource(R.string.setting_target_version),
pref = preferences.version,
onPrefChange = viewModel::setVersion
)
SettingsSwitch(
label = stringResource(R.string.setting_debuggable),
secondaryLabel = stringResource(R.string.setting_debuggable_desc),
pref = preferences.debuggable,
icon = { Icon(painterResource(R.drawable.ic_bug), null) },
onPrefChange = { preferences.debuggable = it },
)
SettingsSwitch(
label = stringResource(R.string.setting_hermes_replace_libcpp),
secondaryLabel = stringResource(R.string.setting_hermes_replace_libcpp_desc),
icon = { Icon(painterResource(R.drawable.ic_copy_file), null) },
pref = preferences.hermesReplaceLibCpp,
onPrefChange = { preferences.hermesReplaceLibCpp = it },
)
}
}
Button(
modifier = Modifier
.fillMaxWidth()
.padding(18.dp),
shape = ShapeDefaults.Large,
onClick = viewModel::clearCacheDir
) {
Text(
text = stringResource(R.string.setting_clear_cache),
textAlign = TextAlign.Center
)
}
}
}
@Composable
fun ThemeDialog(
currentTheme: Theme,
onDismissRequest: () -> Unit,
onConfirm: (Theme) -> Unit
) {
var selectedTheme by rememberSaveable { mutableStateOf(currentTheme) }
AlertDialog(
onDismissRequest = onDismissRequest,
icon = {
Icon(
painter = painterResource(R.drawable.ic_brush),
contentDescription = stringResource(R.string.settings_theme)
)
},
title = { Text(stringResource(R.string.settings_theme)) },
text = {
Column {
Theme.values().forEach { theme ->
Row(
modifier = Modifier
.clickable { selectedTheme = theme }
.padding(start = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = theme.toDisplayName(),
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.action_apply))
}
}
)
}

View file

@ -1,16 +1,16 @@
package com.aliucord.manager.ui.viewmodel package com.aliucord.manager.ui.screens.about
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel import cafe.adriel.voyager.core.model.ScreenModel
import androidx.lifecycle.viewModelScope import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.domain.repository.GithubRepository import com.aliucord.manager.domain.repository.GithubRepository
import com.aliucord.manager.network.dto.GithubUser import com.aliucord.manager.network.dto.GithubUser
import com.aliucord.manager.network.utils.fold import com.aliucord.manager.network.utils.fold
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
class AboutViewModel( class AboutModel(
private val githubRepository: GithubRepository private val githubRepository: GithubRepository,
) : ViewModel() { ) : ScreenModel {
val contributors = mutableStateListOf<GithubUser>() val contributors = mutableStateListOf<GithubUser>()
var fetchError by mutableStateOf(false) var fetchError by mutableStateOf(false)
@ -19,13 +19,14 @@ class AboutViewModel(
} }
fun fetchContributors() { fun fetchContributors() {
viewModelScope.launch { screenModelScope.launch {
githubRepository.getContributors().fold( githubRepository.getContributors().fold(
success = { success = {
contributors.clear()
contributors.addAll(it) contributors.addAll(it)
fetchError = false fetchError = false
}, },
fail = { fetchError = true } fail = { fetchError = true },
) )
} }
} }

View file

@ -0,0 +1,208 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screens.about
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material.ripple.rememberRipple
import androidx.compose.material3.*
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
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.compose.ui.unit.sp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import coil.compose.AsyncImage
import com.aliucord.manager.R
import com.aliucord.manager.ui.components.*
class AboutScreen : Screen {
override val key = "About"
@Composable
override fun Content() {
val model = getScreenModel<AboutModel>()
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.navigation_about)) },
navigationIcon = { BackButton() },
)
}
) { paddingValues ->
LazyColumn(
modifier = Modifier.padding(paddingValues),
) {
item(key = "PROJECT_HEADER") {
ProjectHeader()
}
if (!model.fetchError) {
item(key = "MAIN_CONTRIBUTORS") {
MainContributors()
}
item("CONTRIBUTORS_HEADER") {
Text(
text = stringResource(R.string.contributors),
style = MaterialTheme.typography.titleLarge,
color = MaterialTheme.colorScheme.primary,
modifier = Modifier
.padding(start = 16.dp, bottom = 16.dp)
)
}
if (model.contributors.isNotEmpty()) {
items(model.contributors, key = { "user-${it.name}" }) { user ->
ContributorCommitsItem(user)
}
} else {
item {
Box(
modifier = Modifier
.fillMaxWidth()
.padding(16.dp),
contentAlignment = Alignment.Center,
content = { CircularProgressIndicator() }
)
}
}
} else {
item(key = "LOAD_FAILURE") {
Box(
modifier = Modifier.padding(top = 12.dp)
) {
LoadFailure(onRetry = { model.fetchContributors() })
}
}
}
}
}
}
}
@Composable
private fun ProjectHeader(modifier: Modifier = Modifier) {
Column(
horizontalAlignment = Alignment.CenterHorizontally,
modifier = modifier
.fillMaxWidth()
.padding(vertical = 16.dp)
) {
AsyncImage(
model = "https://github.com/Aliucord.png",
contentDescription = stringResource(R.string.aliucord),
modifier = Modifier.size(71.dp)
)
Text(
text = stringResource(R.string.aliucord),
style = MaterialTheme.typography.titleMedium.copy(
fontSize = 26.sp
)
)
Text(
text = stringResource(R.string.app_description),
style = MaterialTheme.typography.titleSmall.copy(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
)
Row(
horizontalArrangement = Arrangement.Center,
modifier = Modifier.fillMaxWidth()
) {
val uriHandler = LocalUriHandler.current
TextButton(onClick = { uriHandler.openUri("https://github.com/Aliucord") }) {
Icon(
painter = painterResource(R.drawable.ic_account_github_white_24dp),
contentDescription = stringResource(R.string.github)
)
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
Text(text = stringResource(id = R.string.github))
}
TextButton(onClick = { uriHandler.openUri("https://aliucord.com") }) {
Icon(
painter = painterResource(R.drawable.ic_link),
contentDescription = stringResource(R.string.website)
)
Spacer(modifier = Modifier.width(ButtonDefaults.IconSpacing))
Text(text = stringResource(id = R.string.website))
}
}
}
}
@Composable
private fun MainContributors(modifier: Modifier = Modifier) {
Row(
horizontalArrangement = Arrangement.SpaceEvenly,
verticalAlignment = Alignment.CenterVertically,
modifier = modifier
.fillMaxWidth()
.padding(top = 16.dp, bottom = 20.dp)
) {
UserEntry("Vendicated", "the ven")
UserEntry("Juby210", "Fox")
UserEntry("rushii", "explod", "DiamondMiner88")
}
}
@Composable
private fun UserEntry(name: String, roles: String, username: String = name) {
val uriHandler = LocalUriHandler.current
Column(
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(16.dp),
modifier = Modifier
.clickable(
onClick = { uriHandler.openUri("https://github.com/$username") },
indication = rememberRipple(bounded = false, radius = 90.dp),
interactionSource = remember { MutableInteractionSource() }
)
.widthIn(min = 100.dp)
) {
AsyncImage(
modifier = Modifier
.size(71.dp)
.clip(CircleShape),
model = "https://github.com/$username.png",
contentDescription = username
)
Column(
horizontalAlignment = Alignment.CenterHorizontally
) {
Text(
text = name,
style = MaterialTheme.typography.titleMedium.copy(
fontSize = 18.sp
)
)
Text(
text = roles,
style = MaterialTheme.typography.titleSmall.copy(
color = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.6f)
)
)
}
}
}

View file

@ -1,12 +1,12 @@
package com.aliucord.manager.ui.viewmodel package com.aliucord.manager.ui.screens.home
import android.app.Application import android.app.Application
import android.content.pm.PackageManager import android.content.pm.PackageManager
import android.util.Log import android.util.Log
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import androidx.paging.* import androidx.paging.*
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.BuildConfig import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.domain.manager.PreferencesManager import com.aliucord.manager.domain.manager.PreferencesManager
@ -19,11 +19,11 @@ import com.aliucord.manager.util.getPackageVersion
import com.aliucord.manager.util.showToast import com.aliucord.manager.util.showToast
import kotlinx.coroutines.* import kotlinx.coroutines.*
class HomeViewModel( class HomeModel(
private val application: Application, private val application: Application,
private val github: GithubRepository, private val github: GithubRepository,
val preferences: PreferencesManager val preferences: PreferencesManager,
) : ViewModel() { ) : ScreenModel {
var supportedVersion by mutableStateOf<DiscordVersion>(DiscordVersion.None) var supportedVersion by mutableStateOf<DiscordVersion>(DiscordVersion.None)
private set private set
@ -54,10 +54,10 @@ class HomeViewModel(
state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1) state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1)
} }
} }
}.flow.cachedIn(viewModelScope) }.flow.cachedIn(screenModelScope)
init { init {
viewModelScope.launch(Dispatchers.IO) { screenModelScope.launch(Dispatchers.IO) {
_fetchInstalledVersion() _fetchInstalledVersion()
_fetchSupportedVersion() _fetchSupportedVersion()
} }
@ -110,13 +110,13 @@ class HomeViewModel(
} }
fun fetchInstalledVersion() { fun fetchInstalledVersion() {
viewModelScope.launch(Dispatchers.IO) { screenModelScope.launch(Dispatchers.IO) {
_fetchInstalledVersion() _fetchInstalledVersion()
} }
} }
fun fetchSupportedVersion() { fun fetchSupportedVersion() {
viewModelScope.launch(Dispatchers.IO) { screenModelScope.launch(Dispatchers.IO) {
_fetchSupportedVersion() _fetchSupportedVersion()
} }
} }

View file

@ -0,0 +1,113 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screens.home
import androidx.compose.foundation.layout.*
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Info
import androidx.compose.material3.*
import androidx.compose.runtime.*
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.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
import com.aliucord.manager.ui.components.dialogs.InstallerDialog
import com.aliucord.manager.ui.components.home.CommitList
import com.aliucord.manager.ui.components.home.InfoCard
import com.aliucord.manager.ui.screens.about.AboutScreen
import com.aliucord.manager.ui.screens.install.InstallScreen
class HomeScreen : Screen {
override val key = "Home"
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val model = getScreenModel<HomeModel>()
var showInstallerDialog by remember { mutableStateOf(false) }
LaunchedEffect(model.preferences.packageName) {
model.fetchInstalledVersion()
}
if (showInstallerDialog) {
InstallerDialog(
onDismiss = { showInstallerDialog = false },
onConfirm = { data ->
showInstallerDialog = false
navigator.push(InstallScreen(data))
}
)
}
// TODO: add a way to open plugins and settings
Scaffold(
topBar = { HomeAppBar() },
) { paddingValues ->
Column(
modifier = Modifier
.fillMaxSize()
.padding(paddingValues)
.padding(horizontal = 16.dp)
.padding(top = 8.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
InfoCard(
packageName = model.preferences.packageName,
supportedVersion = model.supportedVersion,
currentVersion = model.installedVersion,
onDownloadClick = { showInstallerDialog = true },
onLaunchClick = model::launchAliucord,
onUninstallClick = model::uninstallAliucord
)
CommitList(
commits = model.commits,
onRetry = { model.fetchSupportedVersion() }
)
}
}
}
}
@Composable
private fun HomeAppBar() {
TopAppBar(
title = { Text(stringResource(R.string.navigation_home)) },
actions = {
val uriHandler = LocalUriHandler.current
val navigator = LocalNavigator.currentOrThrow
IconButton(
onClick = {
uriHandler.openUri("https://discord.gg/${BuildConfig.SUPPORT_SERVER}")
}
) {
Icon(
painter = painterResource(R.drawable.ic_discord),
contentDescription = stringResource(R.string.support_server)
)
}
IconButton(onClick = { navigator.push(AboutScreen()) }) {
Icon(
imageVector = Icons.Default.Info,
contentDescription = stringResource(R.string.navigation_about)
)
}
}
)
}

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.ui.viewmodel package com.aliucord.manager.ui.screens.install
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.app.Application import android.app.Application
@ -6,8 +6,8 @@ import android.os.Build
import android.util.Log import android.util.Log
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel import cafe.adriel.voyager.core.model.ScreenModel
import androidx.lifecycle.viewModelScope import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.BuildConfig import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.domain.manager.DownloadManager import com.aliucord.manager.domain.manager.DownloadManager
@ -16,9 +16,8 @@ import com.aliucord.manager.domain.repository.AliucordMavenRepository
import com.aliucord.manager.domain.repository.GithubRepository import com.aliucord.manager.domain.repository.GithubRepository
import com.aliucord.manager.installer.util.* import com.aliucord.manager.installer.util.*
import com.aliucord.manager.network.utils.getOrThrow import com.aliucord.manager.network.utils.getOrThrow
import com.aliucord.manager.ui.component.installer.InstallStatus import com.aliucord.manager.ui.components.installer.InstallStatus
import com.aliucord.manager.ui.component.installer.InstallStepData import com.aliucord.manager.ui.components.installer.InstallStepData
import com.aliucord.manager.ui.screen.InstallData
import com.aliucord.manager.util.* import com.aliucord.manager.util.*
import com.github.diamondminer88.zip.* import com.github.diamondminer88.zip.*
import kotlinx.coroutines.* import kotlinx.coroutines.*
@ -27,14 +26,14 @@ import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.measureTimedValue import kotlin.time.measureTimedValue
class InstallViewModel( class InstallModel(
private val application: Application, private val application: Application,
private val downloadManager: DownloadManager, private val downloadManager: DownloadManager,
private val preferences: PreferencesManager, private val preferences: PreferencesManager,
private val githubRepository: GithubRepository, private val githubRepository: GithubRepository,
private val aliucordMaven: AliucordMavenRepository, private val aliucordMaven: AliucordMavenRepository,
private val installData: InstallData private val installData: InstallData,
) : ViewModel() { ) : ScreenModel {
private val externalCacheDir = application.externalCacheDir!! private val externalCacheDir = application.externalCacheDir!!
private val installationRunning = AtomicBoolean(false) private val installationRunning = AtomicBoolean(false)
@ -86,7 +85,7 @@ class InstallViewModel(
application.showToast(R.string.action_cleared_cache) application.showToast(R.string.action_cleared_cache)
} }
private val installJob = viewModelScope.launch(Dispatchers.Main) { private val installJob = screenModelScope.launch(Dispatchers.Main) {
if (installationRunning.getAndSet(true)) { if (installationRunning.getAndSet(true)) {
return@launch return@launch
} }
@ -137,7 +136,7 @@ class InstallViewModel(
} }
} }
override fun onCleared() { override fun onDispose() {
if (installationRunning.getAndSet(false)) { if (installationRunning.getAndSet(false)) {
installJob.cancel("ViewModel cleared") installJob.cancel("ViewModel cleared")
} }
@ -375,7 +374,7 @@ class InstallViewModel(
enum class InstallStepGroup( enum class InstallStepGroup(
@StringRes @StringRes
val nameResId: Int val nameResId: Int,
) { ) {
APK_DL(R.string.install_group_apk_dl), APK_DL(R.string.install_group_apk_dl),
LIB_DL(R.string.install_group_lib_dl), LIB_DL(R.string.install_group_lib_dl),
@ -388,7 +387,7 @@ class InstallViewModel(
val group: InstallStepGroup, val group: InstallStepGroup,
@StringRes @StringRes
val nameResId: Int val nameResId: Int,
) { ) {
// Kotlin // Kotlin
FETCH_KT_VERSION(InstallStepGroup.APK_DL, R.string.install_step_fetch_kt_version), FETCH_KT_VERSION(InstallStepGroup.APK_DL, R.string.install_step_fetch_kt_version),

View file

@ -0,0 +1,153 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screens.install
import android.os.Parcelable
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.selection.SelectionContainer
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import cafe.adriel.voyager.navigator.LocalNavigator
import cafe.adriel.voyager.navigator.currentOrThrow
import com.aliucord.manager.R
import com.aliucord.manager.ui.components.BackButton
import com.aliucord.manager.ui.components.back
import com.aliucord.manager.ui.components.dialogs.DownloadMethod
import com.aliucord.manager.ui.components.installer.InstallGroup
import com.aliucord.manager.ui.components.installer.InstallStatus
import com.aliucord.manager.ui.screens.install.InstallModel.InstallStepGroup
import kotlinx.collections.immutable.toImmutableList
import kotlinx.parcelize.Parcelize
import org.koin.core.parameter.parametersOf
@Immutable // this isn't *really* stable, but this never gets modified after being passed to a composable, so...
@Parcelize
data class InstallData(
val downloadMethod: DownloadMethod,
var baseApk: String? = null,
var splits: List<String>? = null,
) : Parcelable
class InstallScreen(val data: InstallData) : Screen {
override val key = "Install"
@Composable
override fun Content() {
val navigator = LocalNavigator.currentOrThrow
val model = getScreenModel<InstallModel>(parameters = { parametersOf(data) })
var expandedGroup by remember { mutableStateOf<InstallStepGroup?>(null) }
if (model.returnToHome)
navigator.back(null)
LaunchedEffect(model.currentStep) {
expandedGroup = model.currentStep?.group
}
Scaffold(
topBar = {
TopAppBar(
title = { Text(stringResource(R.string.installer)) },
navigationIcon = {
// TODO: add confirm to exit dialog to button as well as BackHandler
BackButton()
}
)
}
) { paddingValues ->
Column(Modifier.padding(paddingValues)) {
val isCurrentlyProcessing by remember {
derivedStateOf {
model.steps[model.currentStep]?.status == InstallStatus.ONGOING
}
}
if (isCurrentlyProcessing) {
LinearProgressIndicator(
modifier = Modifier
.fillMaxWidth()
.height(8.dp)
.padding(bottom = 4.dp)
)
}
Column(
verticalArrangement = Arrangement.spacedBy(12.dp),
modifier = Modifier
.verticalScroll(rememberScrollState())
.fillMaxWidth()
.padding(16.dp)
) {
for (group in InstallStepGroup.entries) key(group) {
InstallGroup(
name = stringResource(group.nameResId),
isCurrent = expandedGroup == group,
onClick = { expandedGroup = group },
subSteps = model.getSteps(group).toImmutableList(),
)
}
if (model.isFinished && model.stacktrace.isEmpty()) {
Row(
horizontalArrangement = Arrangement.End,
modifier = Modifier.fillMaxWidth()
) {
FilledTonalButton(onClick = model::clearCache) {
Text(stringResource(R.string.setting_clear_cache))
}
}
}
if (model.stacktrace.isNotEmpty()) {
SelectionContainer {
Text(
text = model.stacktrace,
style = MaterialTheme.typography.labelSmall,
fontFamily = FontFamily.Monospace,
softWrap = false,
modifier = Modifier
.clip(RoundedCornerShape(10.dp))
.background(MaterialTheme.colorScheme.surfaceColorAtElevation(10.dp))
.padding(10.dp)
.horizontalScroll(rememberScrollState())
)
}
Row(
horizontalArrangement = Arrangement.spacedBy(10.dp, Alignment.End),
modifier = Modifier.fillMaxWidth()
) {
FilledTonalButton(onClick = model::clearCache) {
Text(stringResource(R.string.setting_clear_cache))
}
Spacer(Modifier.weight(1f, true))
OutlinedButton(onClick = model::saveDebugToFile) {
Text(stringResource(R.string.installer_save_file))
}
FilledTonalButton(onClick = model::copyDebugToClipboard) {
Text(stringResource(R.string.action_copy))
}
}
}
}
}
}
}
}

View file

@ -1,9 +1,9 @@
package com.aliucord.manager.ui.viewmodel package com.aliucord.manager.ui.screens.plugins
import android.util.Log import android.util.Log
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel import cafe.adriel.voyager.core.model.ScreenModel
import androidx.lifecycle.viewModelScope import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.BuildConfig import com.aliucord.manager.BuildConfig
import com.aliucord.manager.aliucordDir import com.aliucord.manager.aliucordDir
import com.aliucord.manager.domain.model.Plugin import com.aliucord.manager.domain.model.Plugin
@ -11,8 +11,7 @@ import kotlinx.coroutines.*
import kotlinx.serialization.ExperimentalSerializationApi import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.* import kotlinx.serialization.json.*
@OptIn(ExperimentalSerializationApi::class) class PluginsModel : ScreenModel {
class PluginsViewModel : ViewModel() {
var showChangelogDialog by mutableStateOf(false) var showChangelogDialog by mutableStateOf(false)
private set private set
@ -35,10 +34,6 @@ class PluginsViewModel : ViewModel() {
this.search = search this.search = search
} }
fun clearSearch() {
search = ""
}
fun showChangelogDialog(plugin: Plugin) { fun showChangelogDialog(plugin: Plugin) {
showChangelogDialog = true showChangelogDialog = true
selectedPlugin = plugin selectedPlugin = plugin
@ -65,13 +60,14 @@ class PluginsViewModel : ViewModel() {
private val settingsFile = aliucordDir.resolve("settings/Aliucord.json") private val settingsFile = aliucordDir.resolve("settings/Aliucord.json")
fun setPluginEnabled(plugin: String, enable: Boolean) { fun setPluginEnabled(plugin: String, enable: Boolean) {
viewModelScope.launch(Dispatchers.IO) { screenModelScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) { withContext(Dispatchers.Main) {
enabled.clear() enabled.clear()
enabled.putAll(loadEnabled()) enabled.putAll(loadEnabled())
enabled[plugin] = enable enabled[plugin] = enable
} }
@OptIn(ExperimentalSerializationApi::class)
try { try {
val settings = settingsFile.inputStream() val settings = settingsFile.inputStream()
.use { Json.decodeFromStream<Map<String, JsonElement>>(it) } .use { Json.decodeFromStream<Map<String, JsonElement>>(it) }
@ -91,6 +87,7 @@ class PluginsViewModel : ViewModel() {
private suspend fun loadEnabled(): Map<String, Boolean> = withContext(Dispatchers.IO) { private suspend fun loadEnabled(): Map<String, Boolean> = withContext(Dispatchers.IO) {
if (!settingsFile.exists()) return@withContext emptyMap() if (!settingsFile.exists()) return@withContext emptyMap()
@OptIn(ExperimentalSerializationApi::class)
try { try {
settingsFile.inputStream() settingsFile.inputStream()
.use { Json.decodeFromStream<Map<String, JsonElement>>(it) } .use { Json.decodeFromStream<Map<String, JsonElement>>(it) }
@ -105,11 +102,11 @@ class PluginsViewModel : ViewModel() {
} }
init { init {
viewModelScope.launch { screenModelScope.launch {
enabled.putAll(loadEnabled()) enabled.putAll(loadEnabled())
} }
viewModelScope.launch(Dispatchers.IO) { screenModelScope.launch(Dispatchers.IO) {
val pluginsDir = aliucordDir.resolve("plugins") val pluginsDir = aliucordDir.resolve("plugins")
if (!pluginsDir.exists() && !pluginsDir.mkdirs()) { if (!pluginsDir.exists() && !pluginsDir.mkdirs()) {

View file

@ -0,0 +1,207 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screens.plugins
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.KeyboardActions
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.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.input.ImeAction
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import com.aliucord.manager.R
import com.aliucord.manager.ui.components.plugins.Changelog
import com.aliucord.manager.ui.components.plugins.PluginCard
class PluginsScreen : Screen {
override val key = "Plugins"
@Composable
override fun Content() {
val model = getScreenModel<PluginsModel>()
if (model.showUninstallDialog) {
val plugin = model.selectedPlugin!!
UninstallPluginDialog(
pluginName = plugin.manifest.name,
onConfirm = { model.uninstallPlugin(plugin) },
onDismiss = model::hideUninstallDialog
)
}
if (model.showChangelogDialog) {
Changelog(
plugin = model.selectedPlugin!!,
onDismiss = model::hideChangelogDialog
)
}
Column(
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
PluginSearch(
currentFilter = model.search,
onFilterChange = model::search,
modifier = Modifier.fillMaxWidth(),
)
if (model.error) {
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.spacedBy(8.dp),
) {
Icon(
imageVector = Icons.Default.Warning,
contentDescription = null,
tint = MaterialTheme.colorScheme.error,
)
Text(
text = stringResource(R.string.plugins_error),
color = MaterialTheme.colorScheme.error,
)
}
}
} else if (model.plugins.isNotEmpty()) {
LazyColumn(
contentPadding = PaddingValues(bottom = 15.dp, top = 6.dp),
verticalArrangement = Arrangement.spacedBy(14.dp)
) {
items(
// TODO: remember {} this
model.plugins.filter { plugin ->
plugin.manifest.run {
name.contains(model.search, true)
|| description.contains(model.search, true)
|| authors.any { (name) -> name.contains(model.search, true) }
}
}
) { plugin ->
PluginCard(
plugin = plugin,
enabled = model.enabled[plugin.manifest.name] ?: true,
onClickDelete = { model.showUninstallDialog(plugin) },
onClickShowChangelog = { model.showChangelogDialog(plugin) },
onSetEnabled = { model.setPluginEnabled(plugin.manifest.name, it) }
)
}
}
} else {
Box(
modifier = Modifier.fillMaxSize()
) {
Column(
modifier = Modifier.align(Alignment.Center),
horizontalAlignment = Alignment.CenterHorizontally
) {
Icon(
painter = painterResource(R.drawable.ic_extension_off),
contentDescription = null
)
Text(stringResource(R.string.plugins_none_installed))
}
}
}
}
}
}
@Composable
private fun PluginSearch(
currentFilter: String,
onFilterChange: (String) -> Unit,
modifier: Modifier = Modifier,
) {
val focusManager = LocalFocusManager.current
OutlinedTextField(
value = currentFilter,
onValueChange = onFilterChange,
singleLine = true,
shape = RoundedCornerShape(12.dp),
label = { Text(stringResource(R.string.action_search)) },
trailingIcon = {
val isFilterBlank by remember { derivedStateOf { currentFilter.isEmpty() } }
if (!isFilterBlank) {
IconButton(onClick = { onFilterChange("") }) {
Icon(
imageVector = Icons.Default.Close,
contentDescription = stringResource(R.string.action_clear)
)
}
} else {
Icon(
imageVector = Icons.Default.Search,
contentDescription = stringResource(R.string.plugins_search)
)
}
},
keyboardOptions = KeyboardOptions(autoCorrect = false, imeAction = ImeAction.Search),
keyboardActions = KeyboardActions { focusManager.clearFocus() },
modifier = modifier,
)
}
@Composable
private fun UninstallPluginDialog(
pluginName: String,
onConfirm: () -> Unit,
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
icon = {
Icon(
imageVector = Icons.Default.Delete,
contentDescription = stringResource(R.string.action_uninstall)
)
},
title = {
Text("${stringResource(R.string.action_uninstall)} $pluginName")
},
text = {
Text(stringResource(R.string.plugins_delete_plugin_body))
},
confirmButton = {
Button(
onClick = onConfirm,
) {
Text(stringResource(android.R.string.ok))
}
},
dismissButton = {
Button(
onClick = onDismiss,
colors = ButtonDefaults.buttonColors(
contentColor = MaterialTheme.colorScheme.onSecondaryContainer,
containerColor = MaterialTheme.colorScheme.secondaryContainer
)
) {
Text(stringResource(android.R.string.cancel))
}
}
)
}

View file

@ -1,20 +1,20 @@
package com.aliucord.manager.ui.viewmodel package com.aliucord.manager.ui.screens.settings
import android.app.Application import android.app.Application
import androidx.compose.runtime.* import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel import cafe.adriel.voyager.core.model.ScreenModel
import androidx.lifecycle.viewModelScope import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.domain.manager.PreferencesManager import com.aliucord.manager.domain.manager.PreferencesManager
import com.aliucord.manager.ui.theme.Theme import com.aliucord.manager.ui.components.Theme
import com.aliucord.manager.util.showToast import com.aliucord.manager.util.showToast
import com.aliucord.manager.util.throttle import com.aliucord.manager.util.throttle
class SettingsViewModel( class SettingsModel(
private val application: Application, private val application: Application,
val preferences: PreferencesManager val preferences: PreferencesManager,
) : ViewModel() { ) : ScreenModel {
private val checkPackageNameThrottled = throttle(50L, viewModelScope) { private val checkPackageNameThrottled = throttle(50L, screenModelScope) {
packageNameError = !PACKAGE_REGEX.matches(preferences.packageName) packageNameError = !PACKAGE_REGEX.matches(preferences.packageName)
} }
@ -58,7 +58,7 @@ class SettingsViewModel(
checkPackageNameThrottled() checkPackageNameThrottled()
} }
override fun onCleared() { override fun onDispose() {
if (!PACKAGE_REGEX.matches(preferences.packageName)) { if (!PACKAGE_REGEX.matches(preferences.packageName)) {
preferences.packageName = "com.aliucord" preferences.packageName = "com.aliucord"
application.showToast(R.string.settings_invalid_package_name) application.showToast(R.string.settings_invalid_package_name)

View file

@ -0,0 +1,210 @@
/*
* Copyright (c) 2022 Juby210 & zt
* Licensed under the Open Software License version 3.0
*/
package com.aliucord.manager.ui.screens.settings
import androidx.compose.animation.*
import androidx.compose.foundation.*
import androidx.compose.foundation.layout.*
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 androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.unit.dp
import cafe.adriel.voyager.core.screen.Screen
import cafe.adriel.voyager.koin.getScreenModel
import com.aliucord.manager.R
import com.aliucord.manager.ui.components.Theme
import com.aliucord.manager.ui.components.settings.*
class SettingsScreen : Screen {
override val key = "Settings"
@Composable
override fun Content() {
val model = getScreenModel<SettingsModel>()
Column(
modifier = Modifier
.verticalScroll(state = rememberScrollState())
) {
val preferences = model.preferences
if (model.showThemeDialog) {
ThemeDialog(
currentTheme = preferences.theme,
onDismissRequest = model::hideThemeDialog,
onConfirm = model::setTheme
)
}
SettingsHeader(stringResource(R.string.settings_appearance))
SettingsItem(
modifier = Modifier.clickable(onClick = model::showThemeDialog),
icon = { Icon(painterResource(R.drawable.ic_brush), null) },
text = { Text(stringResource(R.string.settings_theme)) }
) {
FilledTonalButton(onClick = model::showThemeDialog) {
Text(preferences.theme.toDisplayName())
}
}
SettingsSwitch(
label = stringResource(R.string.setting_dynamic_color),
pref = preferences.dynamicColor,
icon = { Icon(painterResource(R.drawable.ic_palette), null) }
) {
preferences.dynamicColor = it
}
SettingsHeader(stringResource(R.string.settings_advanced))
SettingsTextField(
label = stringResource(R.string.setting_app_name),
pref = preferences.appName,
onPrefChange = model::setAppName
)
Spacer(modifier = Modifier.height(4.dp))
SettingsSwitch(
label = stringResource(R.string.setting_replace_icon),
secondaryLabel = stringResource(R.string.setting_replace_icon_desc),
pref = preferences.replaceIcon,
icon = { Icon(painterResource(R.drawable.ic_app_shortcut), null) }
) {
preferences.replaceIcon = it
}
SettingsSwitch(
label = stringResource(R.string.setting_keep_patched_apks),
secondaryLabel = stringResource(R.string.setting_keep_patched_apks_desc),
icon = { Icon(painterResource(R.drawable.ic_delete_forever), null) },
pref = preferences.keepPatchedApks,
onPrefChange = { preferences.keepPatchedApks = it },
)
Spacer(modifier = Modifier.height(14.dp))
SettingsSwitch(
label = stringResource(R.string.settings_developer_options),
pref = preferences.devMode,
icon = { Icon(painterResource(R.drawable.ic_code), null) }
) {
preferences.devMode = it
}
AnimatedVisibility(
visible = preferences.devMode,
enter = expandVertically(),
exit = shrinkVertically()
) {
Column(
verticalArrangement = Arrangement.spacedBy(6.dp)
) {
SettingsTextField(
label = stringResource(R.string.setting_package_name),
pref = preferences.packageName,
onPrefChange = model::setPackageName,
error = model.packageNameError
)
SettingsTextField(
label = stringResource(R.string.setting_target_version),
pref = preferences.version,
onPrefChange = model::setVersion
)
SettingsSwitch(
label = stringResource(R.string.setting_debuggable),
secondaryLabel = stringResource(R.string.setting_debuggable_desc),
pref = preferences.debuggable,
icon = { Icon(painterResource(R.drawable.ic_bug), null) },
onPrefChange = { preferences.debuggable = it },
)
SettingsSwitch(
label = stringResource(R.string.setting_hermes_replace_libcpp),
secondaryLabel = stringResource(R.string.setting_hermes_replace_libcpp_desc),
icon = { Icon(painterResource(R.drawable.ic_copy_file), null) },
pref = preferences.hermesReplaceLibCpp,
onPrefChange = { preferences.hermesReplaceLibCpp = it },
)
}
}
Button(
modifier = Modifier
.fillMaxWidth()
.padding(18.dp),
shape = ShapeDefaults.Large,
onClick = model::clearCacheDir
) {
Text(
text = stringResource(R.string.setting_clear_cache),
textAlign = TextAlign.Center
)
}
}
}
}
@Composable
fun ThemeDialog(
currentTheme: Theme,
onDismissRequest: () -> Unit,
onConfirm: (Theme) -> Unit,
) {
var selectedTheme by rememberSaveable { mutableStateOf(currentTheme) }
AlertDialog(
onDismissRequest = onDismissRequest,
icon = {
Icon(
painter = painterResource(R.drawable.ic_brush),
contentDescription = stringResource(R.string.settings_theme)
)
},
title = { Text(stringResource(R.string.settings_theme)) },
text = {
Column {
Theme.entries.forEach { theme ->
Row(
modifier = Modifier
.clickable { selectedTheme = theme }
.padding(start = 8.dp),
verticalAlignment = Alignment.CenterVertically
) {
Text(
text = theme.toDisplayName(),
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.action_apply))
}
}
)
}

View file

@ -16,7 +16,7 @@ inline fun <T> Iterable<T>.joinToAnnotatedString(
is Char -> append(it) is Char -> append(it)
else -> append(it.toString()) else -> append(it.toString())
} }
} },
) { ) {
prefix(this@Builder) prefix(this@Builder)
for ((count, element) in this@joinToAnnotatedString.withIndex()) { for ((count, element) in this@joinToAnnotatedString.withIndex()) {

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.ui.dialog package com.aliucord.manager.ui.widgets.updater
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.* import androidx.compose.material3.*
@ -8,12 +8,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
import com.aliucord.manager.R import com.aliucord.manager.R
import com.aliucord.manager.ui.viewmodel.UpdaterViewModel
import org.koin.androidx.compose.getViewModel import org.koin.androidx.compose.getViewModel
@Composable @Composable
fun UpdaterDialog( fun UpdaterDialog(
viewModel: UpdaterViewModel = getViewModel() viewModel: UpdaterViewModel = getViewModel(),
) { ) {
if (!viewModel.showDialog) return if (!viewModel.showDialog) return

View file

@ -1,4 +1,4 @@
package com.aliucord.manager.ui.viewmodel package com.aliucord.manager.ui.widgets.updater
import android.app.Application import android.app.Application
import androidx.compose.runtime.* import androidx.compose.runtime.*

View file

@ -8,7 +8,7 @@ import kotlinx.coroutines.*
inline fun throttle( inline fun throttle(
skipMs: Long, skipMs: Long,
coroutineScope: CoroutineScope, coroutineScope: CoroutineScope,
crossinline destinationFunction: suspend () -> Unit crossinline destinationFunction: suspend () -> Unit,
): () -> Unit { ): () -> Unit {
var throttleJob: Job? = null var throttleJob: Job? = null
return { return {

View file

@ -17,7 +17,7 @@ kotlin = "1.9.22"
kotlinx-immutable = "0.3.7" kotlinx-immutable = "0.3.7"
kotlinx-serialization = "1.5.0" kotlinx-serialization = "1.5.0"
ktor = "2.3.7" ktor = "2.3.7"
navigation-reimagined = "1.3.0" # TODO: migrate to voyager voyager = "1.0.0"
zip = "2.1.0" zip = "2.1.0"
[libraries] [libraries]
@ -58,12 +58,16 @@ ktor-logging = { module = "io.ktor:ktor-client-logging", version.ref = "ktor" }
ktor-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" } ktor-content-negotiation = { module = "io.ktor:ktor-client-content-negotiation", version.ref = "ktor" }
ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" } ktor-serialization-json = { module = "io.ktor:ktor-serialization-kotlinx-json", version.ref = "ktor" }
# Voyager
voyager-navigator = { module = "cafe.adriel.voyager:voyager-navigator", version.ref = "voyager" }
voyager-transitions = { module = "cafe.adriel.voyager:voyager-transitions", version.ref = "voyager" }
voyager-koin = { module = "cafe.adriel.voyager:voyager-koin", version.ref = "voyager" }
# Other # Other
apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" } apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
axml = { module = "com.aliucord:axml", version.ref = "axml" } axml = { module = "com.aliucord:axml", version.ref = "axml" }
bouncycastle = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bouncycastle" } bouncycastle = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bouncycastle" }
coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" } coil = { module = "io.coil-kt:coil-compose", version.ref = "coil" }
navigationReimagined = { module = "dev.olshevski.navigation:reimagined", version.ref = "navigation-reimagined" }
zip = { module = "io.github.diamondminer88:zip-android", version.ref = "zip" } # Only use the "aar" artifact zip = { module = "io.github.diamondminer88:zip-android", version.ref = "zip" } # Only use the "aar" artifact
[bundles] [bundles]
@ -95,6 +99,11 @@ ktor = [
"ktor-content-negotiation", "ktor-content-negotiation",
"ktor-serialization-json", "ktor-serialization-json",
] ]
voyager = [
"voyager-koin",
"voyager-navigator",
"voyager-transitions",
]
[plugins] [plugins]
android-application = { id = "com.android.application", version.ref = "agp" } android-application = { id = "com.android.application", version.ref = "agp" }