Use Voyager on Migrate Manga screen (#8611)
This commit is contained in:
parent
f4ac754d02
commit
a8c5780963
7 changed files with 160 additions and 165 deletions
|
@ -4,31 +4,24 @@ import androidx.compose.foundation.layout.PaddingValues
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.items
|
import androidx.compose.foundation.lazy.items
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import androidx.compose.runtime.LaunchedEffect
|
|
||||||
import androidx.compose.ui.Modifier
|
import androidx.compose.ui.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import eu.kanade.domain.manga.model.Manga
|
import eu.kanade.domain.manga.model.Manga
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.FastScrollLazyColumn
|
import eu.kanade.presentation.components.FastScrollLazyColumn
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
|
||||||
import eu.kanade.presentation.components.Scaffold
|
import eu.kanade.presentation.components.Scaffold
|
||||||
import eu.kanade.presentation.manga.components.BaseMangaListItem
|
import eu.kanade.presentation.manga.components.BaseMangaListItem
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter
|
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaState
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrateMangaPresenter.Event
|
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun MigrateMangaScreen(
|
fun MigrateMangaScreen(
|
||||||
navigateUp: () -> Unit,
|
navigateUp: () -> Unit,
|
||||||
title: String?,
|
title: String?,
|
||||||
presenter: MigrateMangaPresenter,
|
state: MigrateMangaState,
|
||||||
onClickItem: (Manga) -> Unit,
|
onClickItem: (Manga) -> Unit,
|
||||||
onClickCover: (Manga) -> Unit,
|
onClickCover: (Manga) -> Unit,
|
||||||
) {
|
) {
|
||||||
val context = LocalContext.current
|
|
||||||
Scaffold(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
AppBar(
|
AppBar(
|
||||||
|
@ -38,31 +31,21 @@ fun MigrateMangaScreen(
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
when {
|
if (state.isEmpty) {
|
||||||
presenter.isLoading -> LoadingScreen()
|
EmptyScreen(
|
||||||
presenter.isEmpty -> EmptyScreen(
|
|
||||||
textResource = R.string.empty_screen,
|
textResource = R.string.empty_screen,
|
||||||
modifier = Modifier.padding(contentPadding),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
else -> {
|
return@Scaffold
|
||||||
|
}
|
||||||
|
|
||||||
MigrateMangaContent(
|
MigrateMangaContent(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
state = presenter,
|
state = state,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
onClickCover = onClickCover,
|
onClickCover = onClickCover,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
|
||||||
}
|
|
||||||
LaunchedEffect(Unit) {
|
|
||||||
presenter.events.collectLatest { event ->
|
|
||||||
when (event) {
|
|
||||||
Event.FailedFetchingFavorites -> {
|
|
||||||
context.toast(R.string.internal_error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -75,7 +58,7 @@ private fun MigrateMangaContent(
|
||||||
FastScrollLazyColumn(
|
FastScrollLazyColumn(
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
) {
|
) {
|
||||||
items(state.items) { manga ->
|
items(state.titles) { manga ->
|
||||||
MigrateMangaItem(
|
MigrateMangaItem(
|
||||||
manga = manga,
|
manga = manga,
|
||||||
onClickItem = onClickItem,
|
onClickItem = onClickItem,
|
||||||
|
|
|
@ -1,23 +0,0 @@
|
||||||
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() }
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import eu.kanade.domain.manga.interactor.GetFavorites
|
|
||||||
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 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 MigrateMangaPresenter(
|
|
||||||
private val sourceId: Long,
|
|
||||||
private val state: MigrateMangaStateImpl = MigrationMangaState() as MigrateMangaStateImpl,
|
|
||||||
private val getFavorites: GetFavorites = Injekt.get(),
|
|
||||||
) : BasePresenter<MigrationMangaController>(), MigrateMangaState by state {
|
|
||||||
|
|
||||||
private val _events = Channel<Event>(Int.MAX_VALUE)
|
|
||||||
val events = _events.receiveAsFlow()
|
|
||||||
|
|
||||||
override fun onCreate(savedState: Bundle?) {
|
|
||||||
super.onCreate(savedState)
|
|
||||||
presenterScope.launchIO {
|
|
||||||
getFavorites
|
|
||||||
.subscribe(sourceId)
|
|
||||||
.catch {
|
|
||||||
logcat(LogPriority.ERROR, it)
|
|
||||||
_events.send(Event.FailedFetchingFavorites)
|
|
||||||
}
|
|
||||||
.map { list ->
|
|
||||||
list.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title })
|
|
||||||
}
|
|
||||||
.collectLatest { sortedList ->
|
|
||||||
state.isLoading = false
|
|
||||||
state.items = sortedList
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
sealed class Event {
|
|
||||||
object FailedFetchingFavorites : Event()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,51 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
|
||||||
|
|
||||||
import android.os.Bundle
|
|
||||||
import androidx.compose.runtime.Composable
|
|
||||||
import androidx.core.os.bundleOf
|
|
||||||
import eu.kanade.presentation.browse.MigrateMangaScreen
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
|
||||||
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 : FullComposeController<MigrateMangaPresenter> {
|
|
||||||
|
|
||||||
constructor(sourceId: Long, sourceName: String?) : super(
|
|
||||||
bundleOf(
|
|
||||||
SOURCE_ID_EXTRA to sourceId,
|
|
||||||
SOURCE_NAME_EXTRA to sourceName,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
@Suppress("unused")
|
|
||||||
constructor(bundle: Bundle) : this(
|
|
||||||
bundle.getLong(SOURCE_ID_EXTRA),
|
|
||||||
bundle.getString(SOURCE_NAME_EXTRA),
|
|
||||||
)
|
|
||||||
|
|
||||||
private val sourceId: Long = args.getLong(SOURCE_ID_EXTRA)
|
|
||||||
private val sourceName: String? = args.getString(SOURCE_NAME_EXTRA)
|
|
||||||
|
|
||||||
override fun createPresenter() = MigrateMangaPresenter(sourceId)
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
override fun ComposeContent() {
|
|
||||||
MigrateMangaScreen(
|
|
||||||
navigateUp = router::popCurrentController,
|
|
||||||
title = sourceName,
|
|
||||||
presenter = presenter,
|
|
||||||
onClickItem = {
|
|
||||||
router.pushController(SearchController(it.id))
|
|
||||||
},
|
|
||||||
onClickCover = {
|
|
||||||
router.pushController(MangaController(it.id))
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
companion object {
|
|
||||||
const val SOURCE_ID_EXTRA = "source_id_extra"
|
|
||||||
const val SOURCE_NAME_EXTRA = "source_name_extra"
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,66 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.collectAsState
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
|
import eu.kanade.presentation.browse.MigrateMangaScreen
|
||||||
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
|
import eu.kanade.presentation.util.LocalRouter
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
||||||
|
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaScreen
|
||||||
|
import eu.kanade.tachiyomi.util.system.toast
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
|
||||||
|
data class MigrationMangaScreen(
|
||||||
|
private val sourceId: Long,
|
||||||
|
) : Screen {
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
override fun Content() {
|
||||||
|
val context = LocalContext.current
|
||||||
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
|
val router = LocalRouter.currentOrThrow
|
||||||
|
val screenModel = rememberScreenModel { MigrationMangaScreenModel(sourceId) }
|
||||||
|
|
||||||
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
|
if (state.isLoading) {
|
||||||
|
LoadingScreen()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
MigrateMangaScreen(
|
||||||
|
navigateUp = navigator::pop,
|
||||||
|
title = state.source!!.name,
|
||||||
|
state = state,
|
||||||
|
onClickItem = {
|
||||||
|
router.pushController(SearchController(it.id))
|
||||||
|
},
|
||||||
|
onClickCover = {
|
||||||
|
navigator.push(MangaScreen(it.id))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
LaunchedEffect(Unit) {
|
||||||
|
screenModel.events.collectLatest { event ->
|
||||||
|
when (event) {
|
||||||
|
MigrationMangaEvent.FailedFetchingFavorites -> {
|
||||||
|
context.toast(R.string.internal_error)
|
||||||
|
}
|
||||||
|
MigrationMangaEvent.FailedGettingSource -> {
|
||||||
|
context.toast(R.string.loader_not_implemented_error)
|
||||||
|
router.popCurrentController()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,77 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.browse.migration.manga
|
||||||
|
|
||||||
|
import androidx.compose.runtime.Immutable
|
||||||
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
|
import eu.kanade.domain.manga.interactor.GetFavorites
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
import eu.kanade.tachiyomi.source.Source
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
import kotlinx.coroutines.channels.Channel
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.flow.catch
|
||||||
|
import kotlinx.coroutines.flow.collectLatest
|
||||||
|
import kotlinx.coroutines.flow.map
|
||||||
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
|
import logcat.LogPriority
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
|
class MigrationMangaScreenModel(
|
||||||
|
private val sourceId: Long,
|
||||||
|
private val sourceManager: SourceManager = Injekt.get(),
|
||||||
|
private val getFavorites: GetFavorites = Injekt.get(),
|
||||||
|
) : StateScreenModel<MigrateMangaState>(MigrateMangaState()) {
|
||||||
|
|
||||||
|
private val _events: Channel<MigrationMangaEvent> = Channel()
|
||||||
|
val events: Flow<MigrationMangaEvent> = _events.receiveAsFlow()
|
||||||
|
|
||||||
|
init {
|
||||||
|
coroutineScope.launch {
|
||||||
|
mutableState.update { state ->
|
||||||
|
val source = sourceManager.get(sourceId)
|
||||||
|
if (source == null) {
|
||||||
|
_events.send(MigrationMangaEvent.FailedGettingSource)
|
||||||
|
}
|
||||||
|
state.copy(source = source)
|
||||||
|
}
|
||||||
|
|
||||||
|
getFavorites.subscribe(sourceId)
|
||||||
|
.catch {
|
||||||
|
logcat(LogPriority.ERROR, it)
|
||||||
|
_events.send(MigrationMangaEvent.FailedFetchingFavorites)
|
||||||
|
mutableState.update { it.copy(titleList = emptyList()) }
|
||||||
|
}
|
||||||
|
.map {
|
||||||
|
it.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.title })
|
||||||
|
}
|
||||||
|
.collectLatest { list ->
|
||||||
|
mutableState.update { it.copy(titleList = list) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class MigrationMangaEvent {
|
||||||
|
object FailedGettingSource : MigrationMangaEvent()
|
||||||
|
object FailedFetchingFavorites : MigrationMangaEvent()
|
||||||
|
}
|
||||||
|
|
||||||
|
@Immutable
|
||||||
|
data class MigrateMangaState(
|
||||||
|
val source: Source? = null,
|
||||||
|
private val titleList: List<Manga>? = null,
|
||||||
|
) {
|
||||||
|
|
||||||
|
val titles: List<Manga>
|
||||||
|
get() = titleList ?: emptyList()
|
||||||
|
|
||||||
|
val isLoading: Boolean
|
||||||
|
get() = source == null || titleList == null
|
||||||
|
|
||||||
|
val isEmpty: Boolean
|
||||||
|
get() = titles.isEmpty()
|
||||||
|
}
|
|
@ -9,19 +9,18 @@ import androidx.compose.ui.platform.LocalUriHandler
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import cafe.adriel.voyager.core.model.rememberScreenModel
|
import cafe.adriel.voyager.core.model.rememberScreenModel
|
||||||
import cafe.adriel.voyager.core.screen.Screen
|
import cafe.adriel.voyager.core.screen.Screen
|
||||||
|
import cafe.adriel.voyager.navigator.LocalNavigator
|
||||||
import cafe.adriel.voyager.navigator.currentOrThrow
|
import cafe.adriel.voyager.navigator.currentOrThrow
|
||||||
import eu.kanade.presentation.browse.MigrateSourceScreen
|
import eu.kanade.presentation.browse.MigrateSourceScreen
|
||||||
import eu.kanade.presentation.components.AppBar
|
import eu.kanade.presentation.components.AppBar
|
||||||
import eu.kanade.presentation.components.TabContent
|
import eu.kanade.presentation.components.TabContent
|
||||||
import eu.kanade.presentation.util.LocalRouter
|
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaScreen
|
||||||
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun Screen.migrateSourceTab(): TabContent {
|
fun Screen.migrateSourceTab(): TabContent {
|
||||||
val uriHandler = LocalUriHandler.current
|
val uriHandler = LocalUriHandler.current
|
||||||
val router = LocalRouter.currentOrThrow
|
val navigator = LocalNavigator.currentOrThrow
|
||||||
val screenModel = rememberScreenModel { MigrateSourceScreenModel() }
|
val screenModel = rememberScreenModel { MigrateSourceScreenModel() }
|
||||||
val state by screenModel.state.collectAsState()
|
val state by screenModel.state.collectAsState()
|
||||||
|
|
||||||
|
@ -41,12 +40,7 @@ fun Screen.migrateSourceTab(): TabContent {
|
||||||
state = state,
|
state = state,
|
||||||
contentPadding = contentPadding,
|
contentPadding = contentPadding,
|
||||||
onClickItem = { source ->
|
onClickItem = { source ->
|
||||||
router.pushController(
|
navigator.push(MigrationMangaScreen(source.id))
|
||||||
MigrationMangaController(
|
|
||||||
source.id,
|
|
||||||
source.name,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
onToggleSortingDirection = screenModel::toggleSortingDirection,
|
onToggleSortingDirection = screenModel::toggleSortingDirection,
|
||||||
onToggleSortingMode = screenModel::toggleSortingMode,
|
onToggleSortingMode = screenModel::toggleSortingMode,
|
||||||
|
|
Reference in a new issue