From bc3bb82651e85a8b0584a645e0fa07ab97ab1767 Mon Sep 17 00:00:00 2001
From: Ivan Iskandar <12537387+ivaniskandar@users.noreply.github.com>
Date: Wed, 9 Nov 2022 21:26:29 +0700
Subject: [PATCH] Voyager on History tab (#8481)

---
 .../core/prefs/PreferenceMutableState.kt      |   2 +
 .../presentation/components/LoadingScreen.kt  |   4 +-
 .../presentation/history/HistoryScreen.kt     | 116 +++++++-----------
 .../history/components/HistoryToolbar.kt      |  36 ------
 .../ui/base/controller/ComposeController.kt   |  10 +-
 .../tachiyomi/ui/history/HistoryController.kt |  30 ++---
 .../tachiyomi/ui/history/HistoryScreen.kt     |  97 +++++++++++++++
 ...toryPresenter.kt => HistoryScreenModel.kt} | 105 ++++++++--------
 .../widget/TachiyomiBottomNavigationView.kt   |  19 +++
 9 files changed, 233 insertions(+), 186 deletions(-)
 delete mode 100644 app/src/main/java/eu/kanade/presentation/history/components/HistoryToolbar.kt
 create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreen.kt
 rename app/src/main/java/eu/kanade/tachiyomi/ui/history/{HistoryPresenter.kt => HistoryScreenModel.kt} (59%)

diff --git a/app/src/main/java/eu/kanade/core/prefs/PreferenceMutableState.kt b/app/src/main/java/eu/kanade/core/prefs/PreferenceMutableState.kt
index 4efd95539..b8c41b45f 100644
--- a/app/src/main/java/eu/kanade/core/prefs/PreferenceMutableState.kt
+++ b/app/src/main/java/eu/kanade/core/prefs/PreferenceMutableState.kt
@@ -34,3 +34,5 @@ class PreferenceMutableState<T>(
         return { preference.set(it) }
     }
 }
+
+fun <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope)
diff --git a/app/src/main/java/eu/kanade/presentation/components/LoadingScreen.kt b/app/src/main/java/eu/kanade/presentation/components/LoadingScreen.kt
index 0afb5cfa8..357858938 100644
--- a/app/src/main/java/eu/kanade/presentation/components/LoadingScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/components/LoadingScreen.kt
@@ -8,9 +8,9 @@ import androidx.compose.ui.Alignment
 import androidx.compose.ui.Modifier
 
 @Composable
-fun LoadingScreen() {
+fun LoadingScreen(modifier: Modifier = Modifier) {
     Box(
-        modifier = Modifier.fillMaxSize(),
+        modifier = modifier.fillMaxSize(),
         contentAlignment = Alignment.Center,
     ) {
         CircularProgressIndicator()
diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt
index 256f3dca2..5157e6f08 100644
--- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt
+++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt
@@ -1,110 +1,80 @@
 package eu.kanade.presentation.history
 
 import androidx.compose.foundation.layout.padding
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.DeleteSweep
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.ScaffoldDefaults
+import androidx.compose.material3.SnackbarHost
+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.Modifier
-import androidx.compose.ui.platform.LocalContext
+import androidx.compose.ui.res.stringResource
 import eu.kanade.domain.history.model.HistoryWithRelations
+import eu.kanade.presentation.components.AppBarTitle
 import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.components.LoadingScreen
 import eu.kanade.presentation.components.Scaffold
+import eu.kanade.presentation.components.SearchToolbar
 import eu.kanade.presentation.history.components.HistoryContent
-import eu.kanade.presentation.history.components.HistoryDeleteAllDialog
-import eu.kanade.presentation.history.components.HistoryDeleteDialog
-import eu.kanade.presentation.history.components.HistoryToolbar
 import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.history.HistoryPresenter
-import eu.kanade.tachiyomi.ui.history.HistoryPresenter.Dialog
-import eu.kanade.tachiyomi.ui.main.MainActivity
-import eu.kanade.tachiyomi.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.util.system.toast
+import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
+import eu.kanade.tachiyomi.ui.history.HistoryState
 import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
-import kotlinx.coroutines.flow.collectLatest
 import java.util.Date
 
 @Composable
 fun HistoryScreen(
-    presenter: HistoryPresenter,
-    onClickCover: (HistoryWithRelations) -> Unit,
-    onClickResume: (HistoryWithRelations) -> Unit,
+    state: HistoryState,
+    snackbarHostState: SnackbarHostState,
+    incognitoMode: Boolean,
+    downloadedOnlyMode: Boolean,
+    onSearchQueryChange: (String?) -> Unit,
+    onClickCover: (mangaId: Long) -> Unit,
+    onClickResume: (mangaId: Long, chapterId: Long) -> Unit,
+    onDialogChange: (HistoryScreenModel.Dialog?) -> Unit,
 ) {
-    val context = LocalContext.current
-
     Scaffold(
         topBar = { scrollBehavior ->
-            HistoryToolbar(
-                state = presenter,
-                incognitoMode = presenter.isIncognitoMode,
-                downloadedOnlyMode = presenter.isDownloadOnly,
+            SearchToolbar(
+                titleContent = { AppBarTitle(stringResource(R.string.history)) },
+                searchQuery = state.searchQuery,
+                onChangeSearchQuery = onSearchQueryChange,
+                actions = {
+                    IconButton(onClick = { onDialogChange(HistoryScreenModel.Dialog.DeleteAll) }) {
+                        Icon(
+                            Icons.Outlined.DeleteSweep,
+                            contentDescription = stringResource(R.string.pref_clear_history),
+                        )
+                    }
+                },
+                downloadedOnlyMode = downloadedOnlyMode,
+                incognitoMode = incognitoMode,
                 scrollBehavior = scrollBehavior,
             )
         },
+        snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
+        contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets),
     ) { contentPadding ->
-        val items by presenter.getHistory().collectAsState(initial = null)
-        val contentPaddingWithNavBar = TachiyomiBottomNavigationView.withBottomNavPadding(contentPadding)
-        items.let {
+        state.list.let {
             if (it == null) {
-                LoadingScreen()
+                LoadingScreen(modifier = Modifier.padding(contentPadding))
             } else if (it.isEmpty()) {
                 EmptyScreen(
                     textResource = R.string.information_no_recent_manga,
-                    modifier = Modifier.padding(contentPaddingWithNavBar),
+                    modifier = Modifier.padding(contentPadding),
                 )
             } else {
                 HistoryContent(
                     history = it,
-                    contentPadding = contentPaddingWithNavBar,
-                    onClickCover = onClickCover,
-                    onClickResume = onClickResume,
-                    onClickDelete = { item -> presenter.dialog = Dialog.Delete(item) },
+                    contentPadding = contentPadding,
+                    onClickCover = { history -> onClickCover(history.mangaId) },
+                    onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
+                    onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) },
                 )
             }
         }
-
-        LaunchedEffect(items) {
-            if (items != null) {
-                (presenter.view?.activity as? MainActivity)?.ready = true
-            }
-        }
-    }
-    val onDismissRequest = { presenter.dialog = null }
-    when (val dialog = presenter.dialog) {
-        is Dialog.Delete -> {
-            HistoryDeleteDialog(
-                onDismissRequest = onDismissRequest,
-                onDelete = { all ->
-                    if (all) {
-                        presenter.removeAllFromHistory(dialog.history.mangaId)
-                    } else {
-                        presenter.removeFromHistory(dialog.history)
-                    }
-                },
-            )
-        }
-        is Dialog.DeleteAll -> {
-            HistoryDeleteAllDialog(
-                onDismissRequest = onDismissRequest,
-                onDelete = {
-                    presenter.removeAllHistory()
-                },
-            )
-        }
-        null -> {}
-    }
-    LaunchedEffect(Unit) {
-        presenter.events.collectLatest { event ->
-            when (event) {
-                HistoryPresenter.Event.InternalError -> context.toast(R.string.internal_error)
-                HistoryPresenter.Event.NoNextChapterFound -> context.toast(R.string.no_next_chapter)
-                is HistoryPresenter.Event.OpenChapter -> {
-                    val intent = ReaderActivity.newIntent(context, event.chapter.mangaId, event.chapter.id)
-                    context.startActivity(intent)
-                }
-            }
-        }
     }
 }
 
diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryToolbar.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryToolbar.kt
deleted file mode 100644
index 19aef8214..000000000
--- a/app/src/main/java/eu/kanade/presentation/history/components/HistoryToolbar.kt
+++ /dev/null
@@ -1,36 +0,0 @@
-package eu.kanade.presentation.history.components
-
-import androidx.compose.material.icons.Icons
-import androidx.compose.material.icons.outlined.DeleteSweep
-import androidx.compose.material3.Icon
-import androidx.compose.material3.IconButton
-import androidx.compose.material3.TopAppBarScrollBehavior
-import androidx.compose.runtime.Composable
-import androidx.compose.ui.res.stringResource
-import eu.kanade.presentation.components.AppBarTitle
-import eu.kanade.presentation.components.SearchToolbar
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.history.HistoryPresenter
-import eu.kanade.tachiyomi.ui.history.HistoryState
-
-@Composable
-fun HistoryToolbar(
-    state: HistoryState,
-    scrollBehavior: TopAppBarScrollBehavior,
-    incognitoMode: Boolean,
-    downloadedOnlyMode: Boolean,
-) {
-    SearchToolbar(
-        titleContent = { AppBarTitle(stringResource(R.string.history)) },
-        searchQuery = state.searchQuery,
-        onChangeSearchQuery = { state.searchQuery = it },
-        actions = {
-            IconButton(onClick = { state.dialog = HistoryPresenter.Dialog.DeleteAll }) {
-                Icon(Icons.Outlined.DeleteSweep, contentDescription = stringResource(R.string.pref_clear_history))
-            }
-        },
-        downloadedOnlyMode = downloadedOnlyMode,
-        incognitoMode = incognitoMode,
-        scrollBehavior = scrollBehavior,
-    )
-}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt
index 16b12725c..5c0883eee 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt
@@ -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()
+                }
             }
         }
     }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryController.kt
index a7dbf63bb..c0410450e 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryController.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryController.kt
@@ -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)
+        }
     }
 }
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreen.kt
new file mode 100644
index 000000000..0e3b943dd
--- /dev/null
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreen.kt
@@ -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))
+        }
+    }
+}
diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt
similarity index 59%
rename from app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryPresenter.kt
rename to app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt
index e252b0026..4991c89c9 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryPresenter.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/ui/history/HistoryScreenModel.kt
@@ -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,
+)
diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt
index 002a5f029..0bea9f8a0 100644
--- a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt
+++ b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiBottomNavigationView.kt
@@ -9,6 +9,7 @@ import android.os.Parcelable
 import android.util.AttributeSet
 import android.view.ViewPropertyAnimator
 import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.WindowInsets
 import androidx.compose.foundation.layout.calculateEndPadding
 import androidx.compose.foundation.layout.calculateStartPadding
 import androidx.compose.runtime.Composable
@@ -16,6 +17,7 @@ import androidx.compose.runtime.ReadOnlyComposable
 import androidx.compose.runtime.getValue
 import androidx.compose.runtime.mutableStateOf
 import androidx.compose.runtime.setValue
+import androidx.compose.ui.platform.LocalDensity
 import androidx.compose.ui.platform.LocalLayoutDirection
 import androidx.compose.ui.unit.dp
 import androidx.compose.ui.unit.max
@@ -26,6 +28,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView
 import eu.kanade.tachiyomi.R
 import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
 import eu.kanade.tachiyomi.util.system.pxToDp
+import kotlin.math.max
 
 class TachiyomiBottomNavigationView @JvmOverloads constructor(
     context: Context,
@@ -173,5 +176,21 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor(
                 bottom = max(origin.calculateBottomPadding(), bottomNavPadding),
             )
         }
+
+        /**
+         * @see withBottomNavPadding
+         */
+        @ReadOnlyComposable
+        @Composable
+        fun withBottomNavInset(origin: WindowInsets): WindowInsets {
+            val density = LocalDensity.current
+            val layoutDirection = LocalLayoutDirection.current
+            return WindowInsets(
+                left = origin.getLeft(density, layoutDirection),
+                top = origin.getTop(density),
+                right = origin.getRight(density, layoutDirection),
+                bottom = max(origin.getBottom(density), with(density) { bottomNavPadding.roundToPx() }),
+            )
+        }
     }
 }