mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Voyager on History tab (#8481)
This commit is contained in:
		| @@ -34,3 +34,5 @@ class PreferenceMutableState<T>( | ||||
|         return { preference.set(it) } | ||||
|     } | ||||
| } | ||||
|  | ||||
| fun <T> Preference<T>.asState(scope: CoroutineScope) = PreferenceMutableState(this, scope) | ||||
|   | ||||
| @@ -8,9 +8,9 @@ import androidx.compose.ui.Alignment | ||||
| import androidx.compose.ui.Modifier | ||||
|  | ||||
| @Composable | ||||
| fun LoadingScreen() { | ||||
| fun LoadingScreen(modifier: Modifier = Modifier) { | ||||
|     Box( | ||||
|         modifier = Modifier.fillMaxSize(), | ||||
|         modifier = modifier.fillMaxSize(), | ||||
|         contentAlignment = Alignment.Center, | ||||
|     ) { | ||||
|         CircularProgressIndicator() | ||||
|   | ||||
| @@ -1,110 +1,80 @@ | ||||
| package eu.kanade.presentation.history | ||||
|  | ||||
| import androidx.compose.foundation.layout.padding | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.DeleteSweep | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.ScaffoldDefaults | ||||
| import androidx.compose.material3.SnackbarHost | ||||
| import androidx.compose.material3.SnackbarHostState | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.Modifier | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import eu.kanade.domain.history.model.HistoryWithRelations | ||||
| import eu.kanade.presentation.components.AppBarTitle | ||||
| import eu.kanade.presentation.components.EmptyScreen | ||||
| import eu.kanade.presentation.components.LoadingScreen | ||||
| import eu.kanade.presentation.components.Scaffold | ||||
| import eu.kanade.presentation.components.SearchToolbar | ||||
| import eu.kanade.presentation.history.components.HistoryContent | ||||
| import eu.kanade.presentation.history.components.HistoryDeleteAllDialog | ||||
| import eu.kanade.presentation.history.components.HistoryDeleteDialog | ||||
| import eu.kanade.presentation.history.components.HistoryToolbar | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.history.HistoryPresenter | ||||
| import eu.kanade.tachiyomi.ui.history.HistoryPresenter.Dialog | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import eu.kanade.tachiyomi.ui.history.HistoryScreenModel | ||||
| import eu.kanade.tachiyomi.ui.history.HistoryState | ||||
| import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
| import java.util.Date | ||||
|  | ||||
| @Composable | ||||
| fun HistoryScreen( | ||||
|     presenter: HistoryPresenter, | ||||
|     onClickCover: (HistoryWithRelations) -> Unit, | ||||
|     onClickResume: (HistoryWithRelations) -> Unit, | ||||
|     state: HistoryState, | ||||
|     snackbarHostState: SnackbarHostState, | ||||
|     incognitoMode: Boolean, | ||||
|     downloadedOnlyMode: Boolean, | ||||
|     onSearchQueryChange: (String?) -> Unit, | ||||
|     onClickCover: (mangaId: Long) -> Unit, | ||||
|     onClickResume: (mangaId: Long, chapterId: Long) -> Unit, | ||||
|     onDialogChange: (HistoryScreenModel.Dialog?) -> Unit, | ||||
| ) { | ||||
|     val context = LocalContext.current | ||||
|  | ||||
|     Scaffold( | ||||
|         topBar = { scrollBehavior -> | ||||
|             HistoryToolbar( | ||||
|                 state = presenter, | ||||
|                 incognitoMode = presenter.isIncognitoMode, | ||||
|                 downloadedOnlyMode = presenter.isDownloadOnly, | ||||
|             SearchToolbar( | ||||
|                 titleContent = { AppBarTitle(stringResource(R.string.history)) }, | ||||
|                 searchQuery = state.searchQuery, | ||||
|                 onChangeSearchQuery = onSearchQueryChange, | ||||
|                 actions = { | ||||
|                     IconButton(onClick = { onDialogChange(HistoryScreenModel.Dialog.DeleteAll) }) { | ||||
|                         Icon( | ||||
|                             Icons.Outlined.DeleteSweep, | ||||
|                             contentDescription = stringResource(R.string.pref_clear_history), | ||||
|                         ) | ||||
|                     } | ||||
|                 }, | ||||
|                 downloadedOnlyMode = downloadedOnlyMode, | ||||
|                 incognitoMode = incognitoMode, | ||||
|                 scrollBehavior = scrollBehavior, | ||||
|             ) | ||||
|         }, | ||||
|         snackbarHost = { SnackbarHost(hostState = snackbarHostState) }, | ||||
|         contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets), | ||||
|     ) { contentPadding -> | ||||
|         val items by presenter.getHistory().collectAsState(initial = null) | ||||
|         val contentPaddingWithNavBar = TachiyomiBottomNavigationView.withBottomNavPadding(contentPadding) | ||||
|         items.let { | ||||
|         state.list.let { | ||||
|             if (it == null) { | ||||
|                 LoadingScreen() | ||||
|                 LoadingScreen(modifier = Modifier.padding(contentPadding)) | ||||
|             } else if (it.isEmpty()) { | ||||
|                 EmptyScreen( | ||||
|                     textResource = R.string.information_no_recent_manga, | ||||
|                     modifier = Modifier.padding(contentPaddingWithNavBar), | ||||
|                     modifier = Modifier.padding(contentPadding), | ||||
|                 ) | ||||
|             } else { | ||||
|                 HistoryContent( | ||||
|                     history = it, | ||||
|                     contentPadding = contentPaddingWithNavBar, | ||||
|                     onClickCover = onClickCover, | ||||
|                     onClickResume = onClickResume, | ||||
|                     onClickDelete = { item -> presenter.dialog = Dialog.Delete(item) }, | ||||
|                     contentPadding = contentPadding, | ||||
|                     onClickCover = { history -> onClickCover(history.mangaId) }, | ||||
|                     onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) }, | ||||
|                     onClickDelete = { item -> onDialogChange(HistoryScreenModel.Dialog.Delete(item)) }, | ||||
|                 ) | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         LaunchedEffect(items) { | ||||
|             if (items != null) { | ||||
|                 (presenter.view?.activity as? MainActivity)?.ready = true | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|     val onDismissRequest = { presenter.dialog = null } | ||||
|     when (val dialog = presenter.dialog) { | ||||
|         is Dialog.Delete -> { | ||||
|             HistoryDeleteDialog( | ||||
|                 onDismissRequest = onDismissRequest, | ||||
|                 onDelete = { all -> | ||||
|                     if (all) { | ||||
|                         presenter.removeAllFromHistory(dialog.history.mangaId) | ||||
|                     } else { | ||||
|                         presenter.removeFromHistory(dialog.history) | ||||
|                     } | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|         is Dialog.DeleteAll -> { | ||||
|             HistoryDeleteAllDialog( | ||||
|                 onDismissRequest = onDismissRequest, | ||||
|                 onDelete = { | ||||
|                     presenter.removeAllHistory() | ||||
|                 }, | ||||
|             ) | ||||
|         } | ||||
|         null -> {} | ||||
|     } | ||||
|     LaunchedEffect(Unit) { | ||||
|         presenter.events.collectLatest { event -> | ||||
|             when (event) { | ||||
|                 HistoryPresenter.Event.InternalError -> context.toast(R.string.internal_error) | ||||
|                 HistoryPresenter.Event.NoNextChapterFound -> context.toast(R.string.no_next_chapter) | ||||
|                 is HistoryPresenter.Event.OpenChapter -> { | ||||
|                     val intent = ReaderActivity.newIntent(context, event.chapter.mangaId, event.chapter.id) | ||||
|                     context.startActivity(intent) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,36 +0,0 @@ | ||||
| package eu.kanade.presentation.history.components | ||||
|  | ||||
| import androidx.compose.material.icons.Icons | ||||
| import androidx.compose.material.icons.outlined.DeleteSweep | ||||
| import androidx.compose.material3.Icon | ||||
| import androidx.compose.material3.IconButton | ||||
| import androidx.compose.material3.TopAppBarScrollBehavior | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.ui.res.stringResource | ||||
| import eu.kanade.presentation.components.AppBarTitle | ||||
| import eu.kanade.presentation.components.SearchToolbar | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.history.HistoryPresenter | ||||
| import eu.kanade.tachiyomi.ui.history.HistoryState | ||||
|  | ||||
| @Composable | ||||
| fun HistoryToolbar( | ||||
|     state: HistoryState, | ||||
|     scrollBehavior: TopAppBarScrollBehavior, | ||||
|     incognitoMode: Boolean, | ||||
|     downloadedOnlyMode: Boolean, | ||||
| ) { | ||||
|     SearchToolbar( | ||||
|         titleContent = { AppBarTitle(stringResource(R.string.history)) }, | ||||
|         searchQuery = state.searchQuery, | ||||
|         onChangeSearchQuery = { state.searchQuery = it }, | ||||
|         actions = { | ||||
|             IconButton(onClick = { state.dialog = HistoryPresenter.Dialog.DeleteAll }) { | ||||
|                 Icon(Icons.Outlined.DeleteSweep, contentDescription = stringResource(R.string.pref_clear_history)) | ||||
|             } | ||||
|         }, | ||||
|         downloadedOnlyMode = downloadedOnlyMode, | ||||
|         incognitoMode = incognitoMode, | ||||
|         scrollBehavior = scrollBehavior, | ||||
|     ) | ||||
| } | ||||
| @@ -5,6 +5,8 @@ import android.view.LayoutInflater | ||||
| import android.view.View | ||||
| import androidx.activity.OnBackPressedDispatcherOwner | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.CompositionLocalProvider | ||||
| import eu.kanade.presentation.util.LocalRouter | ||||
| import eu.kanade.tachiyomi.databinding.ComposeControllerBinding | ||||
| import eu.kanade.tachiyomi.util.view.setComposeContent | ||||
| import nucleus.presenter.Presenter | ||||
| @@ -21,7 +23,9 @@ abstract class FullComposeController<P : Presenter<*>>(bundle: Bundle? = null) : | ||||
|  | ||||
|         binding.root.apply { | ||||
|             setComposeContent { | ||||
|                 ComposeContent() | ||||
|                 CompositionLocalProvider(LocalRouter provides router) { | ||||
|                     ComposeContent() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| @@ -52,7 +56,9 @@ abstract class BasicFullComposeController(bundle: Bundle? = null) : | ||||
|  | ||||
|         binding.root.apply { | ||||
|             setComposeContent { | ||||
|                 ComposeContent() | ||||
|                 CompositionLocalProvider(LocalRouter provides router) { | ||||
|                     ComposeContent() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -1,30 +1,26 @@ | ||||
| package eu.kanade.tachiyomi.ui.history | ||||
|  | ||||
| import androidx.compose.runtime.Composable | ||||
| import eu.kanade.presentation.history.HistoryScreen | ||||
| import eu.kanade.tachiyomi.ui.base.controller.FullComposeController | ||||
| import cafe.adriel.voyager.navigator.Navigator | ||||
| import eu.kanade.domain.history.interactor.GetNextChapters | ||||
| import eu.kanade.tachiyomi.ui.base.controller.BasicFullComposeController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.RootController | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
|  | ||||
| class HistoryController : FullComposeController<HistoryPresenter>(), RootController { | ||||
|  | ||||
|     override fun createPresenter() = HistoryPresenter() | ||||
| class HistoryController : BasicFullComposeController(), RootController { | ||||
|  | ||||
|     @Composable | ||||
|     override fun ComposeContent() { | ||||
|         HistoryScreen( | ||||
|             presenter = presenter, | ||||
|             onClickCover = { history -> | ||||
|                 router.pushController(MangaController(history.mangaId)) | ||||
|             }, | ||||
|             onClickResume = { history -> | ||||
|                 presenter.getNextChapterForManga(history.mangaId, history.chapterId) | ||||
|             }, | ||||
|         ) | ||||
|         Navigator(screen = HistoryScreen) | ||||
|     } | ||||
|  | ||||
|     fun resumeLastChapterRead() { | ||||
|         presenter.resumeLastChapterRead() | ||||
|         val context = activity ?: return | ||||
|         viewScope.launchIO { | ||||
|             val chapter = Injekt.get<GetNextChapters>().await(onlyUnread = false).firstOrNull() | ||||
|             HistoryScreen.openChapter(context, chapter) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,97 @@ | ||||
| package eu.kanade.tachiyomi.ui.history | ||||
|  | ||||
| import android.content.Context | ||||
| import androidx.compose.material3.SnackbarHostState | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.LaunchedEffect | ||||
| import androidx.compose.runtime.collectAsState | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.ui.platform.LocalContext | ||||
| import cafe.adriel.voyager.core.model.rememberScreenModel | ||||
| import cafe.adriel.voyager.core.screen.Screen | ||||
| import cafe.adriel.voyager.navigator.currentOrThrow | ||||
| import eu.kanade.domain.chapter.model.Chapter | ||||
| import eu.kanade.presentation.history.HistoryScreen | ||||
| import eu.kanade.presentation.history.components.HistoryDeleteAllDialog | ||||
| import eu.kanade.presentation.history.components.HistoryDeleteDialog | ||||
| import eu.kanade.presentation.util.LocalRouter | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.base.controller.pushController | ||||
| import eu.kanade.tachiyomi.ui.main.MainActivity | ||||
| import eu.kanade.tachiyomi.ui.manga.MangaController | ||||
| import eu.kanade.tachiyomi.ui.reader.ReaderActivity | ||||
| import kotlinx.coroutines.flow.collectLatest | ||||
|  | ||||
| object HistoryScreen : Screen { | ||||
|  | ||||
|     private val snackbarHostState = SnackbarHostState() | ||||
|  | ||||
|     @Composable | ||||
|     override fun Content() { | ||||
|         val router = LocalRouter.currentOrThrow | ||||
|         val context = LocalContext.current | ||||
|         val screenModel = rememberScreenModel { HistoryScreenModel() } | ||||
|         val state by screenModel.state.collectAsState() | ||||
|  | ||||
|         HistoryScreen( | ||||
|             state = state, | ||||
|             snackbarHostState = snackbarHostState, | ||||
|             incognitoMode = screenModel.isIncognitoMode, | ||||
|             downloadedOnlyMode = screenModel.isDownloadOnly, | ||||
|             onSearchQueryChange = screenModel::updateSearchQuery, | ||||
|             onClickCover = { router.pushController(MangaController(it)) }, | ||||
|             onClickResume = screenModel::getNextChapterForManga, | ||||
|             onDialogChange = screenModel::setDialog, | ||||
|         ) | ||||
|  | ||||
|         val onDismissRequest = { screenModel.setDialog(null) } | ||||
|         when (val dialog = state.dialog) { | ||||
|             is HistoryScreenModel.Dialog.Delete -> { | ||||
|                 HistoryDeleteDialog( | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onDelete = { all -> | ||||
|                         if (all) { | ||||
|                             screenModel.removeAllFromHistory(dialog.history.mangaId) | ||||
|                         } else { | ||||
|                             screenModel.removeFromHistory(dialog.history) | ||||
|                         } | ||||
|                     }, | ||||
|                 ) | ||||
|             } | ||||
|             is HistoryScreenModel.Dialog.DeleteAll -> { | ||||
|                 HistoryDeleteAllDialog( | ||||
|                     onDismissRequest = onDismissRequest, | ||||
|                     onDelete = screenModel::removeAllHistory, | ||||
|                 ) | ||||
|             } | ||||
|             null -> {} | ||||
|         } | ||||
|  | ||||
|         LaunchedEffect(state.list) { | ||||
|             if (state.list != null) { | ||||
|                 (context as? MainActivity)?.ready = true | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         LaunchedEffect(Unit) { | ||||
|             screenModel.events.collectLatest { e -> | ||||
|                 when (e) { | ||||
|                     HistoryScreenModel.Event.InternalError -> | ||||
|                         snackbarHostState.showSnackbar(context.getString(R.string.internal_error)) | ||||
|                     HistoryScreenModel.Event.HistoryCleared -> | ||||
|                         snackbarHostState.showSnackbar(context.getString(R.string.clear_history_completed)) | ||||
|                     is HistoryScreenModel.Event.OpenChapter -> openChapter(context, e.chapter) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     suspend fun openChapter(context: Context, chapter: Chapter?) { | ||||
|         if (chapter != null) { | ||||
|             val intent = ReaderActivity.newIntent(context, chapter.mangaId, chapter.id) | ||||
|             context.startActivity(intent) | ||||
|         } else { | ||||
|             snackbarHostState.showSnackbar(context.getString(R.string.no_next_chapter)) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -1,11 +1,10 @@ | ||||
| package eu.kanade.tachiyomi.ui.history | ||||
| 
 | ||||
| import androidx.compose.runtime.Composable | ||||
| import androidx.compose.runtime.Stable | ||||
| import androidx.compose.runtime.Immutable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.remember | ||||
| import androidx.compose.runtime.setValue | ||||
| import cafe.adriel.voyager.core.model.StateScreenModel | ||||
| import cafe.adriel.voyager.core.model.coroutineScope | ||||
| import eu.kanade.core.prefs.asState | ||||
| import eu.kanade.core.util.insertSeparators | ||||
| import eu.kanade.domain.base.BasePreferences | ||||
| import eu.kanade.domain.chapter.model.Chapter | ||||
| @@ -14,51 +13,53 @@ import eu.kanade.domain.history.interactor.GetNextChapters | ||||
| import eu.kanade.domain.history.interactor.RemoveHistory | ||||
| import eu.kanade.domain.history.model.HistoryWithRelations | ||||
| import eu.kanade.presentation.history.HistoryUiModel | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter | ||||
| import eu.kanade.tachiyomi.util.lang.launchIO | ||||
| import eu.kanade.tachiyomi.util.lang.toDateKey | ||||
| import eu.kanade.tachiyomi.util.lang.withUIContext | ||||
| import eu.kanade.tachiyomi.util.system.logcat | ||||
| import eu.kanade.tachiyomi.util.system.toast | ||||
| import kotlinx.coroutines.Dispatchers | ||||
| import kotlinx.coroutines.channels.Channel | ||||
| import kotlinx.coroutines.flow.Flow | ||||
| import kotlinx.coroutines.flow.catch | ||||
| import kotlinx.coroutines.flow.distinctUntilChanged | ||||
| import kotlinx.coroutines.flow.flatMapLatest | ||||
| import kotlinx.coroutines.flow.flowOn | ||||
| import kotlinx.coroutines.flow.map | ||||
| import kotlinx.coroutines.flow.receiveAsFlow | ||||
| import kotlinx.coroutines.flow.update | ||||
| import kotlinx.coroutines.launch | ||||
| import logcat.LogPriority | ||||
| import uy.kohesive.injekt.Injekt | ||||
| import uy.kohesive.injekt.api.get | ||||
| import java.util.Date | ||||
| 
 | ||||
| class HistoryPresenter( | ||||
|     private val state: HistoryStateImpl = HistoryState() as HistoryStateImpl, | ||||
| class HistoryScreenModel( | ||||
|     private val getHistory: GetHistory = Injekt.get(), | ||||
|     private val getNextChapters: GetNextChapters = Injekt.get(), | ||||
|     private val removeHistory: RemoveHistory = Injekt.get(), | ||||
|     preferences: BasePreferences = Injekt.get(), | ||||
| ) : BasePresenter<HistoryController>(), HistoryState by state { | ||||
| ) : StateScreenModel<HistoryState>(HistoryState()) { | ||||
| 
 | ||||
|     private val _events: Channel<Event> = Channel(Int.MAX_VALUE) | ||||
|     private val _events: Channel<Event> = Channel(Channel.UNLIMITED) | ||||
|     val events: Flow<Event> = _events.receiveAsFlow() | ||||
| 
 | ||||
|     val isDownloadOnly: Boolean by preferences.downloadedOnly().asState() | ||||
|     val isIncognitoMode: Boolean by preferences.incognitoMode().asState() | ||||
|     val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope) | ||||
|     val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope) | ||||
| 
 | ||||
|     @Composable | ||||
|     fun getHistory(): Flow<List<HistoryUiModel>> { | ||||
|         val query = searchQuery ?: "" | ||||
|         return remember(query) { | ||||
|             getHistory.subscribe(query) | ||||
|     init { | ||||
|         coroutineScope.launch { | ||||
|             state.map { it.searchQuery } | ||||
|                 .distinctUntilChanged() | ||||
|                 .catch { error -> | ||||
|                     logcat(LogPriority.ERROR, error) | ||||
|                     _events.send(Event.InternalError) | ||||
|                 } | ||||
|                 .map { pagingData -> | ||||
|                     pagingData.toHistoryUiModels() | ||||
|                 .flatMapLatest { query -> | ||||
|                     getHistory.subscribe(query ?: "") | ||||
|                         .distinctUntilChanged() | ||||
|                         .catch { error -> | ||||
|                             logcat(LogPriority.ERROR, error) | ||||
|                             _events.send(Event.InternalError) | ||||
|                         } | ||||
|                         .map { it.toHistoryUiModels() } | ||||
|                         .flowOn(Dispatchers.IO) | ||||
|                 } | ||||
|                 .collect { newList -> mutableState.update { it.copy(list = newList) } } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
| @@ -76,67 +77,59 @@ class HistoryPresenter( | ||||
|     } | ||||
| 
 | ||||
|     fun getNextChapterForManga(mangaId: Long, chapterId: Long) { | ||||
|         presenterScope.launchIO { | ||||
|         coroutineScope.launchIO { | ||||
|             sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun resumeLastChapterRead() { | ||||
|         presenterScope.launchIO { | ||||
|             sendNextChapterEvent(getNextChapters.await(onlyUnread = false)) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private suspend fun sendNextChapterEvent(chapters: List<Chapter>) { | ||||
|         val chapter = chapters.firstOrNull() | ||||
|         _events.send(if (chapter != null) Event.OpenChapter(chapter) else Event.NoNextChapterFound) | ||||
|         _events.send(Event.OpenChapter(chapter)) | ||||
|     } | ||||
| 
 | ||||
|     fun removeFromHistory(history: HistoryWithRelations) { | ||||
|         presenterScope.launchIO { | ||||
|         coroutineScope.launchIO { | ||||
|             removeHistory.await(history) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun removeAllFromHistory(mangaId: Long) { | ||||
|         presenterScope.launchIO { | ||||
|         coroutineScope.launchIO { | ||||
|             removeHistory.await(mangaId) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun removeAllHistory() { | ||||
|         presenterScope.launchIO { | ||||
|         coroutineScope.launchIO { | ||||
|             val result = removeHistory.awaitAll() | ||||
|             if (!result) return@launchIO | ||||
|             withUIContext { | ||||
|                 view?.activity?.toast(R.string.clear_history_completed) | ||||
|             } | ||||
|             _events.send(Event.HistoryCleared) | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     fun updateSearchQuery(query: String?) { | ||||
|         mutableState.update { it.copy(searchQuery = query) } | ||||
|     } | ||||
| 
 | ||||
|     fun setDialog(dialog: Dialog?) { | ||||
|         mutableState.update { it.copy(dialog = dialog) } | ||||
|     } | ||||
| 
 | ||||
|     sealed class Dialog { | ||||
|         object DeleteAll : Dialog() | ||||
|         data class Delete(val history: HistoryWithRelations) : Dialog() | ||||
|     } | ||||
| 
 | ||||
|     sealed class Event { | ||||
|         data class OpenChapter(val chapter: Chapter?) : Event() | ||||
|         object InternalError : Event() | ||||
|         object NoNextChapterFound : Event() | ||||
|         data class OpenChapter(val chapter: Chapter) : Event() | ||||
|         object HistoryCleared : Event() | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| @Stable | ||||
| interface HistoryState { | ||||
|     var searchQuery: String? | ||||
|     var dialog: HistoryPresenter.Dialog? | ||||
| } | ||||
| 
 | ||||
| fun HistoryState(): HistoryState { | ||||
|     return HistoryStateImpl() | ||||
| } | ||||
| 
 | ||||
| class HistoryStateImpl : HistoryState { | ||||
|     override var searchQuery: String? by mutableStateOf(null) | ||||
|     override var dialog: HistoryPresenter.Dialog? by mutableStateOf(null) | ||||
| } | ||||
| @Immutable | ||||
| data class HistoryState( | ||||
|     val searchQuery: String? = null, | ||||
|     val list: List<HistoryUiModel>? = null, | ||||
|     val dialog: HistoryScreenModel.Dialog? = null, | ||||
| ) | ||||
| @@ -9,6 +9,7 @@ import android.os.Parcelable | ||||
| import android.util.AttributeSet | ||||
| import android.view.ViewPropertyAnimator | ||||
| import androidx.compose.foundation.layout.PaddingValues | ||||
| import androidx.compose.foundation.layout.WindowInsets | ||||
| import androidx.compose.foundation.layout.calculateEndPadding | ||||
| import androidx.compose.foundation.layout.calculateStartPadding | ||||
| import androidx.compose.runtime.Composable | ||||
| @@ -16,6 +17,7 @@ import androidx.compose.runtime.ReadOnlyComposable | ||||
| import androidx.compose.runtime.getValue | ||||
| import androidx.compose.runtime.mutableStateOf | ||||
| import androidx.compose.runtime.setValue | ||||
| import androidx.compose.ui.platform.LocalDensity | ||||
| import androidx.compose.ui.platform.LocalLayoutDirection | ||||
| import androidx.compose.ui.unit.dp | ||||
| import androidx.compose.ui.unit.max | ||||
| @@ -26,6 +28,7 @@ import com.google.android.material.bottomnavigation.BottomNavigationView | ||||
| import eu.kanade.tachiyomi.R | ||||
| import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale | ||||
| import eu.kanade.tachiyomi.util.system.pxToDp | ||||
| import kotlin.math.max | ||||
|  | ||||
| class TachiyomiBottomNavigationView @JvmOverloads constructor( | ||||
|     context: Context, | ||||
| @@ -173,5 +176,21 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor( | ||||
|                 bottom = max(origin.calculateBottomPadding(), bottomNavPadding), | ||||
|             ) | ||||
|         } | ||||
|  | ||||
|         /** | ||||
|          * @see withBottomNavPadding | ||||
|          */ | ||||
|         @ReadOnlyComposable | ||||
|         @Composable | ||||
|         fun withBottomNavInset(origin: WindowInsets): WindowInsets { | ||||
|             val density = LocalDensity.current | ||||
|             val layoutDirection = LocalLayoutDirection.current | ||||
|             return WindowInsets( | ||||
|                 left = origin.getLeft(density, layoutDirection), | ||||
|                 top = origin.getTop(density), | ||||
|                 right = origin.getRight(density, layoutDirection), | ||||
|                 bottom = max(origin.getBottom(density), with(density) { bottomNavPadding.roundToPx() }), | ||||
|             ) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user