mirror of
https://git.mihon.tech/mihonapp/mihon
synced 2024-11-23 21:55:57 +03:00
Use Stable interface for Browse screens (#7544)
This commit is contained in:
parent
383f7089c4
commit
018ca71336
26 changed files with 505 additions and 307 deletions
|
@ -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
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -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())
|
||||
}
|
|
@ -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(),
|
|
@ -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() }
|
||||
}
|
|
@ -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
|
||||
|
|
|
@ -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() }
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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() }
|
||||
}
|
|
@ -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,
|
||||
|
|
|
@ -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() }
|
||||
}
|
|
@ -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"
|
||||
|
|
|
@ -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() }
|
||||
}
|
|
@ -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 },
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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() }
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -17,9 +17,6 @@ class ExtensionFilterController : ComposeController<ExtensionFilterPresenter>()
|
|||
ExtensionFilterScreen(
|
||||
nestedScrollInterop = nestedScrollInterop,
|
||||
presenter = presenter,
|
||||
onClickLang = { language ->
|
||||
presenter.toggleLanguage(language)
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
|
@ -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) },
|
||||
)
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue