diff --git a/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt b/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt index 793b3d0b..356493ca 100644 --- a/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt +++ b/app/src/main/java/app/revanced/manager/ui/component/patches/PathSelectorDialog.kt @@ -26,7 +26,7 @@ import androidx.compose.ui.window.Dialog import androidx.compose.ui.window.DialogProperties import app.revanced.manager.R import app.revanced.manager.ui.component.AppTopBar -import app.revanced.manager.util.PathSaver +import app.revanced.manager.util.saver.PathSaver import java.nio.file.Path import kotlin.io.path.isDirectory import kotlin.io.path.isRegularFile diff --git a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt index f1d5d131..b9cdb86b 100644 --- a/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt +++ b/app/src/main/java/app/revanced/manager/ui/viewmodel/PatchesSelectorViewModel.kt @@ -5,10 +5,14 @@ import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.saveable.Saver import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.lifecycle.SavedStateHandle import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope +import androidx.lifecycle.viewmodel.compose.SavedStateHandleSaveableApi +import androidx.lifecycle.viewmodel.compose.saveable import app.revanced.manager.domain.manager.PreferencesManager import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.SourceRepository @@ -19,6 +23,8 @@ import app.revanced.manager.util.PatchesSelection import app.revanced.manager.util.SnapshotStateSet import app.revanced.manager.util.flatMapLatestAndCombine import app.revanced.manager.util.mutableStateSetOf +import app.revanced.manager.util.saver.snapshotStateMapSaver +import app.revanced.manager.util.saver.snapshotStateSetSaver import app.revanced.manager.util.toMutableStateSet import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.first @@ -29,11 +35,13 @@ import org.koin.core.component.KoinComponent import org.koin.core.component.get @Stable +@OptIn(SavedStateHandleSaveableApi::class) 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 bundlesFlow = get().sources.flatMapLatestAndCombine( @@ -59,9 +67,30 @@ class PatchesSelectorViewModel( } } - private val selectedPatches = mutableStateMapOf>() - private val patchOptions = - mutableStateMapOf>>() + private val selectedPatches: SnapshotStatePatchesSelection by savedStateHandle.saveable(saver = patchesSelectionSaver, init = { + val map: SnapshotStatePatchesSelection = mutableStateMapOf() + viewModelScope.launch(Dispatchers.Default) { + val bundles = bundlesFlow.first() + val filteredSelection = + selectionRepository.getSelection(appInfo.packageName).mapValues { (uid, patches) -> + // Filter out patches that don't exist. + val filteredPatches = bundles.singleOrNull { it.uid == uid } + ?.let { bundle -> + val allPatches = bundle.all.map { it.name } + patches.filter { allPatches.contains(it) } + } + ?: patches + + filteredPatches.toMutableStateSet() + } + + withContext(Dispatchers.Main) { + map.putAll(filteredSelection) + } + } + return@saveable map + }) + private val patchOptions: SnapshotStateOptions by savedStateHandle.saveable(saver = optionsSaver, init = ::mutableStateMapOf) /** * Show the patch options dialog for this patch. @@ -91,39 +120,17 @@ class PatchesSelectorViewModel( withContext(Dispatchers.Default) { selectionRepository.updateSelection(appInfo.packageName, it) } - }.mapValues { it.value.toMutableList() }.apply { + }.mapValues { it.value.toMutableSet() }.apply { if (allowExperimental) { return@apply } // Filter out unsupported patches that may have gotten selected through the database if the setting is not enabled. bundlesFlow.first().forEach { - this[it.uid]?.removeAll(it.unsupported.map { patch -> patch.name }) + this[it.uid]?.removeAll(it.unsupported.map { patch -> patch.name }.toSet()) } } - init { - viewModelScope.launch(Dispatchers.Default) { - val bundles = bundlesFlow.first() - val filteredSelection = - selectionRepository.getSelection(appInfo.packageName).mapValues { (uid, patches) -> - // Filter out patches that don't exist. - val filteredPatches = bundles.singleOrNull { it.uid == uid } - ?.let { bundle -> - val allPatches = bundle.all.map { it.name } - patches.filter { allPatches.contains(it) } - } - ?: patches - - filteredPatches.toMutableStateSet() - } - - withContext(Dispatchers.Main) { - selectedPatches.putAll(filteredSelection) - } - } - } - fun getOptions(): Options = patchOptions fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name) @@ -164,6 +171,17 @@ class PatchesSelectorViewModel( private fun SnapshotStateMap>.getOrCreate(key: K) = getOrPut(key, ::mutableStateMapOf) + + private val optionsSaver: Saver = snapshotStateMapSaver( + // Patch name -> Options + valueSaver = snapshotStateMapSaver( + // Option key -> Option value + valueSaver = snapshotStateMapSaver() + ) + ) + + private val patchesSelectionSaver: Saver = + snapshotStateMapSaver(valueSaver = snapshotStateSetSaver()) } data class BundleInfo( @@ -174,4 +192,14 @@ class PatchesSelectorViewModel( val unsupported: List, val universal: List ) -} \ No newline at end of file +} + +/** + * [Options] but with observable collection types. + */ +private typealias SnapshotStateOptions = SnapshotStateMap>> + +/** + * [PatchesSelection] but with observable collection types. + */ +private typealias SnapshotStatePatchesSelection = SnapshotStateMap> \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/Util.kt b/app/src/main/java/app/revanced/manager/util/Util.kt index 0f422d44..79ebc371 100644 --- a/app/src/main/java/app/revanced/manager/util/Util.kt +++ b/app/src/main/java/app/revanced/manager/util/Util.kt @@ -7,7 +7,6 @@ import android.graphics.drawable.Drawable import android.util.Log import android.widget.Toast import androidx.annotation.StringRes -import androidx.compose.runtime.saveable.Saver import androidx.core.net.toUri import androidx.lifecycle.Lifecycle import androidx.lifecycle.LifecycleOwner @@ -20,10 +19,8 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.launch -import java.nio.file.Path -import kotlin.io.path.Path -typealias PatchesSelection = Map> +typealias PatchesSelection = Map> typealias Options = Map>> fun Context.openUrl(url: String) { @@ -96,9 +93,4 @@ inline fun Flow>.flatMapLatestAndCombine( combine(iterable.map(transformer)) { combiner(it) } -} - -val PathSaver = Saver( - save = { it.toString() }, - restore = { Path(it) } -) \ No newline at end of file +} \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/saver/PathSaver.kt b/app/src/main/java/app/revanced/manager/util/saver/PathSaver.kt new file mode 100644 index 00000000..d16eeb92 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/saver/PathSaver.kt @@ -0,0 +1,13 @@ +package app.revanced.manager.util.saver + +import androidx.compose.runtime.saveable.Saver +import java.nio.file.Path +import kotlin.io.path.Path + +/** + * A [Saver] that can save [Path]s. Only works with the default filesystem. + */ +val PathSaver = Saver( + save = { it.toString() }, + restore = { Path(it) } +) \ No newline at end of file diff --git a/app/src/main/java/app/revanced/manager/util/saver/SnapshotStateCollectionSavers.kt b/app/src/main/java/app/revanced/manager/util/saver/SnapshotStateCollectionSavers.kt new file mode 100644 index 00000000..985cbeb6 --- /dev/null +++ b/app/src/main/java/app/revanced/manager/util/saver/SnapshotStateCollectionSavers.kt @@ -0,0 +1,75 @@ +package app.revanced.manager.util.saver + +import androidx.compose.runtime.mutableStateMapOf +import androidx.compose.runtime.saveable.Saver +import androidx.compose.runtime.snapshots.SnapshotStateList +import androidx.compose.runtime.snapshots.SnapshotStateMap +import androidx.compose.runtime.toMutableStateList +import androidx.compose.runtime.toMutableStateMap +import app.revanced.manager.util.SnapshotStateSet +import app.revanced.manager.util.toMutableStateSet + +/** + * Create a [Saver] for [SnapshotStateList]s. + */ +fun snapshotStateListSaver() = Saver, List>( + save = { + it.toMutableList() + }, + restore = { + it.toMutableStateList() + } +) + +/** + * Create a [Saver] for [SnapshotStateSet]s. + */ +fun snapshotStateSetSaver() = Saver, Set>( + save = { + it.toMutableSet() + }, + restore = { + it.toMutableStateSet() + } +) + +/** + * Create a [Saver] for [SnapshotStateMap]s. + */ +fun snapshotStateMapSaver() = Saver, Map>( + save = { + it.toMutableMap() + }, + restore = { + mutableStateMapOf().apply { + this.putAll(it) + } + } +) + +/** + * Create a saver for [SnapshotStateMap]s with a custom [Saver] used for the values. + * Null values will not be saved by this [Saver]. + * + * @param valueSaver The [Saver] used for the values of the [Map]. + */ +fun snapshotStateMapSaver( + valueSaver: Saver +) = Saver, Map>( + save = { + buildMap { + it.forEach { (key, value) -> + with(valueSaver) { + save(value)?.let { + this@buildMap[key] = it + } + } + } + } + }, + restore = { + it.mapNotNull { (key, value) -> + valueSaver.restore(value)?.let { restored -> key to restored } + }.toMutableStateMap() + } +) \ No newline at end of file