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