Voyager on History tab (#8481)
This commit is contained in:
parent
ba00d9e5d2
commit
bc3bb82651
9 changed files with 233 additions and 186 deletions
|
@ -34,3 +34,5 @@ class PreferenceMutableState<T>(
|
||||||
return { preference.set(it) }
|
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
|
import androidx.compose.ui.Modifier
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun LoadingScreen() {
|
fun LoadingScreen(modifier: Modifier = Modifier) {
|
||||||
Box(
|
Box(
|
||||||
modifier = Modifier.fillMaxSize(),
|
modifier = modifier.fillMaxSize(),
|
||||||
contentAlignment = Alignment.Center,
|
contentAlignment = Alignment.Center,
|
||||||
) {
|
) {
|
||||||
CircularProgressIndicator()
|
CircularProgressIndicator()
|
||||||
|
|
|
@ -1,110 +1,80 @@
|
||||||
package eu.kanade.presentation.history
|
package eu.kanade.presentation.history
|
||||||
|
|
||||||
import androidx.compose.foundation.layout.padding
|
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.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.Modifier
|
||||||
import androidx.compose.ui.platform.LocalContext
|
import androidx.compose.ui.res.stringResource
|
||||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
|
import eu.kanade.presentation.components.AppBarTitle
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.LoadingScreen
|
import eu.kanade.presentation.components.LoadingScreen
|
||||||
import eu.kanade.presentation.components.Scaffold
|
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.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.R
|
||||||
import eu.kanade.tachiyomi.ui.history.HistoryPresenter
|
import eu.kanade.tachiyomi.ui.history.HistoryScreenModel
|
||||||
import eu.kanade.tachiyomi.ui.history.HistoryPresenter.Dialog
|
import eu.kanade.tachiyomi.ui.history.HistoryState
|
||||||
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.widget.TachiyomiBottomNavigationView
|
import eu.kanade.tachiyomi.widget.TachiyomiBottomNavigationView
|
||||||
import kotlinx.coroutines.flow.collectLatest
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun HistoryScreen(
|
fun HistoryScreen(
|
||||||
presenter: HistoryPresenter,
|
state: HistoryState,
|
||||||
onClickCover: (HistoryWithRelations) -> Unit,
|
snackbarHostState: SnackbarHostState,
|
||||||
onClickResume: (HistoryWithRelations) -> Unit,
|
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(
|
Scaffold(
|
||||||
topBar = { scrollBehavior ->
|
topBar = { scrollBehavior ->
|
||||||
HistoryToolbar(
|
SearchToolbar(
|
||||||
state = presenter,
|
titleContent = { AppBarTitle(stringResource(R.string.history)) },
|
||||||
incognitoMode = presenter.isIncognitoMode,
|
searchQuery = state.searchQuery,
|
||||||
downloadedOnlyMode = presenter.isDownloadOnly,
|
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,
|
scrollBehavior = scrollBehavior,
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
snackbarHost = { SnackbarHost(hostState = snackbarHostState) },
|
||||||
|
contentWindowInsets = TachiyomiBottomNavigationView.withBottomNavInset(ScaffoldDefaults.contentWindowInsets),
|
||||||
) { contentPadding ->
|
) { contentPadding ->
|
||||||
val items by presenter.getHistory().collectAsState(initial = null)
|
state.list.let {
|
||||||
val contentPaddingWithNavBar = TachiyomiBottomNavigationView.withBottomNavPadding(contentPadding)
|
|
||||||
items.let {
|
|
||||||
if (it == null) {
|
if (it == null) {
|
||||||
LoadingScreen()
|
LoadingScreen(modifier = Modifier.padding(contentPadding))
|
||||||
} else if (it.isEmpty()) {
|
} else if (it.isEmpty()) {
|
||||||
EmptyScreen(
|
EmptyScreen(
|
||||||
textResource = R.string.information_no_recent_manga,
|
textResource = R.string.information_no_recent_manga,
|
||||||
modifier = Modifier.padding(contentPaddingWithNavBar),
|
modifier = Modifier.padding(contentPadding),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
HistoryContent(
|
HistoryContent(
|
||||||
history = it,
|
history = it,
|
||||||
contentPadding = contentPaddingWithNavBar,
|
contentPadding = contentPadding,
|
||||||
onClickCover = onClickCover,
|
onClickCover = { history -> onClickCover(history.mangaId) },
|
||||||
onClickResume = onClickResume,
|
onClickResume = { history -> onClickResume(history.mangaId, history.chapterId) },
|
||||||
onClickDelete = { item -> presenter.dialog = Dialog.Delete(item) },
|
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 android.view.View
|
||||||
import androidx.activity.OnBackPressedDispatcherOwner
|
import androidx.activity.OnBackPressedDispatcherOwner
|
||||||
import androidx.compose.runtime.Composable
|
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.databinding.ComposeControllerBinding
|
||||||
import eu.kanade.tachiyomi.util.view.setComposeContent
|
import eu.kanade.tachiyomi.util.view.setComposeContent
|
||||||
import nucleus.presenter.Presenter
|
import nucleus.presenter.Presenter
|
||||||
|
@ -21,7 +23,9 @@ abstract class FullComposeController<P : Presenter<*>>(bundle: Bundle? = null) :
|
||||||
|
|
||||||
binding.root.apply {
|
binding.root.apply {
|
||||||
setComposeContent {
|
setComposeContent {
|
||||||
ComposeContent()
|
CompositionLocalProvider(LocalRouter provides router) {
|
||||||
|
ComposeContent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -52,7 +56,9 @@ abstract class BasicFullComposeController(bundle: Bundle? = null) :
|
||||||
|
|
||||||
binding.root.apply {
|
binding.root.apply {
|
||||||
setComposeContent {
|
setComposeContent {
|
||||||
ComposeContent()
|
CompositionLocalProvider(LocalRouter provides router) {
|
||||||
|
ComposeContent()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,30 +1,26 @@
|
||||||
package eu.kanade.tachiyomi.ui.history
|
package eu.kanade.tachiyomi.ui.history
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
import eu.kanade.presentation.history.HistoryScreen
|
import cafe.adriel.voyager.navigator.Navigator
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
|
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.RootController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.pushController
|
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
|
||||||
class HistoryController : FullComposeController<HistoryPresenter>(), RootController {
|
class HistoryController : BasicFullComposeController(), RootController {
|
||||||
|
|
||||||
override fun createPresenter() = HistoryPresenter()
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
override fun ComposeContent() {
|
override fun ComposeContent() {
|
||||||
HistoryScreen(
|
Navigator(screen = HistoryScreen)
|
||||||
presenter = presenter,
|
|
||||||
onClickCover = { history ->
|
|
||||||
router.pushController(MangaController(history.mangaId))
|
|
||||||
},
|
|
||||||
onClickResume = { history ->
|
|
||||||
presenter.getNextChapterForManga(history.mangaId, history.chapterId)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumeLastChapterRead() {
|
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
|
package eu.kanade.tachiyomi.ui.history
|
||||||
|
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Immutable
|
||||||
import androidx.compose.runtime.Stable
|
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import cafe.adriel.voyager.core.model.StateScreenModel
|
||||||
import androidx.compose.runtime.remember
|
import cafe.adriel.voyager.core.model.coroutineScope
|
||||||
import androidx.compose.runtime.setValue
|
import eu.kanade.core.prefs.asState
|
||||||
import eu.kanade.core.util.insertSeparators
|
import eu.kanade.core.util.insertSeparators
|
||||||
import eu.kanade.domain.base.BasePreferences
|
import eu.kanade.domain.base.BasePreferences
|
||||||
import eu.kanade.domain.chapter.model.Chapter
|
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.interactor.RemoveHistory
|
||||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
import eu.kanade.presentation.history.HistoryUiModel
|
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.launchIO
|
||||||
import eu.kanade.tachiyomi.util.lang.toDateKey
|
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.logcat
|
||||||
import eu.kanade.tachiyomi.util.system.toast
|
import kotlinx.coroutines.Dispatchers
|
||||||
import kotlinx.coroutines.channels.Channel
|
import kotlinx.coroutines.channels.Channel
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
import kotlinx.coroutines.flow.catch
|
import kotlinx.coroutines.flow.catch
|
||||||
import kotlinx.coroutines.flow.distinctUntilChanged
|
import kotlinx.coroutines.flow.distinctUntilChanged
|
||||||
|
import kotlinx.coroutines.flow.flatMapLatest
|
||||||
|
import kotlinx.coroutines.flow.flowOn
|
||||||
import kotlinx.coroutines.flow.map
|
import kotlinx.coroutines.flow.map
|
||||||
import kotlinx.coroutines.flow.receiveAsFlow
|
import kotlinx.coroutines.flow.receiveAsFlow
|
||||||
|
import kotlinx.coroutines.flow.update
|
||||||
|
import kotlinx.coroutines.launch
|
||||||
import logcat.LogPriority
|
import logcat.LogPriority
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
class HistoryPresenter(
|
class HistoryScreenModel(
|
||||||
private val state: HistoryStateImpl = HistoryState() as HistoryStateImpl,
|
|
||||||
private val getHistory: GetHistory = Injekt.get(),
|
private val getHistory: GetHistory = Injekt.get(),
|
||||||
private val getNextChapters: GetNextChapters = Injekt.get(),
|
private val getNextChapters: GetNextChapters = Injekt.get(),
|
||||||
private val removeHistory: RemoveHistory = Injekt.get(),
|
private val removeHistory: RemoveHistory = Injekt.get(),
|
||||||
preferences: BasePreferences = 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 events: Flow<Event> = _events.receiveAsFlow()
|
||||||
|
|
||||||
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState()
|
val isDownloadOnly: Boolean by preferences.downloadedOnly().asState(coroutineScope)
|
||||||
val isIncognitoMode: Boolean by preferences.incognitoMode().asState()
|
val isIncognitoMode: Boolean by preferences.incognitoMode().asState(coroutineScope)
|
||||||
|
|
||||||
@Composable
|
init {
|
||||||
fun getHistory(): Flow<List<HistoryUiModel>> {
|
coroutineScope.launch {
|
||||||
val query = searchQuery ?: ""
|
state.map { it.searchQuery }
|
||||||
return remember(query) {
|
|
||||||
getHistory.subscribe(query)
|
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.catch { error ->
|
.flatMapLatest { query ->
|
||||||
logcat(LogPriority.ERROR, error)
|
getHistory.subscribe(query ?: "")
|
||||||
_events.send(Event.InternalError)
|
.distinctUntilChanged()
|
||||||
}
|
.catch { error ->
|
||||||
.map { pagingData ->
|
logcat(LogPriority.ERROR, error)
|
||||||
pagingData.toHistoryUiModels()
|
_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) {
|
fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false))
|
sendNextChapterEvent(getNextChapters.await(mangaId, chapterId, onlyUnread = false))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun resumeLastChapterRead() {
|
|
||||||
presenterScope.launchIO {
|
|
||||||
sendNextChapterEvent(getNextChapters.await(onlyUnread = false))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private suspend fun sendNextChapterEvent(chapters: List<Chapter>) {
|
private suspend fun sendNextChapterEvent(chapters: List<Chapter>) {
|
||||||
val chapter = chapters.firstOrNull()
|
val chapter = chapters.firstOrNull()
|
||||||
_events.send(if (chapter != null) Event.OpenChapter(chapter) else Event.NoNextChapterFound)
|
_events.send(Event.OpenChapter(chapter))
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeFromHistory(history: HistoryWithRelations) {
|
fun removeFromHistory(history: HistoryWithRelations) {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
removeHistory.await(history)
|
removeHistory.await(history)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAllFromHistory(mangaId: Long) {
|
fun removeAllFromHistory(mangaId: Long) {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
removeHistory.await(mangaId)
|
removeHistory.await(mangaId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeAllHistory() {
|
fun removeAllHistory() {
|
||||||
presenterScope.launchIO {
|
coroutineScope.launchIO {
|
||||||
val result = removeHistory.awaitAll()
|
val result = removeHistory.awaitAll()
|
||||||
if (!result) return@launchIO
|
if (!result) return@launchIO
|
||||||
withUIContext {
|
_events.send(Event.HistoryCleared)
|
||||||
view?.activity?.toast(R.string.clear_history_completed)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun updateSearchQuery(query: String?) {
|
||||||
|
mutableState.update { it.copy(searchQuery = query) }
|
||||||
|
}
|
||||||
|
|
||||||
|
fun setDialog(dialog: Dialog?) {
|
||||||
|
mutableState.update { it.copy(dialog = dialog) }
|
||||||
|
}
|
||||||
|
|
||||||
sealed class Dialog {
|
sealed class Dialog {
|
||||||
object DeleteAll : Dialog()
|
object DeleteAll : Dialog()
|
||||||
data class Delete(val history: HistoryWithRelations) : Dialog()
|
data class Delete(val history: HistoryWithRelations) : Dialog()
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class Event {
|
sealed class Event {
|
||||||
|
data class OpenChapter(val chapter: Chapter?) : Event()
|
||||||
object InternalError : Event()
|
object InternalError : Event()
|
||||||
object NoNextChapterFound : Event()
|
object HistoryCleared : Event()
|
||||||
data class OpenChapter(val chapter: Chapter) : Event()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Stable
|
@Immutable
|
||||||
interface HistoryState {
|
data class HistoryState(
|
||||||
var searchQuery: String?
|
val searchQuery: String? = null,
|
||||||
var dialog: HistoryPresenter.Dialog?
|
val list: List<HistoryUiModel>? = null,
|
||||||
}
|
val dialog: HistoryScreenModel.Dialog? = null,
|
||||||
|
)
|
||||||
fun HistoryState(): HistoryState {
|
|
||||||
return HistoryStateImpl()
|
|
||||||
}
|
|
||||||
|
|
||||||
class HistoryStateImpl : HistoryState {
|
|
||||||
override var searchQuery: String? by mutableStateOf(null)
|
|
||||||
override var dialog: HistoryPresenter.Dialog? by mutableStateOf(null)
|
|
||||||
}
|
|
|
@ -9,6 +9,7 @@ import android.os.Parcelable
|
||||||
import android.util.AttributeSet
|
import android.util.AttributeSet
|
||||||
import android.view.ViewPropertyAnimator
|
import android.view.ViewPropertyAnimator
|
||||||
import androidx.compose.foundation.layout.PaddingValues
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.calculateEndPadding
|
import androidx.compose.foundation.layout.calculateEndPadding
|
||||||
import androidx.compose.foundation.layout.calculateStartPadding
|
import androidx.compose.foundation.layout.calculateStartPadding
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -16,6 +17,7 @@ import androidx.compose.runtime.ReadOnlyComposable
|
||||||
import androidx.compose.runtime.getValue
|
import androidx.compose.runtime.getValue
|
||||||
import androidx.compose.runtime.mutableStateOf
|
import androidx.compose.runtime.mutableStateOf
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.platform.LocalDensity
|
||||||
import androidx.compose.ui.platform.LocalLayoutDirection
|
import androidx.compose.ui.platform.LocalLayoutDirection
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.compose.ui.unit.max
|
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.R
|
||||||
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
import eu.kanade.tachiyomi.util.system.applySystemAnimatorScale
|
||||||
import eu.kanade.tachiyomi.util.system.pxToDp
|
import eu.kanade.tachiyomi.util.system.pxToDp
|
||||||
|
import kotlin.math.max
|
||||||
|
|
||||||
class TachiyomiBottomNavigationView @JvmOverloads constructor(
|
class TachiyomiBottomNavigationView @JvmOverloads constructor(
|
||||||
context: Context,
|
context: Context,
|
||||||
|
@ -173,5 +176,21 @@ class TachiyomiBottomNavigationView @JvmOverloads constructor(
|
||||||
bottom = max(origin.calculateBottomPadding(), bottomNavPadding),
|
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 a new issue