feat: save patch options and selected patches in bundle (#50)

This commit is contained in:
Ax333l 2023-07-04 11:09:16 +02:00 committed by GitHub
parent a0ab7a0e26
commit 370276dcab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 147 additions and 39 deletions

View File

@ -26,7 +26,7 @@ import androidx.compose.ui.window.Dialog
import androidx.compose.ui.window.DialogProperties import androidx.compose.ui.window.DialogProperties
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.util.PathSaver import app.revanced.manager.util.saver.PathSaver
import java.nio.file.Path import java.nio.file.Path
import kotlin.io.path.isDirectory import kotlin.io.path.isDirectory
import kotlin.io.path.isRegularFile import kotlin.io.path.isRegularFile

View File

@ -5,10 +5,14 @@ import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateListOf import androidx.compose.runtime.mutableStateListOf
import androidx.compose.runtime.mutableStateMapOf import androidx.compose.runtime.mutableStateMapOf
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.Saver
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.runtime.snapshots.SnapshotStateMap import androidx.compose.runtime.snapshots.SnapshotStateMap
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope 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.manager.PreferencesManager
import app.revanced.manager.domain.repository.PatchSelectionRepository import app.revanced.manager.domain.repository.PatchSelectionRepository
import app.revanced.manager.domain.repository.SourceRepository 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.SnapshotStateSet
import app.revanced.manager.util.flatMapLatestAndCombine import app.revanced.manager.util.flatMapLatestAndCombine
import app.revanced.manager.util.mutableStateSetOf 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 app.revanced.manager.util.toMutableStateSet
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.first import kotlinx.coroutines.flow.first
@ -29,11 +35,13 @@ import org.koin.core.component.KoinComponent
import org.koin.core.component.get import org.koin.core.component.get
@Stable @Stable
@OptIn(SavedStateHandleSaveableApi::class)
class PatchesSelectorViewModel( 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 prefs: PreferencesManager = get()
private val savedStateHandle: SavedStateHandle = get()
val allowExperimental get() = prefs.allowExperimental val allowExperimental get() = prefs.allowExperimental
val bundlesFlow = get<SourceRepository>().sources.flatMapLatestAndCombine( val bundlesFlow = get<SourceRepository>().sources.flatMapLatestAndCombine(
@ -59,9 +67,30 @@ class PatchesSelectorViewModel(
} }
} }
private val selectedPatches = mutableStateMapOf<Int, SnapshotStateSet<String>>() private val selectedPatches: SnapshotStatePatchesSelection by savedStateHandle.saveable(saver = patchesSelectionSaver, init = {
private val patchOptions = val map: SnapshotStatePatchesSelection = mutableStateMapOf()
mutableStateMapOf<Int, SnapshotStateMap<String, SnapshotStateMap<String, Any?>>>() 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. * Show the patch options dialog for this patch.
@ -91,39 +120,17 @@ class PatchesSelectorViewModel(
withContext(Dispatchers.Default) { withContext(Dispatchers.Default) {
selectionRepository.updateSelection(appInfo.packageName, it) selectionRepository.updateSelection(appInfo.packageName, it)
} }
}.mapValues { it.value.toMutableList() }.apply { }.mapValues { it.value.toMutableSet() }.apply {
if (allowExperimental) { if (allowExperimental) {
return@apply return@apply
} }
// Filter out unsupported patches that may have gotten selected through the database if the setting is not enabled. // Filter out unsupported patches that may have gotten selected through the database if the setting is not enabled.
bundlesFlow.first().forEach { 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(): Options = patchOptions
fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name) fun getOptions(bundle: Int, patch: PatchInfo) = patchOptions[bundle]?.get(patch.name)
@ -164,6 +171,17 @@ class PatchesSelectorViewModel(
private fun <K, K2, V> SnapshotStateMap<K, SnapshotStateMap<K2, V>>.getOrCreate(key: K) = private fun <K, K2, V> SnapshotStateMap<K, SnapshotStateMap<K2, V>>.getOrCreate(key: K) =
getOrPut(key, ::mutableStateMapOf) getOrPut(key, ::mutableStateMapOf)
private val optionsSaver: Saver<SnapshotStateOptions, Options> = snapshotStateMapSaver(
// Patch name -> Options
valueSaver = snapshotStateMapSaver(
// Option key -> Option value
valueSaver = snapshotStateMapSaver()
)
)
private val patchesSelectionSaver: Saver<SnapshotStatePatchesSelection, PatchesSelection> =
snapshotStateMapSaver(valueSaver = snapshotStateSetSaver())
} }
data class BundleInfo( data class BundleInfo(
@ -174,4 +192,14 @@ class PatchesSelectorViewModel(
val unsupported: List<PatchInfo>, val unsupported: List<PatchInfo>,
val universal: List<PatchInfo> val universal: List<PatchInfo>
) )
} }
/**
* [Options] but with observable collection types.
*/
private typealias SnapshotStateOptions = SnapshotStateMap<Int, SnapshotStateMap<String, SnapshotStateMap<String, Any?>>>
/**
* [PatchesSelection] but with observable collection types.
*/
private typealias SnapshotStatePatchesSelection = SnapshotStateMap<Int, SnapshotStateSet<String>>

View File

@ -7,7 +7,6 @@ import android.graphics.drawable.Drawable
import android.util.Log import android.util.Log
import android.widget.Toast import android.widget.Toast
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.saveable.Saver
import androidx.core.net.toUri import androidx.core.net.toUri
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
@ -20,10 +19,8 @@ import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.flatMapLatest import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import java.nio.file.Path
import kotlin.io.path.Path
typealias PatchesSelection = Map<Int, List<String>> typealias PatchesSelection = Map<Int, Set<String>>
typealias Options = Map<Int, Map<String, Map<String, Any?>>> typealias Options = Map<Int, Map<String, Map<String, Any?>>>
fun Context.openUrl(url: String) { fun Context.openUrl(url: String) {
@ -96,9 +93,4 @@ inline fun <T, reified R, C> Flow<Iterable<T>>.flatMapLatestAndCombine(
combine(iterable.map(transformer)) { combine(iterable.map(transformer)) {
combiner(it) combiner(it)
} }
} }
val PathSaver = Saver<Path, String>(
save = { it.toString() },
restore = { Path(it) }
)

View File

@ -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<Path, String>(
save = { it.toString() },
restore = { Path(it) }
)

View File

@ -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 <T> snapshotStateListSaver() = Saver<SnapshotStateList<T>, List<T>>(
save = {
it.toMutableList()
},
restore = {
it.toMutableStateList()
}
)
/**
* Create a [Saver] for [SnapshotStateSet]s.
*/
fun <T> snapshotStateSetSaver() = Saver<SnapshotStateSet<T>, Set<T>>(
save = {
it.toMutableSet()
},
restore = {
it.toMutableStateSet()
}
)
/**
* Create a [Saver] for [SnapshotStateMap]s.
*/
fun <K, V> snapshotStateMapSaver() = Saver<SnapshotStateMap<K, V>, Map<K, V>>(
save = {
it.toMutableMap()
},
restore = {
mutableStateMapOf<K, V>().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 <K, Original, Saveable : Any> snapshotStateMapSaver(
valueSaver: Saver<Original, Saveable>
) = Saver<SnapshotStateMap<K, Original>, Map<K, Saveable>>(
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()
}
)