From c751851941796ae7fa10f9dfba9b75396bbbf692 Mon Sep 17 00:00:00 2001 From: Andreas Date: Sat, 23 Jul 2022 16:01:51 +0200 Subject: [PATCH] Use Stable interface for History screen (#7586) - Adds Stable interface - Move last Dialog into Compose - Make History screen be full Compose screen --- .../presentation/history/HistoryScreen.kt | 249 +++++------------- .../history/components/HistoryContent.kt | 95 +++++++ .../history/components/HistoryDialog.kt | 103 ++++++++ .../history/components/HistoryToolbar.kt | 96 +++++++ .../history/ClearHistoryDialogController.kt | 21 -- .../ui/recent/history/HistoryController.kt | 74 +----- .../ui/recent/history/HistoryPresenter.kt | 106 ++++---- app/src/main/res/menu/history.xml | 20 -- app/src/main/res/values/strings.xml | 1 + 9 files changed, 416 insertions(+), 349 deletions(-) create mode 100644 app/src/main/java/eu/kanade/presentation/history/components/HistoryContent.kt create mode 100644 app/src/main/java/eu/kanade/presentation/history/components/HistoryDialog.kt create mode 100644 app/src/main/java/eu/kanade/presentation/history/components/HistoryToolbar.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt delete mode 100644 app/src/main/res/menu/history.xml 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 f93b7136b4..56664ba041 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/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, - 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(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 -> {} + } + 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) } } } } - - 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), - ) - } - } - }, - 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 { diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryContent.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryContent.kt new file mode 100644 index 0000000000..4e7e750de1 --- /dev/null +++ b/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, + 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) + } + } + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialog.kt b/app/src/main/java/eu/kanade/presentation/history/components/HistoryDialog.kt new file mode 100644 index 0000000000..e354f41aac --- /dev/null +++ b/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)) + } + }, + ) +} 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 new file mode 100644 index 0000000000..cd8ae5891c --- /dev/null +++ b/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() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt deleted file mode 100644 index a4080a01d5..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt +++ /dev/null @@ -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() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt index f413b73c89..a46cbf624f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt +++ b/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(), RootController { - - private var query = "" - - override fun getTitle() = resources?.getString(R.string.label_recent_manga) +class HistoryController : FullComposeController(), 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(), 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() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt index c7ae8a046f..d324b16e5e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt +++ b/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() { +) : BasePresenter(), HistoryState by state { - private val _query: MutableStateFlow = MutableStateFlow("") - private val _state: MutableStateFlow = MutableStateFlow(HistoryState.Loading) - val state: StateFlow = _state.asStateFlow() + private val _events: Channel = Channel(Int.MAX_VALUE) + val events: Flow = _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 { + 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.toHistoryUiModels(): PagingData { @@ -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() + } } -sealed class HistoryState { - object Loading : HistoryState() - data class Error(val error: Throwable) : HistoryState() - data class Success(val uiModels: Flow>) : HistoryState() +@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) } diff --git a/app/src/main/res/menu/history.xml b/app/src/main/res/menu/history.xml deleted file mode 100644 index 284cd197a4..0000000000 --- a/app/src/main/res/menu/history.xml +++ /dev/null @@ -1,20 +0,0 @@ - - - - - - - - diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index c9765543f2..e09345dbe3 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -82,6 +82,7 @@ Next chapter Retry Remove + Remove everything Start Resume Open in browser