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 - 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)

View file

@ -90,7 +90,10 @@ android {
jvmTarget = "11"
freeCompilerArgs += listOf(
"-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.koin)
implementation(libs.bundles.ktor)
implementation(libs.bundles.voyager)
implementation(libs.compose.ui.tooling.preview)
debugImplementation(libs.compose.ui.tooling)
@ -122,7 +126,6 @@ dependencies {
implementation(libs.axml)
implementation(libs.bouncycastle)
implementation(libs.coil)
implementation(libs.navigationReimagined)
implementation(variantOf(libs.zip) { artifactType("aar") })
}

View file

@ -12,17 +12,16 @@ import androidx.activity.compose.BackHandler
import androidx.activity.compose.setContent
import androidx.compose.foundation.isSystemInDarkTheme
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.ui.dialog.StoragePermissionsDialog
import com.aliucord.manager.ui.dialog.UpdaterDialog
import com.aliucord.manager.ui.navigation.*
import com.aliucord.manager.ui.screen.*
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 com.aliucord.manager.ui.components.*
import com.aliucord.manager.ui.components.dialogs.StoragePermissionsDialog
import com.aliucord.manager.ui.screens.home.HomeScreen
import com.aliucord.manager.ui.widgets.updater.UpdaterDialog
import org.koin.android.ext.android.inject
// TODO: move to a path provider in DI
val aliucordDir = Environment.getExternalStorageDirectory().resolve("Aliucord")
class MainActivity : ComponentActivity() {
@ -38,12 +37,6 @@ class MainActivity : ComponentActivity() {
isDarkTheme = preferences.theme == Theme.DARK || preferences.theme == Theme.SYSTEM && isSystemInDarkTheme(),
isDynamicColor = preferences.dynamicColor
) {
val navController = rememberNavController<AppDestination>(AppDestination.Home)
BackHandler {
navController.back()
}
StoragePermissionsDialog()
@Suppress("KotlinConstantConditions")
@ -55,36 +48,15 @@ class MainActivity : ComponentActivity() {
UpdaterDialog()
}
NavHost(
controller = navController,
) {
when (val dest = this.currentHostEntry.destination) {
is BaseScreenDestination -> BaseScreen(
currentNavItem = dest,
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() }
)
Navigator(
screen = HomeScreen(),
onBackPressed = null,
) { navigator ->
BackHandler {
navigator.back(this@MainActivity)
}
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.GithubRepository
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.androidx.viewmodel.dsl.viewModelOf
import org.koin.core.context.startKoin
import org.koin.core.module.dsl.factoryOf
import org.koin.core.module.dsl.singleOf
import org.koin.dsl.module
@ -39,13 +45,13 @@ class ManagerApplication : Application() {
singleOf(::AliucordMavenRepository)
})
// ViewModels
// UI Models
modules(module {
viewModelOf(::HomeViewModel)
viewModelOf(::PluginsViewModel)
viewModelOf(::AboutViewModel)
viewModelOf(::InstallViewModel)
viewModelOf(::SettingsViewModel)
factoryOf(::HomeModel)
factoryOf(::PluginsModel)
factoryOf(::AboutModel)
factoryOf(::InstallModel)
factoryOf(::SettingsModel)
viewModelOf(::UpdaterViewModel)
})

View file

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

View file

@ -2,7 +2,7 @@ package com.aliucord.manager.domain.manager
import android.content.SharedPreferences
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) {
var theme by enumPreference("theme", Theme.SYSTEM)

View file

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

View file

@ -10,13 +10,12 @@ import com.github.diamondminer88.zip.ZipReader
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.Json
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
private val json: Json = GlobalContext.get().get()
@Immutable
data class Plugin(val file: File) {
data class Plugin(val file: File) : KoinComponent {
val manifest: Manifest
init {
@ -25,7 +24,7 @@ data class Plugin(val file: File) {
?: throw Exception("Plugin ${file.nameWithoutExtension} has no manifest.")
@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
class AliucordMavenRepository(
private val maven: MavenService
private val maven: MavenService,
) {
suspend fun getAliuhookVersion(): ApiResponse<String> {
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
class GithubRepository(
private val service: AliucordGithubService
private val service: AliucordGithubService,
) {
suspend fun getCommits(page: Int = 0) = service.getCommits(page)
suspend fun getContributors() = service.getContributors()

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -8,7 +8,7 @@ import kotlinx.coroutines.withContext
class AliucordGithubService(
val github: GithubService,
val http: HttpService
val http: HttpService,
) {
suspend fun getDataJson(): ApiResponse<Version> {
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(
success: (T) -> R,
error: (ApiError) -> R,
failure: (ApiFailure) -> R
failure: (ApiFailure) -> R,
): R {
return when (this) {
is ApiResponse.Success -> success(this.data)

View file

@ -7,7 +7,7 @@ data class SemVer(
val major: Int,
val minor: Int,
val patch: Int,
private val vPrefix: Boolean = false
private val vPrefix: Boolean = false,
) : Comparable<SemVer> {
override fun compareTo(other: SemVer): Int {
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
*/
package com.aliucord.manager.ui.component
package com.aliucord.manager.ui.components
import androidx.compose.foundation.clickable
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.material3.*
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
@ -22,41 +21,15 @@ import coil.compose.AsyncImage
import com.aliucord.manager.R
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
fun ContributorEntry(user: GithubUser) {
fun ContributorCommitsItem(
user: GithubUser,
modifier: Modifier = Modifier,
) {
val uriHandler = LocalUriHandler.current
Row(
modifier = Modifier
modifier = modifier
.clickable {
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.material3.*

View file

@ -3,7 +3,7 @@
* 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 androidx.compose.foundation.isSystemInDarkTheme
@ -19,7 +19,7 @@ import com.google.accompanist.systemuicontroller.rememberSystemUiController
fun ManagerTheme(
isDarkTheme: Boolean = isSystemInDarkTheme(),
isDynamicColor: Boolean = true,
content: @Composable () -> Unit
content: @Composable () -> Unit,
) {
val dynamicColor = isDynamicColor && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S
val colorScheme = when {

View file

@ -3,18 +3,18 @@
* 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.saveable.rememberSaveable
import com.aliucord.manager.ui.screen.InstallData
import com.aliucord.manager.ui.screens.install.InstallData
enum class DownloadMethod { DOWNLOAD }
@Composable
fun InstallerDialog(
onDismiss: () -> Unit,
onConfirm: (InstallData) -> Unit
onConfirm: (InstallData) -> Unit,
) {
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.content.Intent
@ -7,6 +7,7 @@ import android.os.Environment
import android.provider.Settings
import androidx.activity.compose.rememberLauncherForActivityResult
import androidx.activity.result.contract.ActivityResultContracts
import androidx.annotation.RequiresApi
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.ui.res.stringResource
@ -26,6 +27,7 @@ fun StoragePermissionsDialog() {
}
@Composable
@RequiresApi(Build.VERSION_CODES.R)
private fun ManageStorageDialog() {
var manageStorageGranted by remember { mutableStateOf(Environment.isExternalStorageManager()) }
@ -59,8 +61,8 @@ private fun ManageStorageDialog() {
}
}
@OptIn(ExperimentalPermissionsApi::class)
@Composable
@OptIn(ExperimentalPermissionsApi::class)
private fun ExternalStorageDialog() {
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.layout.*
@ -22,7 +22,7 @@ import androidx.paging.compose.items
import coil.compose.AsyncImage
import com.aliucord.manager.R
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 kotlinx.collections.immutable.persistentListOf
import kotlinx.coroutines.flow.Flow
@ -30,7 +30,7 @@ import kotlinx.coroutines.flow.Flow
@Composable
fun CommitList(
commits: Flow<PagingData<Commit>>,
onRetry: () -> Unit
onRetry: () -> Unit,
) {
ElevatedCard(
modifier = Modifier.padding(bottom = 16.dp)
@ -113,7 +113,7 @@ fun CommitList(
@Composable
fun CommitItem(
commit: Commit
commit: Commit,
) {
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.material.icons.Icons
@ -23,7 +23,7 @@ fun InfoCard(
currentVersion: DiscordVersion,
onDownloadClick: () -> Unit,
onUninstallClick: () -> Unit,
onLaunchClick: () -> Unit
onLaunchClick: () -> Unit,
) {
ElevatedCard {
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.foundation.background
@ -35,7 +35,7 @@ class InstallStepData(
val nameResId: Int,
status: InstallStatus,
duration: Float = 0f,
cached: Boolean = false
cached: Boolean = false,
) {
var status by mutableStateOf(status)
var duration by mutableStateOf(duration)
@ -47,7 +47,7 @@ fun InstallGroup(
name: String,
isCurrent: Boolean,
subSteps: ImmutableList<InstallStepData>,
onClick: () -> Unit
onClick: () -> Unit,
) {
val status = when {
subSteps.all { it.status == InstallStatus.QUEUED } ->

View file

@ -3,7 +3,7 @@
* 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.lazy.LazyColumn
@ -64,7 +64,7 @@ private fun AnnotatedString.Builder.hyperlink(content: String) {
@Composable
fun Changelog(
plugin: Plugin,
onDismiss: () -> Unit
onDismiss: () -> Unit,
) {
AlertDialog(
onDismissRequest = onDismiss,
@ -164,7 +164,7 @@ fun Changelog(
@Composable
private fun LinkText(
modifier: Modifier = Modifier,
annotatedString: AnnotatedString
annotatedString: AnnotatedString,
) {
val urlHandler = LocalUriHandler.current

View file

@ -3,7 +3,7 @@
* 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.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.material3.MaterialTheme
@ -9,7 +9,7 @@ import androidx.compose.ui.unit.dp
@Composable
fun SettingsHeader(
text: String
text: String,
) {
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.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.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.material3.*
import androidx.compose.material3.OutlinedTextField
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun SettingsTextField(
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.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.domain.repository.GithubRepository
import com.aliucord.manager.network.dto.GithubUser
import com.aliucord.manager.network.utils.fold
import kotlinx.coroutines.launch
class AboutViewModel(
private val githubRepository: GithubRepository
) : ViewModel() {
class AboutModel(
private val githubRepository: GithubRepository,
) : ScreenModel {
val contributors = mutableStateListOf<GithubUser>()
var fetchError by mutableStateOf(false)
@ -19,13 +19,14 @@ class AboutViewModel(
}
fun fetchContributors() {
viewModelScope.launch {
screenModelScope.launch {
githubRepository.getContributors().fold(
success = {
contributors.clear()
contributors.addAll(it)
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.content.pm.PackageManager
import android.util.Log
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
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.R
import com.aliucord.manager.domain.manager.PreferencesManager
@ -19,11 +19,11 @@ import com.aliucord.manager.util.getPackageVersion
import com.aliucord.manager.util.showToast
import kotlinx.coroutines.*
class HomeViewModel(
class HomeModel(
private val application: Application,
private val github: GithubRepository,
val preferences: PreferencesManager
) : ViewModel() {
val preferences: PreferencesManager,
) : ScreenModel {
var supportedVersion by mutableStateOf<DiscordVersion>(DiscordVersion.None)
private set
@ -54,10 +54,10 @@ class HomeViewModel(
state.closestPageToPosition(it)?.prevKey?.plus(1) ?: state.closestPageToPosition(it)?.nextKey?.minus(1)
}
}
}.flow.cachedIn(viewModelScope)
}.flow.cachedIn(screenModelScope)
init {
viewModelScope.launch(Dispatchers.IO) {
screenModelScope.launch(Dispatchers.IO) {
_fetchInstalledVersion()
_fetchSupportedVersion()
}
@ -110,13 +110,13 @@ class HomeViewModel(
}
fun fetchInstalledVersion() {
viewModelScope.launch(Dispatchers.IO) {
screenModelScope.launch(Dispatchers.IO) {
_fetchInstalledVersion()
}
}
fun fetchSupportedVersion() {
viewModelScope.launch(Dispatchers.IO) {
screenModelScope.launch(Dispatchers.IO) {
_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.app.Application
@ -6,8 +6,8 @@ import android.os.Build
import android.util.Log
import androidx.annotation.StringRes
import androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.R
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.installer.util.*
import com.aliucord.manager.network.utils.getOrThrow
import com.aliucord.manager.ui.component.installer.InstallStatus
import com.aliucord.manager.ui.component.installer.InstallStepData
import com.aliucord.manager.ui.screen.InstallData
import com.aliucord.manager.ui.components.installer.InstallStatus
import com.aliucord.manager.ui.components.installer.InstallStepData
import com.aliucord.manager.util.*
import com.github.diamondminer88.zip.*
import kotlinx.coroutines.*
@ -27,14 +26,14 @@ import java.util.Date
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.time.measureTimedValue
class InstallViewModel(
class InstallModel(
private val application: Application,
private val downloadManager: DownloadManager,
private val preferences: PreferencesManager,
private val githubRepository: GithubRepository,
private val aliucordMaven: AliucordMavenRepository,
private val installData: InstallData
) : ViewModel() {
private val installData: InstallData,
) : ScreenModel {
private val externalCacheDir = application.externalCacheDir!!
private val installationRunning = AtomicBoolean(false)
@ -86,7 +85,7 @@ class InstallViewModel(
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)) {
return@launch
}
@ -137,7 +136,7 @@ class InstallViewModel(
}
}
override fun onCleared() {
override fun onDispose() {
if (installationRunning.getAndSet(false)) {
installJob.cancel("ViewModel cleared")
}
@ -375,7 +374,7 @@ class InstallViewModel(
enum class InstallStepGroup(
@StringRes
val nameResId: Int
val nameResId: Int,
) {
APK_DL(R.string.install_group_apk_dl),
LIB_DL(R.string.install_group_lib_dl),
@ -388,7 +387,7 @@ class InstallViewModel(
val group: InstallStepGroup,
@StringRes
val nameResId: Int
val nameResId: Int,
) {
// Kotlin
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 androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.BuildConfig
import com.aliucord.manager.aliucordDir
import com.aliucord.manager.domain.model.Plugin
@ -11,8 +11,7 @@ import kotlinx.coroutines.*
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.json.*
@OptIn(ExperimentalSerializationApi::class)
class PluginsViewModel : ViewModel() {
class PluginsModel : ScreenModel {
var showChangelogDialog by mutableStateOf(false)
private set
@ -35,10 +34,6 @@ class PluginsViewModel : ViewModel() {
this.search = search
}
fun clearSearch() {
search = ""
}
fun showChangelogDialog(plugin: Plugin) {
showChangelogDialog = true
selectedPlugin = plugin
@ -65,13 +60,14 @@ class PluginsViewModel : ViewModel() {
private val settingsFile = aliucordDir.resolve("settings/Aliucord.json")
fun setPluginEnabled(plugin: String, enable: Boolean) {
viewModelScope.launch(Dispatchers.IO) {
screenModelScope.launch(Dispatchers.IO) {
withContext(Dispatchers.Main) {
enabled.clear()
enabled.putAll(loadEnabled())
enabled[plugin] = enable
}
@OptIn(ExperimentalSerializationApi::class)
try {
val settings = settingsFile.inputStream()
.use { Json.decodeFromStream<Map<String, JsonElement>>(it) }
@ -91,6 +87,7 @@ class PluginsViewModel : ViewModel() {
private suspend fun loadEnabled(): Map<String, Boolean> = withContext(Dispatchers.IO) {
if (!settingsFile.exists()) return@withContext emptyMap()
@OptIn(ExperimentalSerializationApi::class)
try {
settingsFile.inputStream()
.use { Json.decodeFromStream<Map<String, JsonElement>>(it) }
@ -105,11 +102,11 @@ class PluginsViewModel : ViewModel() {
}
init {
viewModelScope.launch {
screenModelScope.launch {
enabled.putAll(loadEnabled())
}
viewModelScope.launch(Dispatchers.IO) {
screenModelScope.launch(Dispatchers.IO) {
val pluginsDir = aliucordDir.resolve("plugins")
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 androidx.compose.runtime.*
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import cafe.adriel.voyager.core.model.ScreenModel
import cafe.adriel.voyager.core.model.screenModelScope
import com.aliucord.manager.R
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.throttle
class SettingsViewModel(
class SettingsModel(
private val application: Application,
val preferences: PreferencesManager
) : ViewModel() {
private val checkPackageNameThrottled = throttle(50L, viewModelScope) {
val preferences: PreferencesManager,
) : ScreenModel {
private val checkPackageNameThrottled = throttle(50L, screenModelScope) {
packageNameError = !PACKAGE_REGEX.matches(preferences.packageName)
}
@ -58,7 +58,7 @@ class SettingsViewModel(
checkPackageNameThrottled()
}
override fun onCleared() {
override fun onDispose() {
if (!PACKAGE_REGEX.matches(preferences.packageName)) {
preferences.packageName = "com.aliucord"
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)
else -> append(it.toString())
}
}
},
) {
prefix(this@Builder)
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.material3.*
@ -8,12 +8,11 @@ import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import androidx.compose.ui.window.DialogProperties
import com.aliucord.manager.R
import com.aliucord.manager.ui.viewmodel.UpdaterViewModel
import org.koin.androidx.compose.getViewModel
@Composable
fun UpdaterDialog(
viewModel: UpdaterViewModel = getViewModel()
viewModel: UpdaterViewModel = getViewModel(),
) {
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 androidx.compose.runtime.*

View file

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

View file

@ -17,7 +17,7 @@ kotlin = "1.9.22"
kotlinx-immutable = "0.3.7"
kotlinx-serialization = "1.5.0"
ktor = "2.3.7"
navigation-reimagined = "1.3.0" # TODO: migrate to voyager
voyager = "1.0.0"
zip = "2.1.0"
[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-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
apksig = { module = "com.android.tools.build:apksig", version.ref = "apksig" }
axml = { module = "com.aliucord:axml", version.ref = "axml" }
bouncycastle = { module = "org.bouncycastle:bcpkix-jdk15on", version.ref = "bouncycastle" }
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
[bundles]
@ -95,6 +99,11 @@ ktor = [
"ktor-content-negotiation",
"ktor-serialization-json",
]
voyager = [
"voyager-koin",
"voyager-navigator",
"voyager-transitions",
]
[plugins]
android-application = { id = "com.android.application", version.ref = "agp" }