Use Stable interface for Browse screens (#7544)

This commit is contained in:
Andreas 2022-07-16 20:44:37 +02:00 committed by GitHub
parent 383f7089c4
commit 018ca71336
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
26 changed files with 505 additions and 307 deletions

View File

@ -1,5 +1,8 @@
package eu.kanade.presentation.browse package eu.kanade.presentation.browse
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.util.DisplayMetrics import android.util.DisplayMetrics
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.foundation.background import androidx.compose.foundation.background
@ -32,7 +35,6 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -51,6 +53,7 @@ import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.components.DIVIDER_ALPHA import eu.kanade.presentation.components.DIVIDER_ALPHA
import eu.kanade.presentation.components.Divider import eu.kanade.presentation.components.Divider
import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.horizontalPadding
@ -66,65 +69,68 @@ fun ExtensionDetailsScreen(
nestedScrollInterop: NestedScrollConnection, nestedScrollInterop: NestedScrollConnection,
presenter: ExtensionDetailsPresenter, presenter: ExtensionDetailsPresenter,
onClickUninstall: () -> Unit, onClickUninstall: () -> Unit,
onClickAppInfo: () -> Unit,
onClickSourcePreferences: (sourceId: Long) -> Unit, onClickSourcePreferences: (sourceId: Long) -> Unit,
onClickSource: (sourceId: Long) -> Unit, onClickSource: (sourceId: Long) -> Unit,
) { ) {
val extension = presenter.extension when {
presenter.isLoading -> LoadingScreen()
presenter.extension == null -> EmptyScreen(textResource = R.string.empty_screen)
else -> {
val context = LocalContext.current
val extension = presenter.extension
var showNsfwWarning by remember { mutableStateOf(false) }
if (extension == null) { ScrollbarLazyColumn(
EmptyScreen(textResource = R.string.empty_screen) modifier = Modifier.nestedScroll(nestedScrollInterop),
return contentPadding = WindowInsets.navigationBars.asPaddingValues(),
} ) {
when {
val sources by presenter.sourcesState.collectAsState() extension.isUnofficial ->
item {
var showNsfwWarning by remember { mutableStateOf(false) } WarningBanner(R.string.unofficial_extension_message)
}
ScrollbarLazyColumn( extension.isObsolete ->
modifier = Modifier.nestedScroll(nestedScrollInterop), item {
contentPadding = WindowInsets.navigationBars.asPaddingValues(), WarningBanner(R.string.obsolete_extension_message)
) { }
when {
extension.isUnofficial ->
item {
WarningBanner(R.string.unofficial_extension_message)
} }
extension.isObsolete ->
item { item {
WarningBanner(R.string.obsolete_extension_message) DetailsHeader(
extension = extension,
onClickUninstall = onClickUninstall,
onClickAppInfo = {
Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", extension.pkgName, null)
context.startActivity(this)
}
},
onClickAgeRating = {
showNsfwWarning = true
},
)
} }
}
item { items(
DetailsHeader( items = presenter.sources,
extension = extension, key = { it.source.id },
onClickUninstall = onClickUninstall, ) { source ->
onClickAppInfo = onClickAppInfo, SourceSwitchPreference(
onClickAgeRating = { modifier = Modifier.animateItemPlacement(),
showNsfwWarning = true source = source,
}, onClickSourcePreferences = onClickSourcePreferences,
) onClickSource = onClickSource,
)
}
}
if (showNsfwWarning) {
NsfwWarningDialog(
onClickConfirm = {
showNsfwWarning = false
},
)
}
} }
items(
items = sources,
key = { it.source.id },
) { source ->
SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(),
source = source,
onClickSourcePreferences = onClickSourcePreferences,
onClickSource = onClickSource,
)
}
}
if (showNsfwWarning) {
NsfwWarningDialog(
onClickConfirm = {
showNsfwWarning = false
},
)
} }
} }

View File

@ -0,0 +1,25 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionSourceItem
@Stable
interface ExtensionDetailsState {
val isLoading: Boolean
val extension: Extension.Installed?
val sources: List<ExtensionSourceItem>
}
fun ExtensionDetailsState(): ExtensionDetailsState {
return ExtensionDetailsStateImpl()
}
class ExtensionDetailsStateImpl : ExtensionDetailsState {
override var isLoading: Boolean by mutableStateOf(true)
override var extension: Extension.Installed? by mutableStateOf(null)
override var sources: List<ExtensionSourceItem> by mutableStateOf(emptyList())
}

View File

@ -5,10 +5,8 @@ import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
@ -19,47 +17,52 @@ import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterPresenter import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterPresenter
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionFilterState
import eu.kanade.tachiyomi.ui.browse.extension.FilterUiModel
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun ExtensionFilterScreen( fun ExtensionFilterScreen(
nestedScrollInterop: NestedScrollConnection, nestedScrollInterop: NestedScrollConnection,
presenter: ExtensionFilterPresenter, presenter: ExtensionFilterPresenter,
onClickLang: (String) -> Unit,
) { ) {
val state by presenter.state.collectAsState() val context = LocalContext.current
when {
when (state) { presenter.isLoading -> LoadingScreen()
is ExtensionFilterState.Loading -> LoadingScreen() presenter.isEmpty -> EmptyScreen(textResource = R.string.empty_screen)
is ExtensionFilterState.Error -> Text(text = (state as ExtensionFilterState.Error).error.message!!) else -> {
is ExtensionFilterState.Success ->
SourceFilterContent( SourceFilterContent(
nestedScrollInterop = nestedScrollInterop, nestedScrollInterop = nestedScrollInterop,
items = (state as ExtensionFilterState.Success).models, state = presenter,
onClickLang = onClickLang, onClickLang = {
presenter.toggleLanguage(it)
},
) )
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest {
when (it) {
ExtensionFilterPresenter.Event.FailedFetchingLanguages -> {
context.toast(R.string.internal_error)
}
}
}
} }
} }
@Composable @Composable
fun SourceFilterContent( fun SourceFilterContent(
nestedScrollInterop: NestedScrollConnection, nestedScrollInterop: NestedScrollConnection,
items: List<FilterUiModel>, state: ExtensionFilterState,
onClickLang: (String) -> Unit, onClickLang: (String) -> Unit,
) { ) {
if (items.isEmpty()) {
EmptyScreen(textResource = R.string.empty_screen)
return
}
LazyColumn( LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop), modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(), contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) { ) {
items( items(
items = items, items = state.items,
) { model -> ) { model ->
ExtensionFilterItem( ExtensionFilterItem(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),

View File

@ -0,0 +1,25 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.extension.FilterUiModel
@Stable
interface ExtensionFilterState {
val isLoading: Boolean
val items: List<FilterUiModel>
val isEmpty: Boolean
}
fun ExtensionFilterState(): ExtensionFilterState {
return ExtensionFilterStateImpl()
}
class ExtensionFilterStateImpl : ExtensionFilterState {
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<FilterUiModel> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@ -23,7 +23,6 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember import androidx.compose.runtime.remember
@ -40,7 +39,9 @@ import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.presentation.browse.components.BaseBrowseItem import eu.kanade.presentation.browse.components.BaseBrowseItem
import eu.kanade.presentation.browse.components.ExtensionIcon import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.FastScrollLazyColumn import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.SwipeRefreshIndicator import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.theme.header import eu.kanade.presentation.theme.header
import eu.kanade.presentation.util.horizontalPadding import eu.kanade.presentation.util.horizontalPadding
@ -49,7 +50,6 @@ import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionState
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter import eu.kanade.tachiyomi.ui.browse.extension.ExtensionsPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
@ -69,19 +69,18 @@ fun ExtensionScreen(
onRefresh: () -> Unit, onRefresh: () -> Unit,
onLaunched: () -> Unit, onLaunched: () -> Unit,
) { ) {
val state by presenter.state.collectAsState()
val isRefreshing = presenter.isRefreshing
SwipeRefresh( SwipeRefresh(
modifier = Modifier.nestedScroll(nestedScrollInterop), modifier = Modifier.nestedScroll(nestedScrollInterop),
state = rememberSwipeRefreshState(isRefreshing), state = rememberSwipeRefreshState(presenter.isRefreshing),
indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) }, indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) },
onRefresh = onRefresh, onRefresh = onRefresh,
) { ) {
when (state) { when {
is ExtensionState.Initialized -> { presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(R.string.empty_screen)
else -> {
ExtensionContent( ExtensionContent(
items = (state as ExtensionState.Initialized).list, state = presenter,
onLongClickItem = onLongClickItem, onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel, onClickItemCancel = onClickItemCancel,
onInstallExtension = onInstallExtension, onInstallExtension = onInstallExtension,
@ -93,14 +92,13 @@ fun ExtensionScreen(
onLaunched = onLaunched, onLaunched = onLaunched,
) )
} }
ExtensionState.Uninitialized -> {}
} }
} }
} }
@Composable @Composable
fun ExtensionContent( fun ExtensionContent(
items: List<ExtensionUiModel>, state: ExtensionsState,
onLongClickItem: (Extension) -> Unit, onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit, onClickItemCancel: (Extension) -> Unit,
onInstallExtension: (Extension.Available) -> Unit, onInstallExtension: (Extension.Available) -> Unit,
@ -117,7 +115,7 @@ fun ExtensionContent(
contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) { ) {
items( items(
items = items, items = state.items,
key = { key = {
when (it) { when (it) {
is ExtensionUiModel.Header.Resource -> it.textRes is ExtensionUiModel.Header.Resource -> it.textRes

View File

@ -0,0 +1,25 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionUiModel
interface ExtensionsState {
val isLoading: Boolean
val isRefreshing: Boolean
val items: List<ExtensionUiModel>
val isEmpty: Boolean
}
fun ExtensionState(): ExtensionsState {
return ExtensionsStateImpl()
}
class ExtensionsStateImpl : ExtensionsState {
override var isLoading: Boolean by mutableStateOf(true)
override var isRefreshing: Boolean by mutableStateOf(false)
override var items: List<ExtensionUiModel> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@ -4,61 +4,66 @@ import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.asPaddingValues import androidx.compose.foundation.layout.asPaddingValues
import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.manga.model.Manga import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.EmptyScreen import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.manga.components.BaseMangaListItem import eu.kanade.presentation.manga.components.BaseMangaListItem
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaPresenter import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter.Event
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun MigrateMangaScreen( fun MigrateMangaScreen(
nestedScrollInterop: NestedScrollConnection, nestedScrollInterop: NestedScrollConnection,
presenter: MigrationMangaPresenter, presenter: MigrateMangaPresenter,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit, onClickCover: (Manga) -> Unit,
) { ) {
val state by presenter.state.collectAsState() val context = LocalContext.current
when {
when (state) { presenter.isLoading -> LoadingScreen()
MigrateMangaState.Loading -> LoadingScreen() presenter.isEmpty -> EmptyScreen(textResource = R.string.empty_screen)
is MigrateMangaState.Error -> Text(text = (state as MigrateMangaState.Error).error.message!!) else -> {
is MigrateMangaState.Success -> {
MigrateMangaContent( MigrateMangaContent(
nestedScrollInterop = nestedScrollInterop, nestedScrollInterop = nestedScrollInterop,
list = (state as MigrateMangaState.Success).list, state = presenter,
onClickItem = onClickItem, onClickItem = onClickItem,
onClickCover = onClickCover, onClickCover = onClickCover,
) )
} }
} }
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
Event.FailedFetchingFavorites -> {
context.toast(R.string.internal_error)
}
}
}
}
} }
@Composable @Composable
fun MigrateMangaContent( fun MigrateMangaContent(
nestedScrollInterop: NestedScrollConnection, nestedScrollInterop: NestedScrollConnection,
list: List<Manga>, state: MigrateMangaState,
onClickItem: (Manga) -> Unit, onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit, onClickCover: (Manga) -> Unit,
) { ) {
if (list.isEmpty()) {
EmptyScreen(textResource = R.string.empty_screen)
return
}
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop), modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(), contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) { ) {
items(list) { manga -> items(state.items) { manga ->
MigrateMangaItem( MigrateMangaItem(
manga = manga, manga = manga,
onClickItem = onClickItem, onClickItem = onClickItem,

View File

@ -0,0 +1,23 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.domain.manga.model.Manga
interface MigrateMangaState {
val isLoading: Boolean
val items: List<Manga>
val isEmpty: Boolean
}
fun MigrationMangaState(): MigrateMangaState {
return MigrateMangaStateImpl()
}
class MigrateMangaStateImpl : MigrateMangaState {
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<Manga> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@ -11,12 +11,12 @@ import androidx.compose.foundation.lazy.items
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
@ -32,27 +32,29 @@ import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.presentation.util.plus import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topPaddingValues import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrateSourceState
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.copyToClipboard
@Composable @Composable
fun MigrateSourceScreen( fun MigrateSourceScreen(
nestedScrollInterop: NestedScrollConnection, nestedScrollInterop: NestedScrollConnection,
presenter: MigrationSourcesPresenter, presenter: MigrationSourcesPresenter,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit,
) { ) {
val state by presenter.state.collectAsState() val context = LocalContext.current
when (state) { when {
is MigrateSourceState.Loading -> LoadingScreen() presenter.isLoading -> LoadingScreen()
is MigrateSourceState.Error -> Text(text = (state as MigrateSourceState.Error).error.message!!) presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library)
is MigrateSourceState.Success -> else ->
MigrateSourceList( MigrateSourceList(
nestedScrollInterop = nestedScrollInterop, nestedScrollInterop = nestedScrollInterop,
list = (state as MigrateSourceState.Success).sources, list = presenter.items,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = onLongClickItem, onLongClickItem = { source ->
val sourceId = source.id.toString()
context.copyToClipboard(sourceId, sourceId)
},
) )
} }
} }
@ -64,11 +66,6 @@ fun MigrateSourceList(
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit, onLongClickItem: (Source) -> Unit,
) { ) {
if (list.isEmpty()) {
EmptyScreen(textResource = R.string.information_empty_library)
return
}
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop), modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,

View File

@ -0,0 +1,23 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.domain.source.model.Source
interface MigrateSourceState {
val isLoading: Boolean
val items: List<Pair<Source, Long>>
val isEmpty: Boolean
}
fun MigrateSourceState(): MigrateSourceState {
return MigrateSourceStateImpl()
}
class MigrateSourceStateImpl : MigrateSourceState {
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<Pair<Source, Long>> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@ -6,9 +6,8 @@ import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.lazy.items import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Checkbox import androidx.compose.material3.Checkbox
import androidx.compose.material3.Switch import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
@ -22,9 +21,10 @@ import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel
import eu.kanade.tachiyomi.ui.browse.source.SourceFilterState
import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterPresenter import eu.kanade.tachiyomi.ui.browse.source.SourcesFilterPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun SourcesFilterScreen( fun SourcesFilterScreen(
@ -33,39 +33,43 @@ fun SourcesFilterScreen(
onClickLang: (String) -> Unit, onClickLang: (String) -> Unit,
onClickSource: (Source) -> Unit, onClickSource: (Source) -> Unit,
) { ) {
val state by presenter.state.collectAsState() val context = LocalContext.current
when {
when (state) { presenter.isLoading -> LoadingScreen()
is SourceFilterState.Loading -> LoadingScreen() presenter.isEmpty -> EmptyScreen(textResource = R.string.source_filter_empty_screen)
is SourceFilterState.Error -> Text(text = (state as SourceFilterState.Error).error.message!!) else -> {
is SourceFilterState.Success ->
SourcesFilterContent( SourcesFilterContent(
nestedScrollInterop = nestedScrollInterop, nestedScrollInterop = nestedScrollInterop,
items = (state as SourceFilterState.Success).models, state = presenter,
onClickLang = onClickLang, onClickLang = onClickLang,
onClickSource = onClickSource, onClickSource = onClickSource,
) )
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
SourcesFilterPresenter.Event.FailedFetchingLanguages -> {
context.toast(R.string.internal_error)
}
}
}
} }
} }
@Composable @Composable
fun SourcesFilterContent( fun SourcesFilterContent(
nestedScrollInterop: NestedScrollConnection, nestedScrollInterop: NestedScrollConnection,
items: List<FilterUiModel>, state: SourcesFilterState,
onClickLang: (String) -> Unit, onClickLang: (String) -> Unit,
onClickSource: (Source) -> Unit, onClickSource: (Source) -> Unit,
) { ) {
if (items.isEmpty()) {
EmptyScreen(textResource = R.string.source_filter_empty_screen)
return
}
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop), modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(), contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) { ) {
items( items(
items = items, items = state.items,
contentType = { contentType = {
when (it) { when (it) {
is FilterUiModel.Header -> "header" is FilterUiModel.Header -> "header"

View File

@ -0,0 +1,23 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.source.FilterUiModel
interface SourcesFilterState {
val isLoading: Boolean
val items: List<FilterUiModel>
val isEmpty: Boolean
}
fun SourcesFilterState(): SourcesFilterState {
return SourcesFilterStateImpl()
}
class SourcesFilterStateImpl : SourcesFilterState {
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<FilterUiModel> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@ -19,10 +19,8 @@ import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
@ -42,9 +40,11 @@ import eu.kanade.presentation.util.plus
import eu.kanade.presentation.util.topPaddingValues import eu.kanade.presentation.util.topPaddingValues
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.browse.source.SourceState
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter.Dialog
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable @Composable
fun SourcesScreen( fun SourcesScreen(
@ -55,44 +55,47 @@ fun SourcesScreen(
onClickLatest: (Source) -> Unit, onClickLatest: (Source) -> Unit,
onClickPin: (Source) -> Unit, onClickPin: (Source) -> Unit,
) { ) {
val state by presenter.state.collectAsState() val context = LocalContext.current
when {
when (state) { presenter.isLoading -> LoadingScreen()
is SourceState.Loading -> LoadingScreen() presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen)
is SourceState.Error -> Text(text = (state as SourceState.Error).error.message!!) else -> {
is SourceState.Success -> SourceList( SourceList(
nestedScrollConnection = nestedScrollInterop, nestedScrollConnection = nestedScrollInterop,
list = (state as SourceState.Success).uiModels, state = presenter,
onClickItem = onClickItem, onClickItem = onClickItem,
onClickDisable = onClickDisable, onClickDisable = onClickDisable,
onClickLatest = onClickLatest, onClickLatest = onClickLatest,
onClickPin = onClickPin, onClickPin = onClickPin,
) )
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
SourcesPresenter.Event.FailedFetchingSources -> {
context.toast(R.string.internal_error)
}
}
}
} }
} }
@Composable @Composable
fun SourceList( fun SourceList(
nestedScrollConnection: NestedScrollConnection, nestedScrollConnection: NestedScrollConnection,
list: List<SourceUiModel>, state: SourcesState,
onClickItem: (Source) -> Unit, onClickItem: (Source) -> Unit,
onClickDisable: (Source) -> Unit, onClickDisable: (Source) -> Unit,
onClickLatest: (Source) -> Unit, onClickLatest: (Source) -> Unit,
onClickPin: (Source) -> Unit, onClickPin: (Source) -> Unit,
) { ) {
if (list.isEmpty()) {
EmptyScreen(textResource = R.string.source_empty_screen)
return
}
var sourceState by remember { mutableStateOf<Source?>(null) }
ScrollbarLazyColumn( ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollConnection), modifier = Modifier.nestedScroll(nestedScrollConnection),
contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues, contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) { ) {
items( items(
items = list, items = state.items,
contentType = { contentType = {
when (it) { when (it) {
is SourceUiModel.Header -> "header" is SourceUiModel.Header -> "header"
@ -117,7 +120,7 @@ fun SourceList(
modifier = Modifier.animateItemPlacement(), modifier = Modifier.animateItemPlacement(),
source = model.source, source = model.source,
onClickItem = onClickItem, onClickItem = onClickItem,
onLongClickItem = { sourceState = it }, onLongClickItem = { state.dialog = Dialog(it) },
onClickLatest = onClickLatest, onClickLatest = onClickLatest,
onClickPin = onClickPin, onClickPin = onClickPin,
) )
@ -125,18 +128,19 @@ fun SourceList(
} }
} }
if (sourceState != null) { if (state.dialog != null) {
val source = state.dialog!!.source
SourceOptionsDialog( SourceOptionsDialog(
source = sourceState!!, source = source,
onClickPin = { onClickPin = {
onClickPin(sourceState!!) onClickPin(source)
sourceState = null state.dialog = null
}, },
onClickDisable = { onClickDisable = {
onClickDisable(sourceState!!) onClickDisable(source)
sourceState = null state.dialog = null
}, },
onDismiss = { sourceState = null }, onDismiss = { state.dialog = null },
) )
} }
} }

View File

@ -0,0 +1,27 @@
package eu.kanade.presentation.browse
import androidx.compose.runtime.Stable
import androidx.compose.runtime.derivedStateOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.tachiyomi.ui.browse.source.SourcesPresenter
@Stable
interface SourcesState {
var dialog: SourcesPresenter.Dialog?
val isLoading: Boolean
val items: List<SourceUiModel>
val isEmpty: Boolean
}
fun SourcesState(): SourcesState {
return SourcesStateImpl()
}
class SourcesStateImpl : SourcesState {
override var dialog: SourcesPresenter.Dialog? by mutableStateOf(null)
override var isLoading: Boolean by mutableStateOf(true)
override var items: List<SourceUiModel> by mutableStateOf(emptyList())
override val isEmpty: Boolean by derivedStateOf { items.isEmpty() }
}

View File

@ -20,6 +20,9 @@ import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import logcat.LogPriority import logcat.LogPriority
import rx.Observable import rx.Observable
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
@ -63,9 +66,16 @@ class ExtensionManager(
var installedExtensions = emptyList<Extension.Installed>() var installedExtensions = emptyList<Extension.Installed>()
private set(value) { private set(value) {
field = value field = value
installedExtensionsFlow.value = field
installedExtensionsRelay.call(value) installedExtensionsRelay.call(value)
} }
private val installedExtensionsFlow = MutableStateFlow(installedExtensions)
fun getInstalledExtensionsFlow(): StateFlow<List<Extension.Installed>> {
return installedExtensionsFlow.asStateFlow()
}
fun getAppIconForSource(source: Source): Drawable? { fun getAppIconForSource(source: Source): Drawable? {
return getAppIconForSource(source.id) return getAppIconForSource(source.id)
} }

View File

@ -17,9 +17,6 @@ class ExtensionFilterController : ComposeController<ExtensionFilterPresenter>()
ExtensionFilterScreen( ExtensionFilterScreen(
nestedScrollInterop = nestedScrollInterop, nestedScrollInterop = nestedScrollInterop,
presenter = presenter, presenter = presenter,
onClickLang = { language ->
presenter.toggleLanguage(language)
},
) )
} }
} }

View File

@ -3,32 +3,37 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.os.Bundle import android.os.Bundle
import eu.kanade.domain.extension.interactor.GetExtensionLanguages import eu.kanade.domain.extension.interactor.GetExtensionLanguages
import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.presentation.browse.ExtensionFilterState
import eu.kanade.presentation.browse.ExtensionFilterStateImpl
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import logcat.LogPriority
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ExtensionFilterPresenter( class ExtensionFilterPresenter(
private val state: ExtensionFilterStateImpl = ExtensionFilterState() as ExtensionFilterStateImpl,
private val getExtensionLanguages: GetExtensionLanguages = Injekt.get(), private val getExtensionLanguages: GetExtensionLanguages = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(), private val toggleLanguage: ToggleLanguage = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(),
) : BasePresenter<ExtensionFilterController>() { ) : BasePresenter<ExtensionFilterController>(), ExtensionFilterState by state {
private val _state: MutableStateFlow<ExtensionFilterState> = MutableStateFlow(ExtensionFilterState.Loading) private val _events = Channel<Event>(Int.MAX_VALUE)
val state: StateFlow<ExtensionFilterState> = _state.asStateFlow() val events = _events.receiveAsFlow()
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
presenterScope.launchIO { presenterScope.launchIO {
getExtensionLanguages.subscribe() getExtensionLanguages.subscribe()
.catch { exception -> .catch { exception ->
_state.value = ExtensionFilterState.Error(exception) logcat(LogPriority.ERROR, exception)
_events.send(Event.FailedFetchingLanguages)
} }
.collectLatest(::collectLatestSourceLangMap) .collectLatest(::collectLatestSourceLangMap)
} }
@ -36,19 +41,17 @@ class ExtensionFilterPresenter(
private fun collectLatestSourceLangMap(extLangs: List<String>) { private fun collectLatestSourceLangMap(extLangs: List<String>) {
val enabledLanguages = preferences.enabledLanguages().get() val enabledLanguages = preferences.enabledLanguages().get()
val uiModels = extLangs.map { state.items = extLangs.map {
FilterUiModel(it, it in enabledLanguages) FilterUiModel(it, it in enabledLanguages)
} }
_state.value = ExtensionFilterState.Success(uiModels) state.isLoading = false
} }
fun toggleLanguage(language: String) { fun toggleLanguage(language: String) {
toggleLanguage.await(language) toggleLanguage.await(language)
} }
}
sealed class ExtensionFilterState { sealed class Event {
object Loading : ExtensionFilterState() object FailedFetchingLanguages : Event()
data class Error(val error: Throwable) : ExtensionFilterState() }
data class Success(val models: List<FilterUiModel>) : ExtensionFilterState()
} }

View File

@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application import android.app.Application
import android.os.Bundle import android.os.Bundle
import androidx.annotation.StringRes import androidx.annotation.StringRes
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import eu.kanade.domain.extension.interactor.GetExtensionUpdates import eu.kanade.domain.extension.interactor.GetExtensionUpdates
import eu.kanade.domain.extension.interactor.GetExtensions import eu.kanade.domain.extension.interactor.GetExtensions
import eu.kanade.presentation.browse.ExtensionState
import eu.kanade.presentation.browse.ExtensionsState
import eu.kanade.presentation.browse.ExtensionsStateImpl
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
@ -17,8 +17,6 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update import kotlinx.coroutines.flow.update
@ -27,20 +25,16 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ExtensionsPresenter( class ExtensionsPresenter(
private val state: ExtensionsStateImpl = ExtensionState() as ExtensionsStateImpl,
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensionUpdates: GetExtensionUpdates = Injekt.get(), private val getExtensionUpdates: GetExtensionUpdates = Injekt.get(),
private val getExtensions: GetExtensions = Injekt.get(), private val getExtensions: GetExtensions = Injekt.get(),
) : BasePresenter<ExtensionsController>() { ) : BasePresenter<ExtensionsController>(), ExtensionsState by state {
private val _query: MutableStateFlow<String> = MutableStateFlow("") private val _query: MutableStateFlow<String> = MutableStateFlow("")
private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf()) private var _currentDownloads = MutableStateFlow<Map<String, InstallStep>>(hashMapOf())
private val _state: MutableStateFlow<ExtensionState> = MutableStateFlow(ExtensionState.Uninitialized)
val state: StateFlow<ExtensionState> = _state.asStateFlow()
var isRefreshing: Boolean by mutableStateOf(true)
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@ -86,8 +80,6 @@ class ExtensionsPresenter(
getExtensionUpdates.subscribe(), getExtensionUpdates.subscribe(),
_currentDownloads, _currentDownloads,
) { query, (installed, untrusted, available), updates, downloads -> ) { query, (installed, untrusted, available), updates, downloads ->
isRefreshing = false
val languagesWithExtensions = available val languagesWithExtensions = available
.filter(queryFilter(query)) .filter(queryFilter(query))
.groupBy { LocaleHelper.getSourceDisplayName(it.lang, context) } .groupBy { LocaleHelper.getSourceDisplayName(it.lang, context) }
@ -121,7 +113,9 @@ class ExtensionsPresenter(
items items
}.collectLatest { }.collectLatest {
_state.value = ExtensionState.Initialized(it) state.isRefreshing = false
state.isLoading = false
state.items = it
} }
} }
} }
@ -134,9 +128,9 @@ class ExtensionsPresenter(
fun updateAllExtensions() { fun updateAllExtensions() {
launchIO { launchIO {
val state = _state.value if (state.isEmpty) return@launchIO
if (state !is ExtensionState.Initialized) return@launchIO val items = state.items
state.list.mapNotNull { items.mapNotNull {
if (it !is ExtensionUiModel.Item) return@mapNotNull null if (it !is ExtensionUiModel.Item) return@mapNotNull null
if (it.extension !is Extension.Installed) return@mapNotNull null if (it.extension !is Extension.Installed) return@mapNotNull null
if (it.extension.hasUpdate.not()) return@mapNotNull null if (it.extension.hasUpdate.not()) return@mapNotNull null
@ -189,7 +183,7 @@ class ExtensionsPresenter(
} }
fun findAvailableExtensions() { fun findAvailableExtensions() {
isRefreshing = true state.isRefreshing = true
extensionManager.findAvailableExtensions() extensionManager.findAvailableExtensions()
} }
@ -217,8 +211,3 @@ sealed interface ExtensionUiModel {
} }
} }
} }
sealed class ExtensionState {
object Uninitialized : ExtensionState()
data class Initialized(val list: List<ExtensionUiModel>) : ExtensionState()
}

View File

@ -43,7 +43,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
nestedScrollInterop = nestedScrollInterop, nestedScrollInterop = nestedScrollInterop,
presenter = presenter, presenter = presenter,
onClickUninstall = { presenter.uninstallExtension() }, onClickUninstall = { presenter.uninstallExtension() },
onClickAppInfo = { presenter.openInSettings() },
onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) }, onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) },
onClickSource = { presenter.toggleSource(it) }, onClickSource = { presenter.toggleSource(it) },
) )

View File

@ -1,48 +1,52 @@
package eu.kanade.tachiyomi.ui.browse.extension.details package eu.kanade.tachiyomi.ui.browse.extension.details
import android.app.Application import android.app.Application
import android.content.Intent
import android.net.Uri
import android.os.Bundle import android.os.Bundle
import android.provider.Settings
import eu.kanade.domain.extension.interactor.GetExtensionSources import eu.kanade.domain.extension.interactor.GetExtensionSources
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.presentation.browse.ExtensionDetailsState
import eu.kanade.presentation.browse.ExtensionDetailsStateImpl
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import rx.android.schedulers.AndroidSchedulers import kotlinx.coroutines.flow.take
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class ExtensionDetailsPresenter( class ExtensionDetailsPresenter(
private val pkgName: String, private val pkgName: String,
private val state: ExtensionDetailsStateImpl = ExtensionDetailsState() as ExtensionDetailsStateImpl,
private val context: Application = Injekt.get(), private val context: Application = Injekt.get(),
private val getExtensionSources: GetExtensionSources = Injekt.get(), private val getExtensionSources: GetExtensionSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(),
private val extensionManager: ExtensionManager = Injekt.get(), private val extensionManager: ExtensionManager = Injekt.get(),
) : BasePresenter<ExtensionDetailsController>() { ) : BasePresenter<ExtensionDetailsController>(), ExtensionDetailsState by state {
val extension = extensionManager.installedExtensions.find { it.pkgName == pkgName }
private val _state: MutableStateFlow<List<ExtensionSourceItem>> = MutableStateFlow(emptyList())
val sourcesState: StateFlow<List<ExtensionSourceItem>> = _state.asStateFlow()
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
val extension = extension ?: return presenterScope.launchIO {
extensionManager.getInstalledExtensionsFlow()
.map { it.firstOrNull { it.pkgName == pkgName } }
.collectLatest {
state.extension = it
fetchExtensionSources()
}
}
bindToUninstalledExtension() bindToUninstalledExtension()
}
presenterScope.launchIO { private fun CoroutineScope.fetchExtensionSources() {
getExtensionSources.subscribe(extension) launchIO {
getExtensionSources.subscribe(extension!!)
.map { .map {
it.sortedWith( it.sortedWith(
compareBy( compareBy(
@ -51,20 +55,24 @@ class ExtensionDetailsPresenter(
), ),
) )
} }
.collectLatest { _state.value = it } .collectLatest {
state.isLoading = false
state.sources = it
}
} }
} }
private fun bindToUninstalledExtension() { private fun bindToUninstalledExtension() {
extensionManager.getInstalledExtensionsObservable() presenterScope.launchIO {
.skip(1) extensionManager.getInstalledExtensionsFlow()
.filter { extensions -> extensions.none { it.pkgName == pkgName } } .drop(1)
.map { } .filter { extensions -> extensions.none { it.pkgName == pkgName } }
.take(1) .map { }
.observeOn(AndroidSchedulers.mainThread()) .take(1)
.subscribeFirst({ view, _ -> .collectLatest {
view.onExtensionUninstalled() view?.onExtensionUninstalled()
},) }
}
} }
fun uninstallExtension() { fun uninstallExtension() {
@ -72,13 +80,6 @@ class ExtensionDetailsPresenter(
extensionManager.uninstallExtension(extension.pkgName) extensionManager.uninstallExtension(extension.pkgName)
} }
fun openInSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", pkgName, null)
}
view?.startActivity(intent)
}
fun toggleSource(sourceId: Long) { fun toggleSource(sourceId: Long) {
toggleSource.await(sourceId) toggleSource.await(sourceId)
} }

View File

@ -2,25 +2,29 @@ package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.os.Bundle import android.os.Bundle
import eu.kanade.domain.manga.interactor.GetFavorites import eu.kanade.domain.manga.interactor.GetFavorites
import eu.kanade.domain.manga.model.Manga import eu.kanade.presentation.browse.MigrateMangaState
import eu.kanade.presentation.browse.MigrateMangaStateImpl
import eu.kanade.presentation.browse.MigrationMangaState
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
import logcat.LogPriority
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MigrationMangaPresenter( class MigrateMangaPresenter(
private val sourceId: Long, private val sourceId: Long,
private val state: MigrateMangaStateImpl = MigrationMangaState() as MigrateMangaStateImpl,
private val getFavorites: GetFavorites = Injekt.get(), private val getFavorites: GetFavorites = Injekt.get(),
) : BasePresenter<MigrationMangaController>() { ) : BasePresenter<MigrationMangaController>(), MigrateMangaState by state {
private val _state: MutableStateFlow<MigrateMangaState> = MutableStateFlow(MigrateMangaState.Loading) private val _events = Channel<Event>(Int.MAX_VALUE)
val state: StateFlow<MigrateMangaState> = _state.asStateFlow() val events = _events.receiveAsFlow()
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@ -28,20 +32,20 @@ class MigrationMangaPresenter(
getFavorites getFavorites
.subscribe(sourceId) .subscribe(sourceId)
.catch { exception -> .catch { exception ->
_state.value = MigrateMangaState.Error(exception) logcat(LogPriority.ERROR, exception)
_events.send(Event.FailedFetchingFavorites)
} }
.map { list -> .map { list ->
list.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title }) list.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title })
} }
.collectLatest { sortedList -> .collectLatest { sortedList ->
_state.value = MigrateMangaState.Success(sortedList) state.isLoading = false
state.items = sortedList
} }
} }
} }
}
sealed class MigrateMangaState { sealed class Event {
object Loading : MigrateMangaState() object FailedFetchingFavorites : Event()
data class Error(val error: Throwable) : MigrateMangaState() }
data class Success(val list: List<Manga>) : MigrateMangaState()
} }

View File

@ -10,7 +10,7 @@ import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
class MigrationMangaController : ComposeController<MigrationMangaPresenter> { class MigrationMangaController : ComposeController<MigrateMangaPresenter> {
constructor(sourceId: Long, sourceName: String?) : super( constructor(sourceId: Long, sourceName: String?) : super(
bundleOf( bundleOf(
@ -30,7 +30,7 @@ class MigrationMangaController : ComposeController<MigrationMangaPresenter> {
override fun getTitle(): String? = sourceName override fun getTitle(): String? = sourceName
override fun createPresenter(): MigrationMangaPresenter = MigrationMangaPresenter(sourceId) override fun createPresenter(): MigrateMangaPresenter = MigrateMangaPresenter(sourceId)
@Composable @Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {

View File

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.ComposeController import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.ui.base.controller.pushController import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
class MigrationSourcesController : ComposeController<MigrationSourcesPresenter>() { class MigrationSourcesController : ComposeController<MigrationSourcesPresenter>() {
@ -34,10 +33,6 @@ class MigrationSourcesController : ComposeController<MigrationSourcesPresenter>(
), ),
) )
}, },
onLongClickItem = { source ->
val sourceId = source.id.toString()
activity?.copyToClipboard(sourceId, sourceId)
},
) )
} }

View File

@ -3,24 +3,27 @@ package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.os.Bundle import android.os.Bundle
import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount import eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
import eu.kanade.domain.source.interactor.SetMigrateSorting import eu.kanade.domain.source.interactor.SetMigrateSorting
import eu.kanade.domain.source.model.Source import eu.kanade.presentation.browse.MigrateSourceState
import eu.kanade.presentation.browse.MigrateSourceStateImpl
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import logcat.LogPriority
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class MigrationSourcesPresenter( class MigrationSourcesPresenter(
private val state: MigrateSourceStateImpl = MigrateSourceState() as MigrateSourceStateImpl,
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(), private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
private val setMigrateSorting: SetMigrateSorting = Injekt.get(), private val setMigrateSorting: SetMigrateSorting = Injekt.get(),
) : BasePresenter<MigrationSourcesController>() { ) : BasePresenter<MigrationSourcesController>(), MigrateSourceState by state {
private val _state: MutableStateFlow<MigrateSourceState> = MutableStateFlow(MigrateSourceState.Loading) private val _channel = Channel<Event>(Int.MAX_VALUE)
val state: StateFlow<MigrateSourceState> = _state.asStateFlow() val channel = _channel.receiveAsFlow()
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@ -28,10 +31,12 @@ class MigrationSourcesPresenter(
presenterScope.launchIO { presenterScope.launchIO {
getSourcesWithFavoriteCount.subscribe() getSourcesWithFavoriteCount.subscribe()
.catch { exception -> .catch { exception ->
_state.value = MigrateSourceState.Error(exception) logcat(LogPriority.ERROR, exception)
_channel.send(Event.FailedFetchingSourcesWithCount)
} }
.collectLatest { sources -> .collectLatest { sources ->
_state.value = MigrateSourceState.Success(sources) state.items = sources
state.isLoading = false
} }
} }
} }
@ -43,10 +48,8 @@ class MigrationSourcesPresenter(
fun setTotalSorting(isAscending: Boolean) { fun setTotalSorting(isAscending: Boolean) {
setMigrateSorting.await(SetMigrateSorting.Mode.TOTAL, isAscending) setMigrateSorting.await(SetMigrateSorting.Mode.TOTAL, isAscending)
} }
}
sealed class MigrateSourceState { sealed class Event {
object Loading : MigrateSourceState() object FailedFetchingSourcesWithCount : Event()
data class Error(val error: Throwable) : MigrateSourceState() }
data class Success(val sources: List<Pair<Source, Long>>) : MigrateSourceState()
} }

View File

@ -5,26 +5,30 @@ import eu.kanade.domain.source.interactor.GetLanguagesWithSources
import eu.kanade.domain.source.interactor.ToggleLanguage import eu.kanade.domain.source.interactor.ToggleLanguage
import eu.kanade.domain.source.interactor.ToggleSource import eu.kanade.domain.source.interactor.ToggleSource
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.SourcesFilterState
import eu.kanade.presentation.browse.SourcesFilterStateImpl
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import logcat.LogPriority
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class SourcesFilterPresenter( class SourcesFilterPresenter(
private val state: SourcesFilterStateImpl = SourcesFilterState() as SourcesFilterStateImpl,
private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(), private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(), private val toggleLanguage: ToggleLanguage = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(), private val preferences: PreferencesHelper = Injekt.get(),
) : BasePresenter<SourceFilterController>() { ) : BasePresenter<SourceFilterController>(), SourcesFilterState by state {
private val _state: MutableStateFlow<SourceFilterState> = MutableStateFlow(SourceFilterState.Loading) private val _events = Channel<Event>(Int.MAX_VALUE)
val state: StateFlow<SourceFilterState> = _state.asStateFlow() val events = _events.receiveAsFlow()
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@ -32,14 +36,15 @@ class SourcesFilterPresenter(
presenterScope.launchIO { presenterScope.launchIO {
getLanguagesWithSources.subscribe() getLanguagesWithSources.subscribe()
.catch { exception -> .catch { exception ->
_state.value = SourceFilterState.Error(exception) logcat(LogPriority.ERROR, exception)
_events.send(Event.FailedFetchingLanguages)
} }
.collectLatest(::collectLatestSourceLangMap) .collectLatest(::collectLatestSourceLangMap)
} }
} }
private fun collectLatestSourceLangMap(sourceLangMap: Map<String, List<Source>>) { private fun collectLatestSourceLangMap(sourceLangMap: Map<String, List<Source>>) {
val uiModels = sourceLangMap.flatMap { state.items = sourceLangMap.flatMap {
val isLangEnabled = it.key in preferences.enabledLanguages().get() val isLangEnabled = it.key in preferences.enabledLanguages().get()
val header = listOf(FilterUiModel.Header(it.key, isLangEnabled)) val header = listOf(FilterUiModel.Header(it.key, isLangEnabled))
@ -51,7 +56,7 @@ class SourcesFilterPresenter(
) )
} }
} }
_state.value = SourceFilterState.Success(uiModels) state.isLoading = false
} }
fun toggleSource(source: Source) { fun toggleSource(source: Source) {
@ -61,10 +66,8 @@ class SourcesFilterPresenter(
fun toggleLanguage(language: String) { fun toggleLanguage(language: String) {
toggleLanguage.await(language) toggleLanguage.await(language)
} }
}
sealed class SourceFilterState { sealed class Event {
object Loading : SourceFilterState() object FailedFetchingLanguages : Event()
data class Error(val error: Throwable) : SourceFilterState() }
data class Success(val models: List<FilterUiModel>) : SourceFilterState()
} }

View File

@ -7,32 +7,37 @@ import eu.kanade.domain.source.interactor.ToggleSourcePin
import eu.kanade.domain.source.model.Pin import eu.kanade.domain.source.model.Pin
import eu.kanade.domain.source.model.Source import eu.kanade.domain.source.model.Source
import eu.kanade.presentation.browse.SourceUiModel import eu.kanade.presentation.browse.SourceUiModel
import eu.kanade.presentation.browse.SourcesState
import eu.kanade.presentation.browse.SourcesStateImpl
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import logcat.LogPriority
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.TreeMap import java.util.TreeMap
class SourcesPresenter( class SourcesPresenter(
private val state: SourcesStateImpl = SourcesState() as SourcesStateImpl,
private val getEnabledSources: GetEnabledSources = Injekt.get(), private val getEnabledSources: GetEnabledSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(), private val toggleSource: ToggleSource = Injekt.get(),
private val toggleSourcePin: ToggleSourcePin = Injekt.get(), private val toggleSourcePin: ToggleSourcePin = Injekt.get(),
) : BasePresenter<SourcesController>() { ) : BasePresenter<SourcesController>(), SourcesState by state {
private val _state: MutableStateFlow<SourceState> = MutableStateFlow(SourceState.Loading) private val _events = Channel<Event>(Int.MAX_VALUE)
val state: StateFlow<SourceState> = _state.asStateFlow() val events = _events.receiveAsFlow()
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
presenterScope.launchIO { presenterScope.launchIO {
getEnabledSources.subscribe() getEnabledSources.subscribe()
.catch { exception -> .catch { exception ->
_state.value = SourceState.Error(exception) logcat(LogPriority.ERROR, exception)
_events.send(Event.FailedFetchingSources)
} }
.collectLatest(::collectLatestSources) .collectLatest(::collectLatestSources)
} }
@ -67,7 +72,8 @@ class SourcesPresenter(
}.toTypedArray(), }.toTypedArray(),
) )
} }
_state.value = SourceState.Success(uiModels) state.isLoading = false
state.items = uiModels
} }
fun toggleSource(source: Source) { fun toggleSource(source: Source) {
@ -78,14 +84,14 @@ class SourcesPresenter(
toggleSourcePin.await(source) toggleSourcePin.await(source)
} }
sealed class Event {
object FailedFetchingSources : Event()
}
data class Dialog(val source: Source)
companion object { companion object {
const val PINNED_KEY = "pinned" const val PINNED_KEY = "pinned"
const val LAST_USED_KEY = "last_used" const val LAST_USED_KEY = "last_used"
} }
} }
sealed class SourceState {
object Loading : SourceState()
data class Error(val error: Throwable) : SourceState()
data class Success(val uiModels: List<SourceUiModel>) : SourceState()
}