mirror of
https://github.com/revanced/revanced-manager
synced 2024-05-14 13:56:57 +02:00
feat: switch to Preferences DataStore (#60)
This commit is contained in:
parent
5d3b963682
commit
879884a9fa
@ -68,6 +68,7 @@ dependencies {
|
||||
implementation(libs.compose.activity)
|
||||
implementation(libs.paging.common.ktx)
|
||||
implementation(libs.work.runtime.ktx)
|
||||
implementation(libs.preferences.datastore)
|
||||
|
||||
// Compose
|
||||
implementation(platform(libs.compose.bom))
|
||||
|
@ -5,6 +5,7 @@ import androidx.activity.ComponentActivity
|
||||
import androidx.activity.compose.setContent
|
||||
import androidx.compose.animation.ExperimentalAnimationApi
|
||||
import androidx.compose.foundation.isSystemInDarkTheme
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.core.splashscreen.SplashScreen.Companion.installSplashScreen
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.ui.destination.Destination
|
||||
@ -56,9 +57,12 @@ class MainActivity : ComponentActivity() {
|
||||
)
|
||||
|
||||
setContent {
|
||||
val theme by prefs.theme.getAsState()
|
||||
val dynamicColor by prefs.dynamicColor.getAsState()
|
||||
|
||||
ReVancedManagerTheme(
|
||||
darkTheme = prefs.theme == Theme.SYSTEM && isSystemInDarkTheme() || prefs.theme == Theme.DARK,
|
||||
dynamicColor = prefs.dynamicColor
|
||||
darkTheme = theme == Theme.SYSTEM && isSystemInDarkTheme() || theme == Theme.DARK,
|
||||
dynamicColor = dynamicColor
|
||||
) {
|
||||
val navController =
|
||||
rememberNavController<Destination>(startDestination = Destination.Dashboard)
|
||||
|
@ -2,12 +2,18 @@ package app.revanced.manager
|
||||
|
||||
import android.app.Application
|
||||
import app.revanced.manager.di.*
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import kotlinx.coroutines.MainScope
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.android.ext.android.inject
|
||||
import org.koin.android.ext.koin.androidContext
|
||||
import org.koin.android.ext.koin.androidLogger
|
||||
import org.koin.androidx.workmanager.koin.workManagerFactory
|
||||
import org.koin.core.context.startKoin
|
||||
|
||||
class ManagerApplication : Application() {
|
||||
private val scope = MainScope()
|
||||
private val prefs: PreferencesManager by inject()
|
||||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
|
||||
@ -26,5 +32,9 @@ class ManagerApplication : Application() {
|
||||
databaseModule,
|
||||
)
|
||||
}
|
||||
|
||||
scope.launch {
|
||||
prefs.preload()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,14 +1,9 @@
|
||||
package app.revanced.manager.di
|
||||
|
||||
import android.content.Context
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import org.koin.core.module.dsl.singleOf
|
||||
import org.koin.dsl.module
|
||||
|
||||
val preferencesModule = module {
|
||||
fun providePreferences(
|
||||
context: Context
|
||||
) = PreferencesManager(context.getSharedPreferences("preferences", Context.MODE_PRIVATE))
|
||||
|
||||
singleOf(::providePreferences)
|
||||
singleOf(::PreferencesManager)
|
||||
}
|
@ -4,8 +4,9 @@ import android.app.Application
|
||||
import android.content.Context
|
||||
import app.revanced.manager.util.signing.Signer
|
||||
import app.revanced.manager.util.signing.SigningOptions
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.io.File
|
||||
import java.io.InputStream
|
||||
import java.io.OutputStream
|
||||
import java.nio.file.Files
|
||||
import java.nio.file.Path
|
||||
@ -23,39 +24,46 @@ class KeystoreManager(app: Application, private val prefs: PreferencesManager) {
|
||||
private val keystorePath =
|
||||
app.getDir("signing", Context.MODE_PRIVATE).resolve("manager.keystore").toPath()
|
||||
|
||||
private fun options(
|
||||
cn: String = prefs.keystoreCommonName!!,
|
||||
pass: String = prefs.keystorePass!!,
|
||||
) = SigningOptions(cn, pass, keystorePath)
|
||||
|
||||
private fun updatePrefs(cn: String, pass: String) {
|
||||
prefs.keystoreCommonName = cn
|
||||
prefs.keystorePass = pass
|
||||
private suspend fun updatePrefs(cn: String, pass: String) = prefs.edit {
|
||||
prefs.keystoreCommonName.value = cn
|
||||
prefs.keystorePass.value = pass
|
||||
}
|
||||
|
||||
fun sign(input: File, output: File) = Signer(options()).signApk(input, output)
|
||||
|
||||
init {
|
||||
if (!keystorePath.exists()) {
|
||||
regenerate()
|
||||
}
|
||||
suspend fun sign(input: File, output: File) = withContext(Dispatchers.Default) {
|
||||
Signer(
|
||||
SigningOptions(
|
||||
prefs.keystoreCommonName.get(),
|
||||
prefs.keystorePass.get(),
|
||||
keystorePath
|
||||
)
|
||||
).signApk(
|
||||
input,
|
||||
output
|
||||
)
|
||||
}
|
||||
|
||||
fun regenerate() = Signer(options(DEFAULT, DEFAULT)).regenerateKeystore().also {
|
||||
suspend fun regenerate() = withContext(Dispatchers.Default) {
|
||||
Signer(SigningOptions(DEFAULT, DEFAULT, keystorePath)).regenerateKeystore()
|
||||
updatePrefs(DEFAULT, DEFAULT)
|
||||
}
|
||||
|
||||
fun import(cn: String, pass: String, keystore: Path): Boolean {
|
||||
suspend fun import(cn: String, pass: String, keystore: Path): Boolean {
|
||||
if (!Signer(SigningOptions(cn, pass, keystore)).canUnlock()) {
|
||||
return false
|
||||
}
|
||||
Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING)
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.copy(keystore, keystorePath, StandardCopyOption.REPLACE_EXISTING)
|
||||
}
|
||||
|
||||
updatePrefs(cn, pass)
|
||||
return true
|
||||
}
|
||||
|
||||
fun export(target: OutputStream) {
|
||||
Files.copy(keystorePath, target)
|
||||
fun hasKeystore() = keystorePath.exists()
|
||||
|
||||
suspend fun export(target: OutputStream) {
|
||||
withContext(Dispatchers.IO) {
|
||||
Files.copy(keystorePath, target)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,22 +1,19 @@
|
||||
package app.revanced.manager.domain.manager
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import app.revanced.manager.domain.manager.base.BasePreferenceManager
|
||||
import android.content.Context
|
||||
import app.revanced.manager.domain.manager.base.BasePreferencesManager
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
|
||||
/**
|
||||
* @author Hyperion Authors, zt64
|
||||
*/
|
||||
class PreferencesManager(
|
||||
sharedPreferences: SharedPreferences
|
||||
) : BasePreferenceManager(sharedPreferences) {
|
||||
var dynamicColor by booleanPreference("dynamic_color", true)
|
||||
var theme by enumPreference("theme", Theme.SYSTEM)
|
||||
context: Context
|
||||
) : BasePreferencesManager(context, "settings") {
|
||||
val dynamicColor = booleanPreference("dynamic_color", true)
|
||||
val theme = enumPreference("theme", Theme.SYSTEM)
|
||||
|
||||
var allowExperimental by booleanPreference("allow_experimental", false)
|
||||
val allowExperimental = booleanPreference("allow_experimental", false)
|
||||
|
||||
var preferSplits by booleanPreference("prefer_splits", false)
|
||||
val keystoreCommonName = stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
||||
val keystorePass = stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
||||
|
||||
var keystoreCommonName by stringPreference("keystore_cn", KeystoreManager.DEFAULT)
|
||||
var keystorePass by stringPreference("keystore_pass", KeystoreManager.DEFAULT)
|
||||
val preferSplits = booleanPreference("prefer_splits", false)
|
||||
}
|
||||
|
@ -1,98 +1,133 @@
|
||||
package app.revanced.manager.domain.manager.base
|
||||
|
||||
import android.content.SharedPreferences
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.mutableStateOf
|
||||
import androidx.compose.runtime.setValue
|
||||
import androidx.core.content.edit
|
||||
import kotlin.reflect.KProperty
|
||||
import android.content.Context
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.remember
|
||||
import androidx.datastore.core.DataStore
|
||||
import androidx.datastore.preferences.core.*
|
||||
import androidx.datastore.preferences.preferencesDataStore
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.domain.manager.base.BasePreferencesManager.Companion.editor
|
||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.runBlocking
|
||||
|
||||
/**
|
||||
* @author Hyperion Authors, zt64
|
||||
*/
|
||||
abstract class BasePreferenceManager(
|
||||
private val prefs: SharedPreferences
|
||||
) {
|
||||
protected fun getString(key: String, defaultValue: String?) =
|
||||
prefs.getString(key, defaultValue)!!
|
||||
abstract class BasePreferencesManager(private val context: Context, name: String) {
|
||||
private val Context.dataStore: DataStore<Preferences> by preferencesDataStore(name = name)
|
||||
protected val dataStore get() = context.dataStore
|
||||
|
||||
private fun getBoolean(key: String, defaultValue: Boolean) = prefs.getBoolean(key, defaultValue)
|
||||
private fun getInt(key: String, defaultValue: Int) = prefs.getInt(key, defaultValue)
|
||||
private fun getFloat(key: String, defaultValue: Float) = prefs.getFloat(key, defaultValue)
|
||||
protected inline fun <reified E : Enum<E>> getEnum(key: String, defaultValue: E) =
|
||||
enumValueOf<E>(getString(key, defaultValue.name))
|
||||
|
||||
protected fun putString(key: String, value: String?) = prefs.edit { putString(key, value) }
|
||||
private fun putBoolean(key: String, value: Boolean) = prefs.edit { putBoolean(key, value) }
|
||||
private fun putInt(key: String, value: Int) = prefs.edit { putInt(key, value) }
|
||||
private fun putFloat(key: String, value: Float) = prefs.edit { putFloat(key, value) }
|
||||
protected inline fun <reified E : Enum<E>> putEnum(key: String, value: E) =
|
||||
putString(key, value.name)
|
||||
|
||||
protected class Preference<T>(
|
||||
private val key: String,
|
||||
defaultValue: T,
|
||||
getter: (key: String, defaultValue: T) -> T,
|
||||
private val setter: (key: String, newValue: T) -> Unit
|
||||
) {
|
||||
@Suppress("RedundantSetter")
|
||||
var value by mutableStateOf(getter(key, defaultValue))
|
||||
private set
|
||||
|
||||
operator fun getValue(thisRef: Any?, property: KProperty<*>) = value
|
||||
operator fun setValue(thisRef: Any?, property: KProperty<*>, newValue: T) {
|
||||
value = newValue
|
||||
setter(key, newValue)
|
||||
}
|
||||
suspend fun preload() {
|
||||
dataStore.data.first()
|
||||
}
|
||||
|
||||
protected fun stringPreference(
|
||||
key: String,
|
||||
defaultValue: String?
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getString,
|
||||
setter = ::putString
|
||||
)
|
||||
suspend fun edit(block: EditorContext.() -> Unit) = dataStore.editor(block)
|
||||
|
||||
protected fun booleanPreference(
|
||||
key: String,
|
||||
defaultValue: Boolean
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getBoolean,
|
||||
setter = ::putBoolean
|
||||
)
|
||||
protected fun stringPreference(key: String, default: String) =
|
||||
StringPreference(dataStore, key, default)
|
||||
|
||||
protected fun intPreference(
|
||||
key: String,
|
||||
defaultValue: Int
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getInt,
|
||||
setter = ::putInt
|
||||
)
|
||||
protected fun booleanPreference(key: String, default: Boolean) =
|
||||
BooleanPreference(dataStore, key, default)
|
||||
|
||||
protected fun floatPreference(
|
||||
key: String,
|
||||
defaultValue: Float
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getFloat,
|
||||
setter = ::putFloat
|
||||
)
|
||||
protected fun intPreference(key: String, default: Int) = IntPreference(dataStore, key, default)
|
||||
|
||||
protected fun floatPreference(key: String, default: Float) =
|
||||
FloatPreference(dataStore, key, default)
|
||||
|
||||
protected inline fun <reified E : Enum<E>> enumPreference(
|
||||
key: String,
|
||||
defaultValue: E
|
||||
) = Preference(
|
||||
key = key,
|
||||
defaultValue = defaultValue,
|
||||
getter = ::getEnum,
|
||||
setter = ::putEnum
|
||||
)
|
||||
default: E
|
||||
) = EnumPreference(dataStore, key, default, enumValues())
|
||||
|
||||
companion object {
|
||||
suspend inline fun DataStore<Preferences>.editor(crossinline block: EditorContext.() -> Unit) {
|
||||
edit {
|
||||
EditorContext(it).run(block)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class EditorContext(private val prefs: MutablePreferences) {
|
||||
var <T> Preference<T>.value
|
||||
get() = prefs.run { read() }
|
||||
set(value) = prefs.run { write(value) }
|
||||
}
|
||||
|
||||
abstract class Preference<T>(
|
||||
private val dataStore: DataStore<Preferences>,
|
||||
protected val default: T
|
||||
) {
|
||||
internal abstract fun Preferences.read(): T
|
||||
internal abstract fun MutablePreferences.write(value: T)
|
||||
|
||||
val flow = dataStore.data.map { with(it) { read() } ?: default }.distinctUntilChanged()
|
||||
|
||||
suspend fun get() = flow.first()
|
||||
fun getBlocking() = runBlocking { get() }
|
||||
@Composable
|
||||
fun getAsState() = flow.collectAsStateWithLifecycle(initialValue = remember {
|
||||
getBlocking()
|
||||
})
|
||||
suspend fun update(value: T) = dataStore.editor {
|
||||
this@Preference.value = value
|
||||
}
|
||||
}
|
||||
|
||||
class EnumPreference<E : Enum<E>>(
|
||||
dataStore: DataStore<Preferences>,
|
||||
key: String,
|
||||
default: E,
|
||||
private val enumValues: Array<E>
|
||||
) : Preference<E>(dataStore, default) {
|
||||
private val key = stringPreferencesKey(key)
|
||||
override fun Preferences.read() =
|
||||
this[key]?.let { name ->
|
||||
enumValues.find { it.name == name }
|
||||
} ?: default
|
||||
|
||||
override fun MutablePreferences.write(value: E) {
|
||||
this[key] = value.name
|
||||
}
|
||||
}
|
||||
|
||||
abstract class BasePreference<T>(dataStore: DataStore<Preferences>, default: T) :
|
||||
Preference<T>(dataStore, default) {
|
||||
protected abstract val key: Preferences.Key<T>
|
||||
override fun Preferences.read() = this[key] ?: default
|
||||
override fun MutablePreferences.write(value: T) {
|
||||
this[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
class StringPreference(
|
||||
dataStore: DataStore<Preferences>,
|
||||
key: String,
|
||||
default: String
|
||||
) : BasePreference<String>(dataStore, default) {
|
||||
override val key = stringPreferencesKey(key)
|
||||
}
|
||||
|
||||
class BooleanPreference(
|
||||
dataStore: DataStore<Preferences>,
|
||||
key: String,
|
||||
default: Boolean
|
||||
) : BasePreference<Boolean>(dataStore, default) {
|
||||
override val key = booleanPreferencesKey(key)
|
||||
}
|
||||
|
||||
class IntPreference(
|
||||
dataStore: DataStore<Preferences>,
|
||||
key: String,
|
||||
default: Int
|
||||
) : BasePreference<Int>(dataStore, default) {
|
||||
override val key = intPreferencesKey(key)
|
||||
}
|
||||
|
||||
class FloatPreference(
|
||||
dataStore: DataStore<Preferences>,
|
||||
key: String,
|
||||
default: Float
|
||||
) : BasePreference<Float>(dataStore, default) {
|
||||
override val key = floatPreferencesKey(key)
|
||||
}
|
@ -0,0 +1,50 @@
|
||||
package app.revanced.manager.ui.component.settings
|
||||
|
||||
import androidx.annotation.StringRes
|
||||
import androidx.compose.foundation.clickable
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
import androidx.compose.runtime.rememberCoroutineScope
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import app.revanced.manager.domain.manager.base.Preference
|
||||
import kotlinx.coroutines.CoroutineScope
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
@Composable
|
||||
fun BooleanItem(
|
||||
preference: Preference<Boolean>,
|
||||
coroutineScope: CoroutineScope = rememberCoroutineScope(),
|
||||
@StringRes headline: Int,
|
||||
@StringRes description: Int
|
||||
) {
|
||||
val value by preference.getAsState()
|
||||
|
||||
BooleanItem(
|
||||
value = value,
|
||||
onValueChange = { coroutineScope.launch { preference.update(it) } },
|
||||
headline = headline,
|
||||
description = description
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun BooleanItem(
|
||||
value: Boolean,
|
||||
onValueChange: (Boolean) -> Unit,
|
||||
@StringRes headline: Int,
|
||||
@StringRes description: Int
|
||||
) = ListItem(
|
||||
modifier = Modifier.clickable { onValueChange(!value) },
|
||||
headlineContent = { Text(stringResource(headline)) },
|
||||
supportingContent = { Text(stringResource(description)) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = value,
|
||||
onCheckedChange = onValueChange,
|
||||
)
|
||||
}
|
||||
)
|
@ -183,6 +183,8 @@ fun PatchesSelectorScreen(
|
||||
)
|
||||
}
|
||||
|
||||
val allowExperimental by vm.allowExperimental.getAsState()
|
||||
|
||||
LazyColumn(
|
||||
modifier = Modifier.fillMaxSize()
|
||||
) {
|
||||
@ -237,7 +239,7 @@ fun PatchesSelectorScreen(
|
||||
patchList(
|
||||
patches = bundle.unsupported,
|
||||
filterFlag = SHOW_UNSUPPORTED,
|
||||
supported = vm.allowExperimental
|
||||
supported = allowExperimental
|
||||
) {
|
||||
ListHeader(
|
||||
title = stringResource(R.string.unsupported_patches),
|
||||
|
@ -13,7 +13,6 @@ import androidx.compose.material3.Icon
|
||||
import androidx.compose.material3.IconButton
|
||||
import androidx.compose.material3.ListItem
|
||||
import androidx.compose.material3.Scaffold
|
||||
import androidx.compose.material3.Switch
|
||||
import androidx.compose.material3.Text
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.getValue
|
||||
@ -24,6 +23,7 @@ import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||
import app.revanced.manager.ui.viewmodel.DownloadsViewModel
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@ -58,13 +58,10 @@ fun DownloadsSettingsScreen(
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { prefs.preferSplits = !prefs.preferSplits },
|
||||
headlineContent = { Text(stringResource(R.string.prefer_splits)) },
|
||||
supportingContent = { Text(stringResource(R.string.prefer_splits_description)) },
|
||||
trailingContent = {
|
||||
Switch(checked = prefs.preferSplits, onCheckedChange = { prefs.preferSplits = it })
|
||||
}
|
||||
BooleanItem(
|
||||
preference = prefs.preferSplits,
|
||||
headline = R.string.prefer_splits,
|
||||
description = R.string.prefer_splits_description,
|
||||
)
|
||||
|
||||
GroupHeader(stringResource(R.string.downloaded_apps))
|
||||
|
@ -15,12 +15,15 @@ import androidx.compose.runtime.setValue
|
||||
import androidx.compose.ui.Alignment
|
||||
import androidx.compose.ui.Modifier
|
||||
import androidx.compose.ui.res.stringResource
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.settings.BooleanItem
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import app.revanced.manager.ui.viewmodel.SettingsViewModel
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.compose.koinInject
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@ -30,6 +33,7 @@ fun GeneralSettingsScreen(
|
||||
viewModel: SettingsViewModel
|
||||
) {
|
||||
val prefs = viewModel.prefs
|
||||
val coroutineScope = viewModel.viewModelScope
|
||||
var showThemePicker by rememberSaveable { mutableStateOf(false) }
|
||||
|
||||
if (showThemePicker) {
|
||||
@ -53,22 +57,27 @@ fun GeneralSettingsScreen(
|
||||
.padding(paddingValues)
|
||||
.verticalScroll(rememberScrollState())
|
||||
) {
|
||||
|
||||
GroupHeader(stringResource(R.string.appearance))
|
||||
|
||||
val theme by prefs.theme.getAsState()
|
||||
ListItem(
|
||||
modifier = Modifier.clickable { showThemePicker = true },
|
||||
headlineContent = { Text(stringResource(R.string.theme)) },
|
||||
supportingContent = { Text(stringResource(R.string.theme_description)) },
|
||||
trailingContent = {
|
||||
Button({
|
||||
showThemePicker = true
|
||||
}) { Text(stringResource(prefs.theme.displayName)) }
|
||||
Button(
|
||||
onClick = {
|
||||
showThemePicker = true
|
||||
}
|
||||
) {
|
||||
Text(stringResource(theme.displayName))
|
||||
}
|
||||
}
|
||||
)
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
||||
BooleanItem(
|
||||
value = prefs.dynamicColor,
|
||||
onValueChange = { prefs.dynamicColor = it },
|
||||
preference = prefs.dynamicColor,
|
||||
coroutineScope = coroutineScope,
|
||||
headline = R.string.dynamic_color,
|
||||
description = R.string.dynamic_color_description
|
||||
)
|
||||
@ -76,8 +85,8 @@ fun GeneralSettingsScreen(
|
||||
|
||||
GroupHeader(stringResource(R.string.patcher))
|
||||
BooleanItem(
|
||||
value = prefs.allowExperimental,
|
||||
onValueChange = { prefs.allowExperimental = it },
|
||||
preference = prefs.allowExperimental,
|
||||
coroutineScope = coroutineScope,
|
||||
headline = R.string.experimental_patches,
|
||||
description = R.string.experimental_patches_description
|
||||
)
|
||||
@ -91,7 +100,7 @@ private fun ThemePicker(
|
||||
onConfirm: (Theme) -> Unit,
|
||||
prefs: PreferencesManager = koinInject()
|
||||
) {
|
||||
var selectedTheme by rememberSaveable { mutableStateOf(prefs.theme) }
|
||||
var selectedTheme by rememberSaveable { mutableStateOf(prefs.theme.getBlocking()) }
|
||||
|
||||
AlertDialog(
|
||||
onDismissRequest = onDismiss,
|
||||
@ -122,22 +131,4 @@ private fun ThemePicker(
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun BooleanItem(
|
||||
value: Boolean,
|
||||
onValueChange: (Boolean) -> Unit,
|
||||
@StringRes headline: Int,
|
||||
@StringRes description: Int
|
||||
) = ListItem(
|
||||
modifier = Modifier.clickable { onValueChange(!value) },
|
||||
headlineContent = { Text(stringResource(headline)) },
|
||||
supportingContent = { Text(stringResource(description)) },
|
||||
trailingContent = {
|
||||
Switch(
|
||||
checked = value,
|
||||
onCheckedChange = onValueChange,
|
||||
)
|
||||
}
|
||||
)
|
||||
}
|
@ -25,6 +25,7 @@ import androidx.compose.ui.res.stringResource
|
||||
import androidx.compose.ui.text.style.TextAlign
|
||||
import androidx.compose.ui.unit.dp
|
||||
import androidx.lifecycle.compose.collectAsStateWithLifecycle
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.R
|
||||
import app.revanced.manager.ui.viewmodel.ImportExportViewModel
|
||||
import app.revanced.manager.ui.component.AppTopBar
|
||||
@ -32,6 +33,7 @@ import app.revanced.manager.ui.component.GroupHeader
|
||||
import app.revanced.manager.ui.component.PasswordField
|
||||
import app.revanced.manager.ui.component.sources.SourceSelector
|
||||
import app.revanced.manager.util.toast
|
||||
import kotlinx.coroutines.launch
|
||||
import org.koin.androidx.compose.getViewModel
|
||||
|
||||
@OptIn(ExperimentalMaterial3Api::class)
|
||||
@ -40,6 +42,8 @@ fun ImportExportSettingsScreen(
|
||||
onBackClick: () -> Unit,
|
||||
vm: ImportExportViewModel = getViewModel()
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
|
||||
val importKeystoreLauncher =
|
||||
rememberLauncherForActivityResult(contract = ActivityResultContracts.GetContent()) {
|
||||
it?.let { uri -> vm.startKeystoreImport(uri) }
|
||||
@ -74,7 +78,12 @@ fun ImportExportSettingsScreen(
|
||||
if (vm.showCredentialsDialog) {
|
||||
KeystoreCredentialsDialog(
|
||||
onDismissRequest = vm::cancelKeystoreImport,
|
||||
tryImport = vm::tryKeystoreImport
|
||||
onSubmit = { cn, pass ->
|
||||
vm.viewModelScope.launch {
|
||||
val result = vm.tryKeystoreImport(cn, pass)
|
||||
if (!result) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ -102,6 +111,10 @@ fun ImportExportSettingsScreen(
|
||||
)
|
||||
GroupItem(
|
||||
onClick = {
|
||||
if (!vm.canExport()) {
|
||||
context.toast(context.getString(R.string.export_keystore_unavailable))
|
||||
return@GroupItem
|
||||
}
|
||||
exportKeystoreLauncher.launch("Manager.keystore")
|
||||
},
|
||||
headline = R.string.export_keystore,
|
||||
@ -144,9 +157,8 @@ private fun GroupItem(onClick: () -> Unit, @StringRes headline: Int, @StringRes
|
||||
@Composable
|
||||
fun KeystoreCredentialsDialog(
|
||||
onDismissRequest: () -> Unit,
|
||||
tryImport: (String, String) -> Boolean
|
||||
onSubmit: (String, String) -> Unit
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
var cn by rememberSaveable { mutableStateOf("") }
|
||||
var pass by rememberSaveable { mutableStateOf("") }
|
||||
|
||||
@ -155,11 +167,7 @@ fun KeystoreCredentialsDialog(
|
||||
confirmButton = {
|
||||
TextButton(
|
||||
onClick = {
|
||||
if (!tryImport(
|
||||
cn,
|
||||
pass
|
||||
)
|
||||
) context.toast(context.getString(R.string.import_keystore_wrong_credentials))
|
||||
onSubmit(cn, pass)
|
||||
}
|
||||
) {
|
||||
Text(stringResource(R.string.import_keystore_dialog_button))
|
||||
|
@ -118,7 +118,7 @@ class AppDownloaderViewModel(
|
||||
?: appDownloader.downloadApp(
|
||||
version,
|
||||
savePath,
|
||||
preferSplit = prefs.preferSplits
|
||||
preferSplit = prefs.preferSplits.get()
|
||||
).also {
|
||||
downloadedAppRepository.add(
|
||||
selectedApp.packageName,
|
||||
|
@ -48,17 +48,20 @@ class ImportExportViewModel(
|
||||
private var keystoreImportPath by mutableStateOf<Path?>(null)
|
||||
val showCredentialsDialog by derivedStateOf { keystoreImportPath != null }
|
||||
|
||||
fun startKeystoreImport(content: Uri) {
|
||||
val path = File.createTempFile("signing", "ks", app.cacheDir).toPath()
|
||||
Files.copy(
|
||||
contentResolver.openInputStream(content)!!,
|
||||
path,
|
||||
StandardCopyOption.REPLACE_EXISTING
|
||||
)
|
||||
fun startKeystoreImport(content: Uri) = viewModelScope.launch {
|
||||
val path = withContext(Dispatchers.IO) {
|
||||
File.createTempFile("signing", "ks", app.cacheDir).toPath().also {
|
||||
Files.copy(
|
||||
contentResolver.openInputStream(content)!!,
|
||||
it,
|
||||
StandardCopyOption.REPLACE_EXISTING
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
knownPasswords.forEach {
|
||||
if (tryKeystoreImport(KeystoreManager.DEFAULT, it, path)) {
|
||||
return
|
||||
return@launch
|
||||
}
|
||||
}
|
||||
|
||||
@ -70,10 +73,10 @@ class ImportExportViewModel(
|
||||
keystoreImportPath = null
|
||||
}
|
||||
|
||||
fun tryKeystoreImport(cn: String, pass: String) =
|
||||
suspend fun tryKeystoreImport(cn: String, pass: String) =
|
||||
tryKeystoreImport(cn, pass, keystoreImportPath!!)
|
||||
|
||||
private fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean {
|
||||
private suspend fun tryKeystoreImport(cn: String, pass: String, path: Path): Boolean {
|
||||
if (keystoreManager.import(cn, pass, path)) {
|
||||
cancelKeystoreImport()
|
||||
return true
|
||||
@ -88,10 +91,14 @@ class ImportExportViewModel(
|
||||
cancelKeystoreImport()
|
||||
}
|
||||
|
||||
fun exportKeystore(target: Uri) =
|
||||
keystoreManager.export(contentResolver.openOutputStream(target)!!)
|
||||
fun canExport() = keystoreManager.hasKeystore()
|
||||
|
||||
fun regenerateKeystore() = keystoreManager.regenerate().also {
|
||||
fun exportKeystore(target: Uri) = viewModelScope.launch {
|
||||
keystoreManager.export(contentResolver.openOutputStream(target)!!)
|
||||
}
|
||||
|
||||
fun regenerateKeystore() = viewModelScope.launch {
|
||||
keystoreManager.regenerate()
|
||||
app.toast(app.getString(R.string.regenerate_keystore_success))
|
||||
}
|
||||
|
||||
|
@ -40,10 +40,9 @@ class PatchesSelectorViewModel(
|
||||
val appInfo: AppInfo
|
||||
) : ViewModel(), KoinComponent {
|
||||
private val selectionRepository: PatchSelectionRepository = get()
|
||||
private val prefs: PreferencesManager = get()
|
||||
private val savedStateHandle: SavedStateHandle = get()
|
||||
val allowExperimental get() = prefs.allowExperimental
|
||||
|
||||
val allowExperimental = get<PreferencesManager>().allowExperimental
|
||||
val bundlesFlow = get<SourceRepository>().sources.flatMapLatestAndCombine(
|
||||
combiner = { it }
|
||||
) { source ->
|
||||
@ -121,7 +120,7 @@ class PatchesSelectorViewModel(
|
||||
selectionRepository.updateSelection(appInfo.packageName, it)
|
||||
}
|
||||
}.mapValues { it.value.toMutableSet() }.apply {
|
||||
if (allowExperimental) {
|
||||
if (allowExperimental.get()) {
|
||||
return@apply
|
||||
}
|
||||
|
||||
|
@ -1,15 +1,15 @@
|
||||
package app.revanced.manager.ui.viewmodel
|
||||
|
||||
import androidx.lifecycle.ViewModel
|
||||
import androidx.lifecycle.viewModelScope
|
||||
import app.revanced.manager.domain.manager.PreferencesManager
|
||||
import app.revanced.manager.ui.theme.Theme
|
||||
import kotlinx.coroutines.launch
|
||||
|
||||
class SettingsViewModel(
|
||||
val prefs: PreferencesManager
|
||||
) : ViewModel() {
|
||||
|
||||
fun setTheme(theme: Theme) {
|
||||
prefs.theme = theme
|
||||
fun setTheme(theme: Theme) = viewModelScope.launch {
|
||||
prefs.theme.update(theme)
|
||||
}
|
||||
|
||||
}
|
@ -45,6 +45,7 @@
|
||||
<string name="import_keystore_wrong_credentials">Wrong keystore credentials</string>
|
||||
<string name="export_keystore">Export keystore</string>
|
||||
<string name="export_keystore_description">Export the current keystore</string>
|
||||
<string name="export_keystore_unavailable">No keystore to export</string>
|
||||
<string name="regenerate_keystore">Regenerate keystore</string>
|
||||
<string name="regenerate_keystore_description">Generate a new keystore</string>
|
||||
<string name="regenerate_keystore_success">The keystore has been successfully replaced</string>
|
||||
|
@ -4,6 +4,7 @@ viewmodel-lifecycle = "2.6.1"
|
||||
splash-screen = "1.0.1"
|
||||
compose-activity = "1.7.2"
|
||||
paging = "3.1.1"
|
||||
preferences-datastore = "1.0.0"
|
||||
work-runtime = "2.8.1ō"
|
||||
compose-bom = "2023.06.01"
|
||||
accompanist = "0.30.1"
|
||||
@ -35,6 +36,7 @@ splash-screen = { group = "androidx.core", name = "core-splashscreen", version.r
|
||||
compose-activity = { group = "androidx.activity", name = "activity-compose", version.ref = "compose-activity" }
|
||||
paging-common-ktx = { group = "androidx.paging", name = "paging-common-ktx", version.ref = "paging" }
|
||||
work-runtime-ktx = { group = "androidx.work", name = "work-runtime-ktx", version.ref = "work-runtime" }
|
||||
preferences-datastore = { group = "androidx.datastore", name = "datastore-preferences", version.ref = "preferences-datastore" }
|
||||
|
||||
# Compose
|
||||
compose-bom = { group = "androidx.compose", name = "compose-bom", version.ref = "compose-bom" }
|
||||
|
Loading…
Reference in New Issue
Block a user