mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-31 14:27:57 +01:00 
			
		
		
		
	Use Stable interface for History screen (#7586)
- Adds Stable interface - Move last Dialog into Compose - Make History screen be full Compose screen
This commit is contained in:
		| @@ -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 -> {} | ||||
|     } | ||||
|     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 { | ||||
|   | ||||
| @@ -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) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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)) | ||||
|             } | ||||
|         }, | ||||
|     ) | ||||
| } | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
| @@ -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() | ||||
|     } | ||||
|   | ||||
| @@ -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() | ||||
|     } | ||||
| } | ||||
|  | ||||
| sealed class HistoryState { | ||||
|     object Loading : HistoryState() | ||||
|     data class Error(val error: Throwable) : HistoryState() | ||||
|     data class Success(val uiModels: Flow<PagingData<HistoryUiModel>>) : 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) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user