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:
Andreas 2022-07-23 16:01:51 +02:00 committed by GitHub
parent 9f2ddaadde
commit c751851941
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 416 additions and 349 deletions

View file

@ -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 {

View file

@ -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)
}
}
}
}
}

View file

@ -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))
}
},
)
}

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -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()
}

View file

@ -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)
}

View file

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

View file

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