Voyager on History tab (#8481)

This commit is contained in:
Ivan Iskandar
2022-11-09 21:26:29 +07:00
committed by GitHub
parent ba00d9e5d2
commit bc3bb82651
9 changed files with 233 additions and 186 deletions

View File

@@ -5,6 +5,8 @@ import android.view.LayoutInflater
import android.view.View
import androidx.activity.OnBackPressedDispatcherOwner
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
import eu.kanade.tachiyomi.util.view.setComposeContent
import nucleus.presenter.Presenter
@@ -21,7 +23,9 @@ abstract class FullComposeController<P : Presenter<*>>(bundle: Bundle? = null) :
binding.root.apply {
setComposeContent {
ComposeContent()
CompositionLocalProvider(LocalRouter provides router) {
ComposeContent()
}
}
}
}
@@ -52,7 +56,9 @@ abstract class BasicFullComposeController(bundle: Bundle? = null) :
binding.root.apply {
setComposeContent {
ComposeContent()
CompositionLocalProvider(LocalRouter provides router) {
ComposeContent()
}
}
}
}

View File

@@ -1,30 +1,26 @@
package eu.kanade.tachiyomi.ui.history
import androidx.compose.runtime.Composable
import eu.kanade.presentation.history.HistoryScreen
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import cafe.adriel.voyager.navigator.Navigator
import eu.kanade.domain.history.interactor.GetNextChapters
import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.launchIO
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class HistoryController : FullComposeController<HistoryPresenter>(), RootController {
override fun createPresenter() = HistoryPresenter()
class HistoryController : BasicFullComposeController(), RootController {
@Composable
override fun ComposeContent() {
HistoryScreen(
presenter = presenter,
onClickCover = { history ->
router.pushController(MangaController(history.mangaId))
},
onClickResume = { history ->
presenter.getNextChapterForManga(history.mangaId, history.chapterId)
},
)
Navigator(screen = HistoryScreen)
}
fun resumeLastChapterRead() {
presenter.resumeLastChapterRead()
val context = activity ?: return
viewScope.launchIO {
val chapter = Injekt.get<GetNextChapters>().await(onlyUnread = false).firstOrNull()
HistoryScreen.openChapter(context, chapter)
}
}
}

View File

@@ -0,0 +1,97 @@
package eu.kanade.tachiyomi.ui.history
import android.content.Context
import androidx.compose.material3.SnackbarHostState
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.currentOrThrow
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.presentation.history.HistoryScreen
import eu.kanade.presentation.history.components.HistoryDeleteAllDialog
import eu.kanade.presentation.history.components.HistoryDeleteDialog
import eu.kanade.presentation.util.LocalRouter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import kotlinx.coroutines.flow.collectLatest
object HistoryScreen : Screen {
private val snackbarHostState = SnackbarHostState()
@Composable
override fun Content() {
val router = LocalRouter.currentOrThrow
val context = LocalContext.current
val screenModel = rememberScreenModel { HistoryScreenModel() }
val state by screenModel.state.collectAsState()
HistoryScreen(
state = state,
snackbarHostState = snackbarHostState,
incognitoMode = screenModel.isIncognitoMode,
downloadedOnlyMode = screenModel.isDownloadOnly,
onSearchQueryChange = screenModel::updateSearchQuery,
onClickCover = { router.pushController(MangaController(it)) },
onClickResume = screenModel::getNextChapterForManga,
onDialogChange = screenModel::setDialog,
)
val onDismissRequest = { screenModel.setDialog(null) }
when (val dialog = state.dialog) {
is HistoryScreenModel.Dialog.Delete -> {
HistoryDeleteDialog(
onDismissRequest = onDismissRequest,
onDelete = { all ->
if (all) {
screenModel.removeAllFromHistory(dialog.history.mangaId)
} else {
screenModel.removeFromHistory(dialog.history)
}
},
)
}
is HistoryScreenModel.Dialog.DeleteAll -> {
HistoryDeleteAllDialog(
onDismissRequest = onDismissRequest,
onDelete = screenModel::removeAllHistory,
)
}
null -> {}
}
LaunchedEffect(state.list) {
if (state.list != null) {
(context as? MainActivity)?.ready = true
}
}
LaunchedEffect(Unit) {
screenModel.events.collectLatest { e ->
when (e) {
HistoryScreenModel.Event.InternalError ->
snackbarHostState.showSnackbar(context.getString(R.string.internal_error))
HistoryScreenModel.Event.HistoryCleared ->
snackbarHostState.showSnackbar(context.getString(R.string.clear_history_completed))
is HistoryScreenModel.Event.OpenChapter -> openChapter(context, e.chapter)
}
}
}
}
suspend fun openChapter(context: Context, chapter: Chapter?) {
if (chapter != null) {
val intent = ReaderActivity.newIntent(context, chapter.mangaId, chapter.id)
context.startActivity(intent)
} else {
snackbarHostState.showSnackbar(context.getString(R.string.no_next_chapter))
}
}
}

View File

@@ -1,11 +1,10 @@
package eu.kanade.tachiyomi.ui.history
import androidx.compose.runtime.Composable
import androidx.compose.runtime.Stable
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import cafe.adriel.voyager.core.model.StateScreenModel
import cafe.adriel.voyager.core.model.coroutineScope
import eu.kanade.core.prefs.asState
import eu.kanade.core.util.insertSeparators
import eu.kanade.domain.base.BasePreferences
import eu.kanade.domain.chapter.model.Chapter
@@ -14,51 +13,53 @@ import eu.kanade.domain.history.interactor.GetNextChapters
import eu.kanade.domain.history.interactor.RemoveHistory
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.presentation.history.HistoryUiModel
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.toDateKey
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.catch
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOn
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
import java.util.Date
class HistoryPresenter(
private val state: HistoryStateImpl = HistoryState() as HistoryStateImpl,
class HistoryScreenModel(
private val getHistory: GetHistory = Injekt.get(),
private val getNextChapters: GetNextChapters = Injekt.get(),
private val removeHistory: RemoveHistory = Injekt.get(),
preferences: BasePreferences = Injekt.get(),
) : BasePresenter<HistoryController>(), HistoryState by state {
) : StateScreenModel<HistoryState>(HistoryState()) {
private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
private val _events: Channel<Event> = Channel(Channel.UNLIMITED)
val events: Flow<Event> = _events.receiveAsFlow()
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope)
val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope)
@Composable
fun getHistory(): Flow<List<HistoryUiModel>> {
val query = searchQuery ?: ""
return remember(query) {
getHistory.subscribe(query)
init {
coroutineScope.launch {
state.map { it.searchQuery }
.distinctUntilChanged()
.catch { error ->
logcat(LogPriority.ERROR, error)
_events.send(Event.InternalError)
}
.map { pagingData ->
pagingData.toHistoryUiModels()
.flatMapLatest { query ->
getHistory.subscribe(query ?: "")
.distinctUntilChanged()
.catch { error ->
logcat(LogPriority.ERROR, error)
_events.send(Event.InternalError)
}
.map { it.toHistoryUiModels() }
.flowOn(Dispatchers.IO)
}
.collect { newList -> mutableState.update { it.copy(list = newList) } }
}
}
@@ -76,67 +77,59 @@ class HistoryPresenter(
}
fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
presenterScope.launchIO {
coroutineScope.launchIO {
sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false))
}
}
fun resumeLastChapterRead() {
presenterScope.launchIO {
sendNextChapterEvent(getNextChapters.await(onlyUnread = false))
}
}
private suspend fun sendNextChapterEvent(chapters: List<Chapter>) {
val chapter = chapters.firstOrNull()
_events.send(if (chapter != null) Event.OpenChapter(chapter) else Event.NoNextChapterFound)
_events.send(Event.OpenChapter(chapter))
}
fun removeFromHistory(history: HistoryWithRelations) {
presenterScope.launchIO {
coroutineScope.launchIO {
removeHistory.await(history)
}
}
fun removeAllFromHistory(mangaId: Long) {
presenterScope.launchIO {
coroutineScope.launchIO {
removeHistory.await(mangaId)
}
}
fun removeAllHistory() {
presenterScope.launchIO {
coroutineScope.launchIO {
val result = removeHistory.awaitAll()
if (!result) return@launchIO
withUIContext {
view?.activity?.toast(R.string.clear_history_completed)
}
_events.send(Event.HistoryCleared)
}
}
fun updateSearchQuery(query: String?) {
mutableState.update { it.copy(searchQuery = query) }
}
fun setDialog(dialog: Dialog?) {
mutableState.update { it.copy(dialog = dialog) }
}
sealed class Dialog {
object DeleteAll : Dialog()
data class Delete(val history: HistoryWithRelations) : Dialog()
}
sealed class Event {
data class OpenChapter(val chapter: Chapter?) : Event()
object InternalError : Event()
object NoNextChapterFound : Event()
data class OpenChapter(val chapter: Chapter) : Event()
object HistoryCleared : Event()
}
}
@Stable
interface HistoryState {
var searchQuery: String?
var dialog: HistoryPresenter.Dialog?
}
fun HistoryState(): HistoryState {
return HistoryStateImpl()
}
class HistoryStateImpl : HistoryState {
override var searchQuery: String? by mutableStateOf(null)
override var dialog: HistoryPresenter.Dialog? by mutableStateOf(null)
}
@Immutable
data class HistoryState(
val searchQuery: String? = null,
val list: List<HistoryUiModel>? = null,
val dialog: HistoryScreenModel.Dialog? = null,
)