mirror of
https://github.com/mihonapp/mihon.git
synced 2025-11-19 15:31:13 +01:00
Voyager on History tab (#8481)
This commit is contained in:
@@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user