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
import android.content.Intent
import android.net.Uri
import android.provider.Settings
import android.util.DisplayMetrics
import androidx.annotation.StringRes
import androidx.compose.foundation.background
@ -32,7 +35,6 @@ import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
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
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.util.horizontalPadding
@ -66,65 +69,68 @@ fun ExtensionDetailsScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: ExtensionDetailsPresenter,
onClickUninstall: () -> Unit,
onClickAppInfo: () -> Unit,
onClickSourcePreferences: (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) {
EmptyScreen(textResource = R.string.empty_screen)
return
}
val sources by presenter.sourcesState.collectAsState()
var showNsfwWarning by remember { mutableStateOf(false) }
ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) {
when {
extension.isUnofficial ->
item {
WarningBanner(R.string.unofficial_extension_message)
ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) {
when {
extension.isUnofficial ->
item {
WarningBanner(R.string.unofficial_extension_message)
}
extension.isObsolete ->
item {
WarningBanner(R.string.obsolete_extension_message)
}
}
extension.isObsolete ->
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 {
DetailsHeader(
extension = extension,
onClickUninstall = onClickUninstall,
onClickAppInfo = onClickAppInfo,
onClickAgeRating = {
showNsfwWarning = true
},
)
items(
items = presenter.sources,
key = { it.source.id },
) { source ->
SourceSwitchPreference(
modifier = Modifier.animateItemPlacement(),
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.lazy.items
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
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.tachiyomi.R
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.toast
import kotlinx.coroutines.flow.collectLatest
@Composable
fun ExtensionFilterScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: ExtensionFilterPresenter,
onClickLang: (String) -> Unit,
) {
val state by presenter.state.collectAsState()
when (state) {
is ExtensionFilterState.Loading -> LoadingScreen()
is ExtensionFilterState.Error -> Text(text = (state as ExtensionFilterState.Error).error.message!!)
is ExtensionFilterState.Success ->
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.empty_screen)
else -> {
SourceFilterContent(
nestedScrollInterop = nestedScrollInterop,
items = (state as ExtensionFilterState.Success).models,
onClickLang = onClickLang,
state = presenter,
onClickLang = {
presenter.toggleLanguage(it)
},
)
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest {
when (it) {
ExtensionFilterPresenter.Event.FailedFetchingLanguages -> {
context.toast(R.string.internal_error)
}
}
}
}
}
@Composable
fun SourceFilterContent(
nestedScrollInterop: NestedScrollConnection,
items: List<FilterUiModel>,
state: ExtensionFilterState,
onClickLang: (String) -> Unit,
) {
if (items.isEmpty()) {
EmptyScreen(textResource = R.string.empty_screen)
return
}
LazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) {
items(
items = items,
items = state.items,
) { model ->
ExtensionFilterItem(
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.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
@ -40,7 +39,9 @@ import com.google.accompanist.swiperefresh.SwipeRefresh
import com.google.accompanist.swiperefresh.rememberSwipeRefreshState
import eu.kanade.presentation.browse.components.BaseBrowseItem
import eu.kanade.presentation.browse.components.ExtensionIcon
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.FastScrollLazyColumn
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.SwipeRefreshIndicator
import eu.kanade.presentation.theme.header
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.extension.model.Extension
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.ExtensionsPresenter
import eu.kanade.tachiyomi.util.system.LocaleHelper
@ -69,19 +69,18 @@ fun ExtensionScreen(
onRefresh: () -> Unit,
onLaunched: () -> Unit,
) {
val state by presenter.state.collectAsState()
val isRefreshing = presenter.isRefreshing
SwipeRefresh(
modifier = Modifier.nestedScroll(nestedScrollInterop),
state = rememberSwipeRefreshState(isRefreshing),
state = rememberSwipeRefreshState(presenter.isRefreshing),
indicator = { s, trigger -> SwipeRefreshIndicator(s, trigger) },
onRefresh = onRefresh,
) {
when (state) {
is ExtensionState.Initialized -> {
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(R.string.empty_screen)
else -> {
ExtensionContent(
items = (state as ExtensionState.Initialized).list,
state = presenter,
onLongClickItem = onLongClickItem,
onClickItemCancel = onClickItemCancel,
onInstallExtension = onInstallExtension,
@ -93,14 +92,13 @@ fun ExtensionScreen(
onLaunched = onLaunched,
)
}
ExtensionState.Uninitialized -> {}
}
}
}
@Composable
fun ExtensionContent(
items: List<ExtensionUiModel>,
state: ExtensionsState,
onLongClickItem: (Extension) -> Unit,
onClickItemCancel: (Extension) -> Unit,
onInstallExtension: (Extension.Available) -> Unit,
@ -117,7 +115,7 @@ fun ExtensionContent(
contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) {
items(
items = items,
items = state.items,
key = {
when (it) {
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.navigationBars
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import eu.kanade.domain.manga.model.Manga
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.LoadingScreen
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.presentation.manga.components.BaseMangaListItem
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaPresenter
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter.Event
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable
fun MigrateMangaScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: MigrationMangaPresenter,
presenter: MigrateMangaPresenter,
onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit,
) {
val state by presenter.state.collectAsState()
when (state) {
MigrateMangaState.Loading -> LoadingScreen()
is MigrateMangaState.Error -> Text(text = (state as MigrateMangaState.Error).error.message!!)
is MigrateMangaState.Success -> {
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.empty_screen)
else -> {
MigrateMangaContent(
nestedScrollInterop = nestedScrollInterop,
list = (state as MigrateMangaState.Success).list,
state = presenter,
onClickItem = onClickItem,
onClickCover = onClickCover,
)
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
Event.FailedFetchingFavorites -> {
context.toast(R.string.internal_error)
}
}
}
}
}
@Composable
fun MigrateMangaContent(
nestedScrollInterop: NestedScrollConnection,
list: List<Manga>,
state: MigrateMangaState,
onClickItem: (Manga) -> Unit,
onClickCover: (Manga) -> Unit,
) {
if (list.isEmpty()) {
EmptyScreen(textResource = R.string.empty_screen)
return
}
ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) {
items(list) { manga ->
items(state.items) { manga ->
MigrateMangaItem(
manga = manga,
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.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextOverflow
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.topPaddingValues
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.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.copyToClipboard
@Composable
fun MigrateSourceScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: MigrationSourcesPresenter,
onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit,
) {
val state by presenter.state.collectAsState()
when (state) {
is MigrateSourceState.Loading -> LoadingScreen()
is MigrateSourceState.Error -> Text(text = (state as MigrateSourceState.Error).error.message!!)
is MigrateSourceState.Success ->
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.information_empty_library)
else ->
MigrateSourceList(
nestedScrollInterop = nestedScrollInterop,
list = (state as MigrateSourceState.Success).sources,
list = presenter.items,
onClickItem = onClickItem,
onLongClickItem = onLongClickItem,
onLongClickItem = { source ->
val sourceId = source.id.toString()
context.copyToClipboard(sourceId, sourceId)
},
)
}
}
@ -64,11 +66,6 @@ fun MigrateSourceList(
onClickItem: (Source) -> Unit,
onLongClickItem: (Source) -> Unit,
) {
if (list.isEmpty()) {
EmptyScreen(textResource = R.string.information_empty_library)
return
}
ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
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.material3.Checkbox
import androidx.compose.material3.Switch
import androidx.compose.material3.Text
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.input.nestedscroll.NestedScrollConnection
@ -22,9 +21,10 @@ import eu.kanade.presentation.components.PreferenceRow
import eu.kanade.presentation.components.ScrollbarLazyColumn
import eu.kanade.tachiyomi.R
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.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable
fun SourcesFilterScreen(
@ -33,39 +33,43 @@ fun SourcesFilterScreen(
onClickLang: (String) -> Unit,
onClickSource: (Source) -> Unit,
) {
val state by presenter.state.collectAsState()
when (state) {
is SourceFilterState.Loading -> LoadingScreen()
is SourceFilterState.Error -> Text(text = (state as SourceFilterState.Error).error.message!!)
is SourceFilterState.Success ->
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(textResource = R.string.source_filter_empty_screen)
else -> {
SourcesFilterContent(
nestedScrollInterop = nestedScrollInterop,
items = (state as SourceFilterState.Success).models,
state = presenter,
onClickLang = onClickLang,
onClickSource = onClickSource,
)
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
SourcesFilterPresenter.Event.FailedFetchingLanguages -> {
context.toast(R.string.internal_error)
}
}
}
}
}
@Composable
fun SourcesFilterContent(
nestedScrollInterop: NestedScrollConnection,
items: List<FilterUiModel>,
state: SourcesFilterState,
onClickLang: (String) -> Unit,
onClickSource: (Source) -> Unit,
) {
if (items.isEmpty()) {
EmptyScreen(textResource = R.string.source_filter_empty_screen)
return
}
ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollInterop),
contentPadding = WindowInsets.navigationBars.asPaddingValues(),
) {
items(
items = items,
items = state.items,
contentType = {
when (it) {
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.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
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.tachiyomi.R
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.Dialog
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.collectLatest
@Composable
fun SourcesScreen(
@ -55,44 +55,47 @@ fun SourcesScreen(
onClickLatest: (Source) -> Unit,
onClickPin: (Source) -> Unit,
) {
val state by presenter.state.collectAsState()
when (state) {
is SourceState.Loading -> LoadingScreen()
is SourceState.Error -> Text(text = (state as SourceState.Error).error.message!!)
is SourceState.Success -> SourceList(
nestedScrollConnection = nestedScrollInterop,
list = (state as SourceState.Success).uiModels,
onClickItem = onClickItem,
onClickDisable = onClickDisable,
onClickLatest = onClickLatest,
onClickPin = onClickPin,
)
val context = LocalContext.current
when {
presenter.isLoading -> LoadingScreen()
presenter.isEmpty -> EmptyScreen(R.string.source_empty_screen)
else -> {
SourceList(
nestedScrollConnection = nestedScrollInterop,
state = presenter,
onClickItem = onClickItem,
onClickDisable = onClickDisable,
onClickLatest = onClickLatest,
onClickPin = onClickPin,
)
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
SourcesPresenter.Event.FailedFetchingSources -> {
context.toast(R.string.internal_error)
}
}
}
}
}
@Composable
fun SourceList(
nestedScrollConnection: NestedScrollConnection,
list: List<SourceUiModel>,
state: SourcesState,
onClickItem: (Source) -> Unit,
onClickDisable: (Source) -> Unit,
onClickLatest: (Source) -> Unit,
onClickPin: (Source) -> Unit,
) {
if (list.isEmpty()) {
EmptyScreen(textResource = R.string.source_empty_screen)
return
}
var sourceState by remember { mutableStateOf<Source?>(null) }
ScrollbarLazyColumn(
modifier = Modifier.nestedScroll(nestedScrollConnection),
contentPadding = WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
) {
items(
items = list,
items = state.items,
contentType = {
when (it) {
is SourceUiModel.Header -> "header"
@ -117,7 +120,7 @@ fun SourceList(
modifier = Modifier.animateItemPlacement(),
source = model.source,
onClickItem = onClickItem,
onLongClickItem = { sourceState = it },
onLongClickItem = { state.dialog = Dialog(it) },
onClickLatest = onClickLatest,
onClickPin = onClickPin,
)
@ -125,18 +128,19 @@ fun SourceList(
}
}
if (sourceState != null) {
if (state.dialog != null) {
val source = state.dialog!!.source
SourceOptionsDialog(
source = sourceState!!,
source = source,
onClickPin = {
onClickPin(sourceState!!)
sourceState = null
onClickPin(source)
state.dialog = null
},
onClickDisable = {
onClickDisable(sourceState!!)
sourceState = null
onClickDisable(source)
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.toast
import kotlinx.coroutines.async
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import logcat.LogPriority
import rx.Observable
import uy.kohesive.injekt.Injekt
@ -63,9 +66,16 @@ class ExtensionManager(
var installedExtensions = emptyList<Extension.Installed>()
private set(value) {
field = value
installedExtensionsFlow.value = field
installedExtensionsRelay.call(value)
}
private val installedExtensionsFlow = MutableStateFlow(installedExtensions)
fun getInstalledExtensionsFlow(): StateFlow<List<Extension.Installed>> {
return installedExtensionsFlow.asStateFlow()
}
fun getAppIconForSource(source: Source): Drawable? {
return getAppIconForSource(source.id)
}

View File

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

View File

@ -3,11 +3,11 @@ package eu.kanade.tachiyomi.ui.browse.extension
import android.app.Application
import android.os.Bundle
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.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.extension.ExtensionManager
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.system.LocaleHelper
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.update
@ -27,20 +25,16 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class ExtensionsPresenter(
private val state: ExtensionsStateImpl = ExtensionState() as ExtensionsStateImpl,
private val extensionManager: ExtensionManager = Injekt.get(),
private val getExtensionUpdates: GetExtensionUpdates = Injekt.get(),
private val getExtensions: GetExtensions = Injekt.get(),
) : BasePresenter<ExtensionsController>() {
) : BasePresenter<ExtensionsController>(), ExtensionsState by state {
private val _query: MutableStateFlow<String> = MutableStateFlow("")
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?) {
super.onCreate(savedState)
@ -86,8 +80,6 @@ class ExtensionsPresenter(
getExtensionUpdates.subscribe(),
_currentDownloads,
) { query, (installed, untrusted, available), updates, downloads ->
isRefreshing = false
val languagesWithExtensions = available
.filter(queryFilter(query))
.groupBy { LocaleHelper.getSourceDisplayName(it.lang, context) }
@ -121,7 +113,9 @@ class ExtensionsPresenter(
items
}.collectLatest {
_state.value = ExtensionState.Initialized(it)
state.isRefreshing = false
state.isLoading = false
state.items = it
}
}
}
@ -134,9 +128,9 @@ class ExtensionsPresenter(
fun updateAllExtensions() {
launchIO {
val state = _state.value
if (state !is ExtensionState.Initialized) return@launchIO
state.list.mapNotNull {
if (state.isEmpty) return@launchIO
val items = state.items
items.mapNotNull {
if (it !is ExtensionUiModel.Item) return@mapNotNull null
if (it.extension !is Extension.Installed) return@mapNotNull null
if (it.extension.hasUpdate.not()) return@mapNotNull null
@ -189,7 +183,7 @@ class ExtensionsPresenter(
}
fun findAvailableExtensions() {
isRefreshing = true
state.isRefreshing = true
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,
presenter = presenter,
onClickUninstall = { presenter.uninstallExtension() },
onClickAppInfo = { presenter.openInSettings() },
onClickSourcePreferences = { router.pushController(SourcePreferencesController(it)) },
onClickSource = { presenter.toggleSource(it) },
)

View File

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

View File

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

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.manga.MangaController
class MigrationMangaController : ComposeController<MigrationMangaPresenter> {
class MigrationMangaController : ComposeController<MigrateMangaPresenter> {
constructor(sourceId: Long, sourceName: String?) : super(
bundleOf(
@ -30,7 +30,7 @@ class MigrationMangaController : ComposeController<MigrationMangaPresenter> {
override fun getTitle(): String? = sourceName
override fun createPresenter(): MigrationMangaPresenter = MigrationMangaPresenter(sourceId)
override fun createPresenter(): MigrateMangaPresenter = MigrateMangaPresenter(sourceId)
@Composable
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.pushController
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.openInBrowser
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 eu.kanade.domain.source.interactor.GetSourcesWithFavoriteCount
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.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import logcat.LogPriority
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrationSourcesPresenter(
private val state: MigrateSourceStateImpl = MigrateSourceState() as MigrateSourceStateImpl,
private val getSourcesWithFavoriteCount: GetSourcesWithFavoriteCount = Injekt.get(),
private val setMigrateSorting: SetMigrateSorting = Injekt.get(),
) : BasePresenter<MigrationSourcesController>() {
) : BasePresenter<MigrationSourcesController>(), MigrateSourceState by state {
private val _state: MutableStateFlow<MigrateSourceState> = MutableStateFlow(MigrateSourceState.Loading)
val state: StateFlow<MigrateSourceState> = _state.asStateFlow()
private val _channel = Channel<Event>(Int.MAX_VALUE)
val channel = _channel.receiveAsFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
@ -28,10 +31,12 @@ class MigrationSourcesPresenter(
presenterScope.launchIO {
getSourcesWithFavoriteCount.subscribe()
.catch { exception ->
_state.value = MigrateSourceState.Error(exception)
logcat(LogPriority.ERROR, exception)
_channel.send(Event.FailedFetchingSourcesWithCount)
}
.collectLatest { sources ->
_state.value = MigrateSourceState.Success(sources)
state.items = sources
state.isLoading = false
}
}
}
@ -43,10 +48,8 @@ class MigrationSourcesPresenter(
fun setTotalSorting(isAscending: Boolean) {
setMigrateSorting.await(SetMigrateSorting.Mode.TOTAL, isAscending)
}
}
sealed class MigrateSourceState {
object Loading : MigrateSourceState()
data class Error(val error: Throwable) : MigrateSourceState()
data class Success(val sources: List<Pair<Source, Long>>) : MigrateSourceState()
sealed class Event {
object FailedFetchingSourcesWithCount : Event()
}
}

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.ToggleSource
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.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.receiveAsFlow
import logcat.LogPriority
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SourcesFilterPresenter(
private val state: SourcesFilterStateImpl = SourcesFilterState() as SourcesFilterStateImpl,
private val getLanguagesWithSources: GetLanguagesWithSources = Injekt.get(),
private val toggleSource: ToggleSource = Injekt.get(),
private val toggleLanguage: ToggleLanguage = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get(),
) : BasePresenter<SourceFilterController>() {
) : BasePresenter<SourceFilterController>(), SourcesFilterState by state {
private val _state: MutableStateFlow<SourceFilterState> = MutableStateFlow(SourceFilterState.Loading)
val state: StateFlow<SourceFilterState> = _state.asStateFlow()
private val _events = Channel<Event>(Int.MAX_VALUE)
val events = _events.receiveAsFlow()
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
@ -32,14 +36,15 @@ class SourcesFilterPresenter(
presenterScope.launchIO {
getLanguagesWithSources.subscribe()
.catch { exception ->
_state.value = SourceFilterState.Error(exception)
logcat(LogPriority.ERROR, exception)
_events.send(Event.FailedFetchingLanguages)
}
.collectLatest(::collectLatestSourceLangMap)
}
}
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 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) {
@ -61,10 +66,8 @@ class SourcesFilterPresenter(
fun toggleLanguage(language: String) {
toggleLanguage.await(language)
}
}
sealed class SourceFilterState {
object Loading : SourceFilterState()
data class Error(val error: Throwable) : SourceFilterState()
data class Success(val models: List<FilterUiModel>) : SourceFilterState()
sealed class Event {
object FailedFetchingLanguages : Event()
}
}

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