Dedupe SearchScreenModels

This commit is contained in:
arkon 2023-07-16 19:44:32 -04:00
parent ef7b285151
commit ca789dca0e
7 changed files with 108 additions and 127 deletions

View file

@ -10,8 +10,8 @@ import eu.kanade.presentation.browse.components.GlobalSearchLoadingResultItem
import eu.kanade.presentation.browse.components.GlobalSearchResultItem import eu.kanade.presentation.browse.components.GlobalSearchResultItem
import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
@ -19,7 +19,7 @@ import tachiyomi.presentation.core.components.material.Scaffold
@Composable @Composable
fun GlobalSearchScreen( fun GlobalSearchScreen(
state: GlobalSearchScreenModel.State, state: SearchScreenModel.State,
navigateUp: () -> Unit, navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit, onSearch: (String) -> Unit,

View file

@ -4,14 +4,15 @@ import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.State
import eu.kanade.presentation.browse.components.GlobalSearchToolbar import eu.kanade.presentation.browse.components.GlobalSearchToolbar
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.migration.search.MigrateSearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import tachiyomi.domain.manga.model.Manga import tachiyomi.domain.manga.model.Manga
import tachiyomi.presentation.core.components.material.Scaffold import tachiyomi.presentation.core.components.material.Scaffold
@Composable @Composable
fun MigrateSearchScreen( fun MigrateSearchScreen(
state: MigrateSearchScreenModel.State, state: SearchScreenModel.State,
fromSourceId: Long?,
navigateUp: () -> Unit, navigateUp: () -> Unit,
onChangeSearchQuery: (String?) -> Unit, onChangeSearchQuery: (String?) -> Unit,
onSearch: (String) -> Unit, onSearch: (String) -> Unit,
@ -40,7 +41,7 @@ fun MigrateSearchScreen(
}, },
) { paddingValues -> ) { paddingValues ->
GlobalSearchContent( GlobalSearchContent(
fromSourceId = state.manga?.source, fromSourceId = fromSourceId,
items = state.filteredItems, items = state.filteredItems,
contentPadding = paddingValues, contentPadding = paddingValues,
getManga = getManga, getManga = getManga,

View file

@ -10,7 +10,6 @@ import eu.kanade.presentation.browse.MigrateSearchScreen
import eu.kanade.presentation.util.Screen import eu.kanade.presentation.util.Screen
import eu.kanade.tachiyomi.ui.manga.MangaScreen import eu.kanade.tachiyomi.ui.manga.MangaScreen
// TODO: this should probably be merged with GlobalSearchScreen somehow to dedupe logic
class MigrateSearchScreen(private val mangaId: Long) : Screen() { class MigrateSearchScreen(private val mangaId: Long) : Screen() {
@Composable @Composable
@ -20,8 +19,12 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) } val screenModel = rememberScreenModel { MigrateSearchScreenModel(mangaId = mangaId) }
val state by screenModel.state.collectAsState() val state by screenModel.state.collectAsState()
val dialogScreenModel = rememberScreenModel { MigrateSearchScreenDialogScreenModel(mangaId = mangaId) }
val dialogState by dialogScreenModel.state.collectAsState()
MigrateSearchScreen( MigrateSearchScreen(
state = state, state = state,
fromSourceId = dialogState.manga?.source,
navigateUp = navigator::pop, navigateUp = navigator::pop,
onChangeSearchQuery = screenModel::updateSearchQuery, onChangeSearchQuery = screenModel::updateSearchQuery,
onSearch = screenModel::search, onSearch = screenModel::search,
@ -29,19 +32,19 @@ class MigrateSearchScreen(private val mangaId: Long) : Screen() {
onChangeSearchFilter = screenModel::setSourceFilter, onChangeSearchFilter = screenModel::setSourceFilter,
onToggleResults = screenModel::toggleFilterResults, onToggleResults = screenModel::toggleFilterResults,
onClickSource = { onClickSource = {
navigator.push(SourceSearchScreen(state.manga!!, it.id, state.searchQuery)) navigator.push(SourceSearchScreen(dialogState.manga!!, it.id, state.searchQuery))
}, },
onClickItem = { screenModel.setDialog(MigrateSearchScreenModel.Dialog.Migrate(it)) }, onClickItem = { dialogScreenModel.setDialog(MigrateSearchScreenDialogScreenModel.Dialog.Migrate(it)) },
onLongClickItem = { navigator.push(MangaScreen(it.id, true)) }, onLongClickItem = { navigator.push(MangaScreen(it.id, true)) },
) )
when (val dialog = state.dialog) { when (val dialog = dialogState.dialog) {
is MigrateSearchScreenModel.Dialog.Migrate -> { is MigrateSearchScreenDialogScreenModel.Dialog.Migrate -> {
MigrateDialog( MigrateDialog(
oldManga = state.manga!!, oldManga = dialogState.manga!!,
newManga = dialog.manga, newManga = dialog.manga,
screenModel = rememberScreenModel { MigrateDialogScreenModel() }, screenModel = rememberScreenModel { MigrateDialogScreenModel() },
onDismissRequest = { screenModel.setDialog(null) }, onDismissRequest = { dialogScreenModel.setDialog(null) },
onClickTitle = { onClickTitle = {
navigator.push(MangaScreen(dialog.manga.id, true)) navigator.push(MangaScreen(dialog.manga.id, true))
}, },

View file

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch
import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrateSearchScreenDialogScreenModel(
val mangaId: Long,
getManga: GetManga = Injekt.get(),
) : StateScreenModel<MigrateSearchScreenDialogScreenModel.State>(State()) {
init {
coroutineScope.launch {
val manga = getManga.await(mangaId)!!
mutableState.update {
it.copy(manga = manga)
}
}
}
fun setDialog(dialog: Dialog?) {
mutableState.update {
it.copy(dialog = dialog)
}
}
@Immutable
data class State(
val manga: Manga? = null,
val dialog: Dialog? = null,
)
sealed class Dialog {
data class Migrate(val manga: Manga) : Dialog()
}
}

View file

@ -1,29 +1,26 @@
package eu.kanade.tachiyomi.ui.browse.migration.search package eu.kanade.tachiyomi.ui.browse.migration.search
import androidx.compose.runtime.Immutable
import cafe.adriel.voyager.core.model.coroutineScope import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchItemResult
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SearchScreenModel
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.SourceFilter
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import tachiyomi.domain.manga.interactor.GetManga import tachiyomi.domain.manga.interactor.GetManga
import tachiyomi.domain.manga.model.Manga
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MigrateSearchScreenModel( class MigrateSearchScreenModel(
val mangaId: Long, val mangaId: Long,
getManga: GetManga = Injekt.get(), getManga: GetManga = Injekt.get(),
) : SearchScreenModel<MigrateSearchScreenModel.State>(State()) { ) : SearchScreenModel() {
init { init {
coroutineScope.launch { coroutineScope.launch {
val manga = getManga.await(mangaId)!! val manga = getManga.await(mangaId)!!
mutableState.update { mutableState.update {
it.copy(manga = manga, searchQuery = manga.title) it.copy(fromSourceId = manga.source, searchQuery = manga.title)
} }
search(manga.title) search(manga.title)
@ -35,61 +32,10 @@ class MigrateSearchScreenModel(
.filter { mutableState.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources } .filter { mutableState.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
.sortedWith( .sortedWith(
compareBy( compareBy(
{ it.id != state.value.manga!!.source }, { it.id != state.value.fromSourceId },
{ "${it.id}" !in pinnedSources }, { "${it.id}" !in pinnedSources },
{ "${it.name.lowercase()} (${it.lang})" }, { "${it.name.lowercase()} (${it.lang})" },
), ),
) )
} }
override fun updateSearchQuery(query: String?) {
mutableState.update {
it.copy(searchQuery = query)
}
}
override fun updateItems(items: Map<CatalogueSource, SearchItemResult>) {
mutableState.update {
it.copy(items = items)
}
}
override fun getItems(): Map<CatalogueSource, SearchItemResult> {
return mutableState.value.items
}
override fun setSourceFilter(filter: SourceFilter) {
mutableState.update { it.copy(sourceFilter = filter) }
}
override fun toggleFilterResults() {
mutableState.update {
it.copy(onlyShowHasResults = !it.onlyShowHasResults)
}
}
fun setDialog(dialog: Dialog?) {
mutableState.update {
it.copy(dialog = dialog)
}
}
@Immutable
data class State(
val manga: Manga? = null,
val dialog: Dialog? = null,
val searchQuery: String? = null,
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
val onlyShowHasResults: Boolean = false,
val items: Map<CatalogueSource, SearchItemResult> = emptyMap(),
) {
val progress: Int = items.count { it.value !is SearchItemResult.Loading }
val total: Int = items.size
val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
}
sealed class Dialog {
data class Migrate(val manga: Manga) : Dialog()
}
} }

View file

@ -1,13 +1,11 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.compose.runtime.Immutable
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import kotlinx.coroutines.flow.update
class GlobalSearchScreenModel( class GlobalSearchScreenModel(
initialQuery: String = "", initialQuery: String = "",
initialExtensionFilter: String? = null, initialExtensionFilter: String? = null,
) : SearchScreenModel<GlobalSearchScreenModel.State>(State(searchQuery = initialQuery)) { ) : SearchScreenModel(State(searchQuery = initialQuery)) {
init { init {
extensionFilter = initialExtensionFilter extensionFilter = initialExtensionFilter
@ -20,42 +18,4 @@ class GlobalSearchScreenModel(
return super.getEnabledSources() return super.getEnabledSources()
.filter { mutableState.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources } .filter { mutableState.value.sourceFilter != SourceFilter.PinnedOnly || "${it.id}" in pinnedSources }
} }
override fun updateSearchQuery(query: String?) {
mutableState.update {
it.copy(searchQuery = query)
}
}
override fun updateItems(items: Map<CatalogueSource, SearchItemResult>) {
mutableState.update {
it.copy(items = items)
}
}
override fun getItems(): Map<CatalogueSource, SearchItemResult> {
return mutableState.value.items
}
override fun setSourceFilter(filter: SourceFilter) {
mutableState.update { it.copy(sourceFilter = filter) }
}
override fun toggleFilterResults() {
mutableState.update {
it.copy(onlyShowHasResults = !it.onlyShowHasResults)
}
}
@Immutable
data class State(
val searchQuery: String? = null,
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
val onlyShowHasResults: Boolean = false,
val items: Map<CatalogueSource, SearchItemResult> = emptyMap(),
) {
val progress: Int = items.count { it.value !is SearchItemResult.Loading }
val total: Int = items.size
val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
}
} }

View file

@ -1,10 +1,9 @@
package eu.kanade.tachiyomi.ui.browse.source.globalsearch package eu.kanade.tachiyomi.ui.browse.source.globalsearch
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.State import androidx.compose.runtime.Immutable
import androidx.compose.runtime.produceState import androidx.compose.runtime.produceState
import cafe.adriel.voyager.core.model.StateScreenModel import cafe.adriel.voyager.core.model.StateScreenModel
import eu.kanade.domain.manga.interactor.UpdateManga
import eu.kanade.domain.manga.model.toDomainManga import eu.kanade.domain.manga.model.toDomainManga
import eu.kanade.domain.source.service.SourcePreferences import eu.kanade.domain.source.service.SourcePreferences
import eu.kanade.presentation.util.ioCoroutineScope import eu.kanade.presentation.util.ioCoroutineScope
@ -16,6 +15,7 @@ import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.filterNotNull import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.update
import kotlinx.coroutines.launch import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext import kotlinx.coroutines.withContext
import tachiyomi.core.util.lang.awaitSingle import tachiyomi.core.util.lang.awaitSingle
@ -27,15 +27,14 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.concurrent.Executors import java.util.concurrent.Executors
abstract class SearchScreenModel<T>( abstract class SearchScreenModel(
initialState: T, initialState: State = State(),
private val sourcePreferences: SourcePreferences = Injekt.get(), private val sourcePreferences: SourcePreferences = Injekt.get(),
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
private val networkToLocalManga: NetworkToLocalManga = Injekt.get(), private val networkToLocalManga: NetworkToLocalManga = Injekt.get(),
private val getManga: GetManga = Injekt.get(), private val getManga: GetManga = Injekt.get(),
private val updateManga: UpdateManga = Injekt.get(), ) : StateScreenModel<SearchScreenModel.State>(initialState) {
) : StateScreenModel<T>(initialState) {
private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher() private val coroutineDispatcher = Executors.newFixedThreadPool(5).asCoroutineDispatcher()
private var searchJob: Job? = null private var searchJob: Job? = null
@ -55,7 +54,7 @@ abstract class SearchScreenModel<T>(
} }
@Composable @Composable
fun getManga(initialManga: Manga): State<Manga> { fun getManga(initialManga: Manga): androidx.compose.runtime.State<Manga> {
return produceState(initialValue = initialManga) { return produceState(initialValue = initialManga) {
getManga.subscribe(initialManga.url, initialManga.source) getManga.subscribe(initialManga.url, initialManga.source)
.filterNotNull() .filterNotNull()
@ -95,19 +94,25 @@ abstract class SearchScreenModel<T>(
.filter { it in enabledSources } .filter { it in enabledSources }
} }
abstract fun updateSearchQuery(query: String?) fun updateSearchQuery(query: String?) {
mutableState.update {
abstract fun updateItems(items: Map<CatalogueSource, SearchItemResult>) it.copy(searchQuery = query)
}
abstract fun getItems(): Map<CatalogueSource, SearchItemResult>
private fun getAndUpdateItems(function: (Map<CatalogueSource, SearchItemResult>) -> Map<CatalogueSource, SearchItemResult>) {
updateItems(function(getItems()))
} }
abstract fun setSourceFilter(filter: SourceFilter) fun getItems(): Map<CatalogueSource, SearchItemResult> {
return mutableState.value.items
}
abstract fun toggleFilterResults() fun setSourceFilter(filter: SourceFilter) {
mutableState.update { it.copy(sourceFilter = filter) }
}
fun toggleFilterResults() {
mutableState.update {
it.copy(onlyShowHasResults = !it.onlyShowHasResults)
}
}
fun search(query: String) { fun search(query: String) {
if (this.query == query) return if (this.query == query) return
@ -147,6 +152,29 @@ abstract class SearchScreenModel<T>(
.awaitAll() .awaitAll()
} }
} }
private fun updateItems(items: Map<CatalogueSource, SearchItemResult>) {
mutableState.update {
it.copy(items = items)
}
}
private fun getAndUpdateItems(function: (Map<CatalogueSource, SearchItemResult>) -> Map<CatalogueSource, SearchItemResult>) {
updateItems(function(getItems()))
}
@Immutable
data class State(
val fromSourceId: Long? = null,
val searchQuery: String? = null,
val sourceFilter: SourceFilter = SourceFilter.PinnedOnly,
val onlyShowHasResults: Boolean = false,
val items: Map<CatalogueSource, SearchItemResult> = emptyMap(),
) {
val progress: Int = items.count { it.value !is SearchItemResult.Loading }
val total: Int = items.size
val filteredItems = items.filter { (_, result) -> result.isVisible(onlyShowHasResults) }
}
} }
enum class SourceFilter { enum class SourceFilter {