Use Voyager on Migrate Manga screen (#8611)

This commit is contained in:
Andreas 2022-11-25 04:25:36 +01:00 committed by GitHub
parent f4ac754d02
commit a8c5780963
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 160 additions and 165 deletions

View File

@ -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,30 +31,20 @@ 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(
contentPadding = contentPadding,
state = presenter,
onClickItem = onClickItem,
onClickCover = onClickCover,
)
}
}
}
LaunchedEffect(Unit) {
presenter.events.collectLatest { event ->
when (event) {
Event.FailedFetchingFavorites -> {
context.toast(R.string.internal_error)
}
}
} }
MigrateMangaContent(
contentPadding = contentPadding,
state = state,
onClickItem = onClickItem,
onClickCover = onClickCover,
)
} }
} }
@ -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,

View File

@ -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() }
}

View File

@ -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()
}
}

View File

@ -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"
}
}

View File

@ -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()
}
}
}
}
}
}

View File

@ -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()
}

View File

@ -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,