Sfoglia il codice sorgente

Use Stable interface for History screen (#7586)

- Adds Stable interface
- Move last Dialog into Compose
- Make History screen be full Compose screen
Andreas 2 anni fa
parent
commit
c751851941

+ 59 - 190
app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt

@@ -1,220 +1,89 @@
 package eu.kanade.presentation.history
 
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.animateFloat
-import androidx.compose.animation.core.infiniteRepeatable
-import androidx.compose.animation.core.rememberInfiniteTransition
-import androidx.compose.animation.core.tween
-import androidx.compose.foundation.interaction.MutableInteractionSource
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.WindowInsets
-import androidx.compose.foundation.layout.asPaddingValues
-import androidx.compose.foundation.layout.navigationBars
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.lazy.rememberLazyListState
-import androidx.compose.foundation.selection.toggleable
-import androidx.compose.material3.AlertDialog
-import androidx.compose.material3.Checkbox
-import androidx.compose.material3.Text
-import androidx.compose.material3.TextButton
+import androidx.compose.foundation.layout.safeContentPadding
+import androidx.compose.material3.Scaffold
 import androidx.compose.runtime.Composable
-import androidx.compose.runtime.collectAsState
-import androidx.compose.runtime.getValue
-import androidx.compose.runtime.mutableStateOf
-import androidx.compose.runtime.remember
-import androidx.compose.runtime.setValue
-import androidx.compose.ui.Alignment
+import androidx.compose.runtime.LaunchedEffect
 import androidx.compose.ui.Modifier
-import androidx.compose.ui.geometry.Offset
-import androidx.compose.ui.graphics.Brush.Companion.linearGradient
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import androidx.compose.ui.input.nestedscroll.nestedScroll
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
+import androidx.compose.ui.platform.LocalContext
 import androidx.paging.LoadState
-import androidx.paging.compose.LazyPagingItems
-import androidx.paging.compose.collectAsLazyPagingItems
-import androidx.paging.compose.items
 import eu.kanade.domain.history.model.HistoryWithRelations
 import eu.kanade.presentation.components.EmptyScreen
 import eu.kanade.presentation.components.LoadingScreen
-import eu.kanade.presentation.components.RelativeDateHeader
-import eu.kanade.presentation.components.ScrollbarLazyColumn
-import eu.kanade.presentation.history.components.HistoryItem
-import eu.kanade.presentation.history.components.HistoryItemShimmer
-import eu.kanade.presentation.util.bottomNavPaddingValues
-import eu.kanade.presentation.util.plus
-import eu.kanade.presentation.util.shimmerGradient
-import eu.kanade.presentation.util.topPaddingValues
+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.data.preference.PreferencesHelper
+import eu.kanade.tachiyomi.ui.reader.ReaderActivity
 import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter
-import eu.kanade.tachiyomi.ui.recent.history.HistoryState
-import uy.kohesive.injekt.Injekt
-import uy.kohesive.injekt.api.get
-import java.text.DateFormat
+import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter.Dialog
+import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.coroutines.flow.collectLatest
 import java.util.Date
 
 @Composable
 fun HistoryScreen(
-    nestedScrollInterop: NestedScrollConnection,
     presenter: HistoryPresenter,
     onClickCover: (HistoryWithRelations) -> Unit,
     onClickResume: (HistoryWithRelations) -> Unit,
-    onClickDelete: (HistoryWithRelations, Boolean) -> Unit,
 ) {
-    val state by presenter.state.collectAsState()
-    when (state) {
-        is HistoryState.Loading -> LoadingScreen()
-        is HistoryState.Error -> Text(text = (state as HistoryState.Error).error.message!!)
-        is HistoryState.Success ->
-            HistoryContent(
-                nestedScroll = nestedScrollInterop,
-                history = (state as HistoryState.Success).uiModels.collectAsLazyPagingItems(),
+    val context = LocalContext.current
+    Scaffold(
+        modifier = Modifier.safeContentPadding(),
+        topBar = {
+            HistoryToolbar(state = presenter)
+        },
+    ) {
+        val items = presenter.getLazyHistory()
+        when {
+            items.loadState.refresh is LoadState.Loading && items.itemCount < 1 -> LoadingScreen()
+            items.loadState.refresh is LoadState.NotLoading && items.itemCount < 1 -> EmptyScreen(textResource = R.string.information_no_recent_manga)
+            else -> HistoryContent(
+                history = items,
+                contentPadding = it,
                 onClickCover = onClickCover,
                 onClickResume = onClickResume,
-                onClickDelete = onClickDelete,
+                onClickDelete = { presenter.dialog = Dialog.Delete(it) },
             )
+        }
     }
-}
-
-@Composable
-fun HistoryContent(
-    history: LazyPagingItems<HistoryUiModel>,
-    onClickCover: (HistoryWithRelations) -> Unit,
-    onClickResume: (HistoryWithRelations) -> Unit,
-    onClickDelete: (HistoryWithRelations, Boolean) -> Unit,
-    preferences: PreferencesHelper = Injekt.get(),
-    nestedScroll: NestedScrollConnection,
-) {
-    if (history.loadState.refresh is LoadState.NotLoading && history.itemCount == 0) {
-        EmptyScreen(textResource = R.string.information_no_recent_manga)
-        return
-    }
-
-    val relativeTime: Int = remember { preferences.relativeTime().get() }
-    val dateFormat: DateFormat = remember { preferences.dateFormat() }
-
-    var removeState by remember { mutableStateOf<HistoryWithRelations?>(null) }
-
-    val scrollState = rememberLazyListState()
-
-    ScrollbarLazyColumn(
-        modifier = Modifier
-            .nestedScroll(nestedScroll),
-        contentPadding = bottomNavPaddingValues + WindowInsets.navigationBars.asPaddingValues() + topPaddingValues,
-        state = scrollState,
-    ) {
-        items(history) { item ->
-            when (item) {
-                is HistoryUiModel.Header -> {
-                    RelativeDateHeader(
-                        modifier = Modifier
-                            .animateItemPlacement(),
-                        date = item.date,
-                        relativeTime = relativeTime,
-                        dateFormat = dateFormat,
-                    )
-                }
-                is HistoryUiModel.Item -> {
-                    val value = item.item
-                    HistoryItem(
-                        modifier = Modifier.animateItemPlacement(),
-                        history = value,
-                        onClickCover = { onClickCover(value) },
-                        onClickResume = { onClickResume(value) },
-                        onClickDelete = { removeState = value },
-                    )
-                }
-                null -> {
-                    val transition = rememberInfiniteTransition()
-                    val translateAnimation = transition.animateFloat(
-                        initialValue = 0f,
-                        targetValue = 1000f,
-                        animationSpec = infiniteRepeatable(
-                            animation = tween(
-                                durationMillis = 1000,
-                                easing = LinearEasing,
-                            ),
-                        ),
-                    )
-
-                    val brush = remember {
-                        linearGradient(
-                            colors = shimmerGradient,
-                            start = Offset(0f, 0f),
-                            end = Offset(
-                                x = translateAnimation.value,
-                                y = 00f,
-                            ),
-                        )
+    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)
                     }
-                    HistoryItemShimmer(brush = brush)
-                }
-            }
+                },
+            )
         }
+        Dialog.DeleteAll -> {
+            HistoryDeleteAllDialog(
+                onDismissRequest = onDismissRequest,
+                onDelete = {
+                    presenter.deleteAllHistory()
+                },
+            )
+        }
+        else -> {}
     }
-
-    if (removeState != null) {
-        RemoveHistoryDialog(
-            onPositive = { all ->
-                onClickDelete(removeState!!, all)
-                removeState = null
-            },
-            onNegative = { removeState = null },
-        )
-    }
-}
-
-@Composable
-fun RemoveHistoryDialog(
-    onPositive: (Boolean) -> Unit,
-    onNegative: () -> Unit,
-) {
-    var removeEverything by remember { mutableStateOf(false) }
-
-    AlertDialog(
-        title = {
-            Text(text = stringResource(R.string.action_remove))
-        },
-        text = {
-            Column {
-                Text(text = stringResource(R.string.dialog_with_checkbox_remove_description))
-                Row(
-                    modifier = Modifier
-                        .padding(top = 16.dp)
-                        .toggleable(
-                            interactionSource = remember { MutableInteractionSource() },
-                            indication = null,
-                            value = removeEverything,
-                            onValueChange = { removeEverything = it },
-                        ),
-                    verticalAlignment = Alignment.CenterVertically,
-                ) {
-                    Checkbox(
-                        checked = removeEverything,
-                        onCheckedChange = null,
-                    )
-                    Text(
-                        modifier = Modifier.padding(start = 4.dp),
-                        text = stringResource(R.string.dialog_with_checkbox_reset),
-                    )
+    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)
                 }
             }
-        },
-        onDismissRequest = onNegative,
-        confirmButton = {
-            TextButton(onClick = { onPositive(removeEverything) }) {
-                Text(text = stringResource(R.string.action_remove))
-            }
-        },
-        dismissButton = {
-            TextButton(onClick = onNegative) {
-                Text(text = stringResource(R.string.action_cancel))
-            }
-        },
-    )
+        }
+    }
 }
 
 sealed class HistoryUiModel {

+ 95 - 0
app/src/main/java/eu/kanade/presentation/history/components/HistoryContent.kt

@@ -0,0 +1,95 @@
+package eu.kanade.presentation.history.components
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.animateFloat
+import androidx.compose.animation.core.infiniteRepeatable
+import androidx.compose.animation.core.rememberInfiniteTransition
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.lazy.rememberLazyListState
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
+import androidx.compose.ui.graphics.Brush
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.items
+import eu.kanade.domain.history.model.HistoryWithRelations
+import eu.kanade.presentation.components.RelativeDateHeader
+import eu.kanade.presentation.components.ScrollbarLazyColumn
+import eu.kanade.presentation.history.HistoryUiModel
+import eu.kanade.presentation.util.bottomNavPaddingValues
+import eu.kanade.presentation.util.plus
+import eu.kanade.presentation.util.shimmerGradient
+import eu.kanade.presentation.util.topPaddingValues
+import eu.kanade.tachiyomi.data.preference.PreferencesHelper
+import uy.kohesive.injekt.Injekt
+import uy.kohesive.injekt.api.get
+import java.text.DateFormat
+
+@Composable
+fun HistoryContent(
+    history: LazyPagingItems<HistoryUiModel>,
+    contentPadding: PaddingValues,
+    onClickCover: (HistoryWithRelations) -> Unit,
+    onClickResume: (HistoryWithRelations) -> Unit,
+    onClickDelete: (HistoryWithRelations) -> Unit,
+    preferences: PreferencesHelper = Injekt.get(),
+) {
+    val relativeTime: Int = remember { preferences.relativeTime().get() }
+    val dateFormat: DateFormat = remember { preferences.dateFormat() }
+
+    ScrollbarLazyColumn(
+        contentPadding = contentPadding + bottomNavPaddingValues + topPaddingValues,
+        state = rememberLazyListState(),
+    ) {
+        items(history) { item ->
+            when (item) {
+                is HistoryUiModel.Header -> {
+                    RelativeDateHeader(
+                        modifier = Modifier
+                            .animateItemPlacement(),
+                        date = item.date,
+                        relativeTime = relativeTime,
+                        dateFormat = dateFormat,
+                    )
+                }
+                is HistoryUiModel.Item -> {
+                    val value = item.item
+                    HistoryItem(
+                        modifier = Modifier.animateItemPlacement(),
+                        history = value,
+                        onClickCover = { onClickCover(value) },
+                        onClickResume = { onClickResume(value) },
+                        onClickDelete = { onClickDelete(value) },
+                    )
+                }
+                null -> {
+                    val transition = rememberInfiniteTransition()
+                    val translateAnimation = transition.animateFloat(
+                        initialValue = 0f,
+                        targetValue = 1000f,
+                        animationSpec = infiniteRepeatable(
+                            animation = tween(
+                                durationMillis = 1000,
+                                easing = LinearEasing,
+                            ),
+                        ),
+                    )
+
+                    val brush = remember {
+                        Brush.linearGradient(
+                            colors = shimmerGradient,
+                            start = Offset(0f, 0f),
+                            end = Offset(
+                                x = translateAnimation.value,
+                                y = 00f,
+                            ),
+                        )
+                    }
+                    HistoryItemShimmer(brush = brush)
+                }
+            }
+        }
+    }
+}

+ 103 - 0
app/src/main/java/eu/kanade/presentation/history/components/HistoryDialog.kt

@@ -0,0 +1,103 @@
+package eu.kanade.presentation.history.components
+
+import androidx.compose.foundation.interaction.MutableInteractionSource
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.selection.toggleable
+import androidx.compose.material3.AlertDialog
+import androidx.compose.material3.Checkbox
+import androidx.compose.material3.Text
+import androidx.compose.material3.TextButton
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import eu.kanade.tachiyomi.R
+
+@Composable
+fun HistoryDeleteDialog(
+    onDismissRequest: () -> Unit,
+    onDelete: (Boolean) -> Unit,
+) {
+    var removeEverything by remember { mutableStateOf(false) }
+
+    AlertDialog(
+        title = {
+            Text(text = stringResource(R.string.action_remove))
+        },
+        text = {
+            Column {
+                Text(text = stringResource(R.string.dialog_with_checkbox_remove_description))
+                Row(
+                    modifier = Modifier
+                        .padding(top = 16.dp)
+                        .toggleable(
+                            interactionSource = remember { MutableInteractionSource() },
+                            indication = null,
+                            value = removeEverything,
+                            onValueChange = { removeEverything = it },
+                        ),
+                    verticalAlignment = Alignment.CenterVertically,
+                ) {
+                    Checkbox(
+                        checked = removeEverything,
+                        onCheckedChange = null,
+                    )
+                    Text(
+                        modifier = Modifier.padding(start = 4.dp),
+                        text = stringResource(R.string.dialog_with_checkbox_reset),
+                    )
+                }
+            }
+        },
+        onDismissRequest = onDismissRequest,
+        confirmButton = {
+            TextButton(onClick = {
+                onDelete(removeEverything)
+                onDismissRequest()
+            },) {
+                Text(text = stringResource(R.string.action_remove))
+            }
+        },
+        dismissButton = {
+            TextButton(onClick = onDismissRequest) {
+                Text(text = stringResource(android.R.string.cancel))
+            }
+        },
+    )
+}
+
+@Composable
+fun HistoryDeleteAllDialog(
+    onDismissRequest: () -> Unit,
+    onDelete: () -> Unit,
+) {
+    AlertDialog(
+        title = {
+            Text(text = stringResource(R.string.action_remove_everything))
+        },
+        text = {
+            Text(text = stringResource(R.string.clear_history_confirmation))
+        },
+        onDismissRequest = onDismissRequest,
+        confirmButton = {
+            TextButton(onClick = {
+                onDelete()
+                onDismissRequest()
+            },) {
+                Text(text = stringResource(android.R.string.ok))
+            }
+        },
+        dismissButton = {
+            TextButton(onClick = onDismissRequest) {
+                Text(text = stringResource(android.R.string.cancel))
+            }
+        },
+    )
+}

+ 96 - 0
app/src/main/java/eu/kanade/presentation/history/components/HistoryToolbar.kt

@@ -0,0 +1,96 @@
+package eu.kanade.presentation.history.components
+
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.text.BasicTextField
+import androidx.compose.material.icons.Icons
+import androidx.compose.material.icons.outlined.ArrowBack
+import androidx.compose.material.icons.outlined.DeleteSweep
+import androidx.compose.material.icons.outlined.Search
+import androidx.compose.material3.Icon
+import androidx.compose.material3.IconButton
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.SmallTopAppBar
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.LaunchedEffect
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.focus.FocusRequester
+import androidx.compose.ui.focus.focusRequester
+import androidx.compose.ui.graphics.SolidColor
+import androidx.compose.ui.res.stringResource
+import eu.kanade.tachiyomi.R
+import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter
+import eu.kanade.tachiyomi.ui.recent.history.HistoryState
+import kotlinx.coroutines.delay
+
+@Composable
+fun HistoryToolbar(
+    state: HistoryState,
+) {
+    if (state.searchQuery == null) {
+        HistoryRegularToolbar(
+            onClickSearch = { state.searchQuery = "" },
+            onClickDelete = { state.dialog = HistoryPresenter.Dialog.DeleteAll },
+        )
+    } else {
+        HistorySearchToolbar(
+            searchQuery = state.searchQuery!!,
+            onChangeSearchQuery = { state.searchQuery = it },
+            onClickCloseSearch = { state.searchQuery = null },
+        )
+    }
+}
+
+@Composable
+fun HistoryRegularToolbar(
+    onClickSearch: () -> Unit,
+    onClickDelete: () -> Unit,
+) {
+    SmallTopAppBar(
+        title = {
+            Text(text = stringResource(id = R.string.history))
+        },
+        actions = {
+            IconButton(onClick = onClickSearch) {
+                Icon(Icons.Outlined.Search, contentDescription = "search")
+            }
+            IconButton(onClick = onClickDelete) {
+                Icon(Icons.Outlined.DeleteSweep, contentDescription = "delete")
+            }
+        },
+    )
+}
+
+@Composable
+fun HistorySearchToolbar(
+    searchQuery: String,
+    onChangeSearchQuery: (String) -> Unit,
+    onClickCloseSearch: () -> Unit,
+) {
+    val focusRequester = remember { FocusRequester.Default }
+    SmallTopAppBar(
+        navigationIcon = {
+            IconButton(onClick = onClickCloseSearch) {
+                Icon(Icons.Outlined.ArrowBack, contentDescription = "delete")
+            }
+        },
+        title = {
+            BasicTextField(
+                value = searchQuery,
+                onValueChange = onChangeSearchQuery,
+                modifier = Modifier
+                    .fillMaxWidth()
+                    .focusRequester(focusRequester),
+                textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground),
+                singleLine = true,
+                cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
+            )
+        },
+    )
+    LaunchedEffect(focusRequester) {
+        // TODO: https://issuetracker.google.com/issues/204502668
+        delay(100)
+        focusRequester.requestFocus()
+    }
+}

+ 0 - 21
app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt

@@ -1,21 +0,0 @@
-package eu.kanade.tachiyomi.ui.recent.history
-
-import android.app.Dialog
-import android.os.Bundle
-import com.google.android.material.dialog.MaterialAlertDialogBuilder
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.DialogController
-
-class ClearHistoryDialogController : DialogController() {
-    override fun onCreateDialog(savedViewState: Bundle?): Dialog {
-        return MaterialAlertDialogBuilder(activity!!)
-            .setMessage(R.string.clear_history_confirmation)
-            .setPositiveButton(android.R.string.ok) { _, _ ->
-                (targetController as? HistoryController)
-                    ?.presenter
-                    ?.deleteAllHistory()
-            }
-            .setNegativeButton(android.R.string.cancel, null)
-            .create()
-    }
-}

+ 3 - 71
app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt

@@ -1,37 +1,19 @@
 package eu.kanade.tachiyomi.ui.recent.history
 
-import android.view.Menu
-import android.view.MenuInflater
-import android.view.MenuItem
-import androidx.appcompat.widget.SearchView
 import androidx.compose.runtime.Composable
-import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
-import eu.kanade.domain.chapter.model.Chapter
 import eu.kanade.presentation.history.HistoryScreen
-import eu.kanade.tachiyomi.R
-import eu.kanade.tachiyomi.ui.base.controller.ComposeController
+import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
 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.ui.reader.ReaderActivity
-import eu.kanade.tachiyomi.util.system.toast
-import kotlinx.coroutines.flow.filter
-import kotlinx.coroutines.flow.launchIn
-import kotlinx.coroutines.flow.onEach
-import reactivecircus.flowbinding.appcompat.queryTextChanges
 
-class HistoryController : ComposeController<HistoryPresenter>(), RootController {
-
-    private var query = ""
-
-    override fun getTitle() = resources?.getString(R.string.label_recent_manga)
+class HistoryController : FullComposeController<HistoryPresenter>(), RootController {
 
     override fun createPresenter() = HistoryPresenter()
 
     @Composable
-    override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) {
+    override fun ComposeContent() {
         HistoryScreen(
-            nestedScrollInterop = nestedScrollInterop,
             presenter = presenter,
             onClickCover = { history ->
                 router.pushController(MangaController(history.mangaId))
@@ -39,59 +21,9 @@ class HistoryController : ComposeController<HistoryPresenter>(), RootController
             onClickResume = { history ->
                 presenter.getNextChapterForManga(history.mangaId, history.chapterId)
             },
-            onClickDelete = { history, all ->
-                if (all) {
-                    // Reset last read of chapter to 0L
-                    presenter.removeAllFromHistory(history.mangaId)
-                } else {
-                    // Remove all chapters belonging to manga from library
-                    presenter.removeFromHistory(history)
-                }
-            },
         )
     }
 
-    override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
-        inflater.inflate(R.menu.history, menu)
-        val searchItem = menu.findItem(R.id.action_search)
-        val searchView = searchItem.actionView as SearchView
-        searchView.maxWidth = Int.MAX_VALUE
-        if (query.isNotEmpty()) {
-            searchItem.expandActionView()
-            searchView.setQuery(query, true)
-            searchView.clearFocus()
-        }
-        searchView.queryTextChanges()
-            .filter { router.backstack.lastOrNull()?.controller == this }
-            .onEach {
-                query = it.toString()
-                presenter.search(query)
-            }
-            .launchIn(viewScope)
-    }
-
-    override fun onOptionsItemSelected(item: MenuItem): Boolean {
-        return when (item.itemId) {
-            R.id.action_clear_history -> {
-                val dialog = ClearHistoryDialogController()
-                dialog.targetController = this@HistoryController
-                dialog.showDialog(router)
-                true
-            }
-            else -> super.onOptionsItemSelected(item)
-        }
-    }
-
-    fun openChapter(chapter: Chapter?) {
-        val activity = activity ?: return
-        if (chapter != null) {
-            val intent = ReaderActivity.newIntent(activity, chapter.mangaId, chapter.id)
-            startActivity(intent)
-        } else {
-            activity.toast(R.string.no_next_chapter)
-        }
-    }
-
     fun resumeLastChapterRead() {
         presenter.resumeLastChapterRead()
     }

+ 59 - 47
app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt

@@ -1,10 +1,19 @@
 package eu.kanade.tachiyomi.ui.recent.history
 
-import android.os.Bundle
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.Stable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.rememberCoroutineScope
+import androidx.compose.runtime.setValue
 import androidx.paging.PagingData
 import androidx.paging.cachedIn
+import androidx.paging.compose.LazyPagingItems
+import androidx.paging.compose.collectAsLazyPagingItems
 import androidx.paging.insertSeparators
 import androidx.paging.map
+import eu.kanade.domain.chapter.model.Chapter
 import eu.kanade.domain.history.interactor.DeleteHistoryTable
 import eu.kanade.domain.history.interactor.GetHistory
 import eu.kanade.domain.history.interactor.GetNextChapter
@@ -17,53 +26,46 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 import eu.kanade.tachiyomi.util.lang.launchIO
 import eu.kanade.tachiyomi.util.lang.launchUI
 import eu.kanade.tachiyomi.util.lang.toDateKey
+import eu.kanade.tachiyomi.util.system.logcat
 import eu.kanade.tachiyomi.util.system.toast
+import kotlinx.coroutines.channels.Channel
 import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
 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
 import java.util.Date
 
-/**
- * Presenter of HistoryFragment.
- * Contains information and data for fragment.
- * Observable updates should be called from here.
- */
 class HistoryPresenter(
+    private val state: HistoryStateImpl = HistoryState() as HistoryStateImpl,
     private val getHistory: GetHistory = Injekt.get(),
     private val getNextChapter: GetNextChapter = Injekt.get(),
     private val deleteHistoryTable: DeleteHistoryTable = Injekt.get(),
     private val removeHistoryById: RemoveHistoryById = Injekt.get(),
     private val removeHistoryByMangaId: RemoveHistoryByMangaId = Injekt.get(),
-) : BasePresenter<HistoryController>() {
+) : BasePresenter<HistoryController>(), HistoryState by state {
 
-    private val _query: MutableStateFlow<String> = MutableStateFlow("")
-    private val _state: MutableStateFlow<HistoryState> = MutableStateFlow(HistoryState.Loading)
-    val state: StateFlow<HistoryState> = _state.asStateFlow()
+    private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
+    val events: Flow<Event> = _events.receiveAsFlow()
 
-    override fun onCreate(savedState: Bundle?) {
-        super.onCreate(savedState)
-
-        presenterScope.launchIO {
-            _query.collectLatest { query ->
-                getHistory.subscribe(query)
-                    .catch { exception ->
-                        _state.value = HistoryState.Error(exception)
-                    }
-                    .map { pagingData ->
-                        pagingData.toHistoryUiModels()
-                    }
-                    .cachedIn(presenterScope)
-                    .let { uiModelsPagingDataFlow ->
-                        _state.value = HistoryState.Success(uiModelsPagingDataFlow)
-                    }
-            }
+    @Composable
+    fun getLazyHistory(): LazyPagingItems<HistoryUiModel> {
+        val scope = rememberCoroutineScope()
+        val query = searchQuery ?: ""
+        val flow = remember(query) {
+            getHistory.subscribe(query)
+                .catch { error ->
+                    logcat(LogPriority.ERROR, error)
+                    _events.send(Event.InternalError)
+                }
+                .map { pagingData ->
+                    pagingData.toHistoryUiModels()
+                }
+                .cachedIn(scope)
         }
+        return flow.collectAsLazyPagingItems()
     }
 
     private fun PagingData<HistoryWithRelations>.toHistoryUiModels(): PagingData<HistoryUiModel> {
@@ -81,12 +83,6 @@ class HistoryPresenter(
             }
     }
 
-    fun search(query: String) {
-        presenterScope.launchIO {
-            _query.emit(query)
-        }
-    }
-
     fun removeFromHistory(history: HistoryWithRelations) {
         presenterScope.launchIO {
             removeHistoryById.await(history)
@@ -102,9 +98,7 @@ class HistoryPresenter(
     fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
         presenterScope.launchIO {
             val chapter = getNextChapter.await(mangaId, chapterId)
-            launchUI {
-                view?.openChapter(chapter)
-            }
+            _events.send(if (chapter != null) Event.OpenChapter(chapter) else Event.NoNextChapterFound)
         }
     }
 
@@ -121,15 +115,33 @@ class HistoryPresenter(
     fun resumeLastChapterRead() {
         presenterScope.launchIO {
             val chapter = getNextChapter.await()
-            launchUI {
-                view?.openChapter(chapter)
-            }
+            _events.send(if (chapter != null) Event.OpenChapter(chapter) else Event.NoNextChapterFound)
         }
     }
+
+    sealed class Dialog {
+        object DeleteAll : Dialog()
+        data class Delete(val history: HistoryWithRelations) : Dialog()
+    }
+
+    sealed class Event {
+        object InternalError : Event()
+        object NoNextChapterFound : Event()
+        data class OpenChapter(val chapter: Chapter) : Event()
+    }
+}
+
+@Stable
+interface HistoryState {
+    var searchQuery: String?
+    var dialog: HistoryPresenter.Dialog?
+}
+
+fun HistoryState(): HistoryState {
+    return HistoryStateImpl()
 }
 
-sealed class HistoryState {
-    object Loading : HistoryState()
-    data class Error(val error: Throwable) : HistoryState()
-    data class Success(val uiModels: Flow<PagingData<HistoryUiModel>>) : HistoryState()
+class HistoryStateImpl : HistoryState {
+    override var searchQuery: String? by mutableStateOf(null)
+    override var dialog: HistoryPresenter.Dialog? by mutableStateOf(null)
 }

+ 0 - 20
app/src/main/res/menu/history.xml

@@ -1,20 +0,0 @@
-<?xml version="1.0" encoding="utf-8"?>
-<menu xmlns:android="http://schemas.android.com/apk/res/android"
-    xmlns:app="http://schemas.android.com/apk/res-auto">
-
-    <item
-        android:id="@+id/action_search"
-        android:icon="@drawable/ic_search_24dp"
-        android:title="@string/action_search"
-        app:actionViewClass="eu.kanade.tachiyomi.widget.TachiyomiSearchView"
-        app:iconTint="?attr/colorOnSurface"
-        app:showAsAction="ifRoom|collapseActionView" />
-
-    <item
-        android:id="@+id/action_clear_history"
-        android:icon="@drawable/ic_delete_sweep_24dp"
-        android:title="@string/pref_clear_history"
-        app:iconTint="?attr/colorOnSurface"
-        app:showAsAction="ifRoom" />
-
-</menu>

+ 1 - 0
app/src/main/res/values/strings.xml

@@ -82,6 +82,7 @@
     <string name="action_next_chapter">Next chapter</string>
     <string name="action_retry">Retry</string>
     <string name="action_remove">Remove</string>
+    <string name="action_remove_everything">Remove everything</string>
     <string name="action_start">Start</string>
     <string name="action_resume">Resume</string>
     <string name="action_open_in_browser">Open in browser</string>