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,221 +1,90 @@
package eu.kanade.presentation.history package eu.kanade.presentation.history
import androidx.compose.animation.core.LinearEasing import androidx.compose.foundation.layout.safeContentPadding
import androidx.compose.animation.core.animateFloat import androidx.compose.material3.Scaffold
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.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState import androidx.compose.runtime.LaunchedEffect
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.Modifier
import androidx.compose.ui.geometry.Offset import androidx.compose.ui.platform.LocalContext
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.paging.LoadState 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.domain.history.model.HistoryWithRelations
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.RelativeDateHeader import eu.kanade.presentation.history.components.HistoryContent
import eu.kanade.presentation.components.ScrollbarLazyColumn import eu.kanade.presentation.history.components.HistoryDeleteAllDialog
import eu.kanade.presentation.history.components.HistoryItem import eu.kanade.presentation.history.components.HistoryDeleteDialog
import eu.kanade.presentation.history.components.HistoryItemShimmer import eu.kanade.presentation.history.components.HistoryToolbar
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.R 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.HistoryPresenter
import eu.kanade.tachiyomi.ui.recent.history.HistoryState import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter.Dialog
import uy.kohesive.injekt.Injekt import eu.kanade.tachiyomi.util.system.toast
import uy.kohesive.injekt.api.get import kotlinx.coroutines.flow.collectLatest
import java.text.DateFormat
import java.util.Date import java.util.Date
@Composable @Composable
fun HistoryScreen( fun HistoryScreen(
nestedScrollInterop: NestedScrollConnection,
presenter: HistoryPresenter, presenter: HistoryPresenter,
onClickCover: (HistoryWithRelations) -> Unit, onClickCover: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit, onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations, Boolean) -> Unit,
) { ) {
val state by presenter.state.collectAsState() val context = LocalContext.current
when (state) { Scaffold(
is HistoryState.Loading -> LoadingScreen() modifier = Modifier.safeContentPadding(),
is HistoryState.Error -> Text(text = (state as HistoryState.Error).error.message!!) topBar = {
is HistoryState.Success -> HistoryToolbar(state = presenter)
HistoryContent( },
nestedScroll = nestedScrollInterop, ) {
history = (state as HistoryState.Success).uiModels.collectAsLazyPagingItems(), 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, onClickCover = onClickCover,
onClickResume = onClickResume, onClickResume = onClickResume,
onClickDelete = onClickDelete, onClickDelete = { presenter.dialog = Dialog.Delete(it) },
) )
} }
} }
val onDismissRequest = { presenter.dialog = null }
@Composable when (val dialog = presenter.dialog) {
fun HistoryContent( is Dialog.Delete -> {
history: LazyPagingItems<HistoryUiModel>, HistoryDeleteDialog(
onClickCover: (HistoryWithRelations) -> Unit, onDismissRequest = onDismissRequest,
onClickResume: (HistoryWithRelations) -> Unit, onDelete = { all ->
onClickDelete: (HistoryWithRelations, Boolean) -> Unit, if (all) {
preferences: PreferencesHelper = Injekt.get(), presenter.removeAllFromHistory(dialog.history.mangaId)
nestedScroll: NestedScrollConnection, } else {
) { presenter.removeFromHistory(dialog.history)
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,
),
)
}
HistoryItemShimmer(brush = brush)
}
}
}
}
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))
} }
}, },
) )
} }
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)
}
}
}
}
}
sealed class HistoryUiModel { sealed class HistoryUiModel {
data class Header(val date: Date) : HistoryUiModel() data class Header(val date: Date) : 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 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.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.presentation.history.HistoryScreen
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.ui.base.controller.FullComposeController
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
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.ui.base.controller.pushController
import eu.kanade.tachiyomi.ui.manga.MangaController 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 { class HistoryController : FullComposeController<HistoryPresenter>(), RootController {
private var query = ""
override fun getTitle() = resources?.getString(R.string.label_recent_manga)
override fun createPresenter() = HistoryPresenter() override fun createPresenter() = HistoryPresenter()
@Composable @Composable
override fun ComposeContent(nestedScrollInterop: NestedScrollConnection) { override fun ComposeContent() {
HistoryScreen( HistoryScreen(
nestedScrollInterop = nestedScrollInterop,
presenter = presenter, presenter = presenter,
onClickCover = { history -> onClickCover = { history ->
router.pushController(MangaController(history.mangaId)) router.pushController(MangaController(history.mangaId))
@ -39,59 +21,9 @@ class HistoryController : ComposeController<HistoryPresenter>(), RootController
onClickResume = { history -> onClickResume = { history ->
presenter.getNextChapterForManga(history.mangaId, history.chapterId) 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() { fun resumeLastChapterRead() {
presenter.resumeLastChapterRead() presenter.resumeLastChapterRead()
} }

View File

@ -1,10 +1,19 @@
package eu.kanade.tachiyomi.ui.recent.history 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.PagingData
import androidx.paging.cachedIn import androidx.paging.cachedIn
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.insertSeparators import androidx.paging.insertSeparators
import androidx.paging.map import androidx.paging.map
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.history.interactor.DeleteHistoryTable import eu.kanade.domain.history.interactor.DeleteHistoryTable
import eu.kanade.domain.history.interactor.GetHistory import eu.kanade.domain.history.interactor.GetHistory
import eu.kanade.domain.history.interactor.GetNextChapter 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.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.toDateKey import eu.kanade.tachiyomi.util.lang.toDateKey
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.channels.Channel
import kotlinx.coroutines.flow.Flow 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.catch
import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.receiveAsFlow
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
/**
* Presenter of HistoryFragment.
* Contains information and data for fragment.
* Observable updates should be called from here.
*/
class HistoryPresenter( class HistoryPresenter(
private val state: HistoryStateImpl = HistoryState() as HistoryStateImpl,
private val getHistory: GetHistory = Injekt.get(), private val getHistory: GetHistory = Injekt.get(),
private val getNextChapter: GetNextChapter = Injekt.get(), private val getNextChapter: GetNextChapter = Injekt.get(),
private val deleteHistoryTable: DeleteHistoryTable = Injekt.get(), private val deleteHistoryTable: DeleteHistoryTable = Injekt.get(),
private val removeHistoryById: RemoveHistoryById = Injekt.get(), private val removeHistoryById: RemoveHistoryById = Injekt.get(),
private val removeHistoryByMangaId: RemoveHistoryByMangaId = Injekt.get(), private val removeHistoryByMangaId: RemoveHistoryByMangaId = Injekt.get(),
) : BasePresenter<HistoryController>() { ) : BasePresenter<HistoryController>(), HistoryState by state {
private val _query: MutableStateFlow<String> = MutableStateFlow("") private val _events: Channel<Event> = Channel(Int.MAX_VALUE)
private val _state: MutableStateFlow<HistoryState> = MutableStateFlow(HistoryState.Loading) val events: Flow<Event> = _events.receiveAsFlow()
val state: StateFlow<HistoryState> = _state.asStateFlow()
override fun onCreate(savedState: Bundle?) { @Composable
super.onCreate(savedState) fun getLazyHistory(): LazyPagingItems<HistoryUiModel> {
val scope = rememberCoroutineScope()
presenterScope.launchIO { val query = searchQuery ?: ""
_query.collectLatest { query -> val flow = remember(query) {
getHistory.subscribe(query) getHistory.subscribe(query)
.catch { exception -> .catch { error ->
_state.value = HistoryState.Error(exception) logcat(LogPriority.ERROR, error)
_events.send(Event.InternalError)
} }
.map { pagingData -> .map { pagingData ->
pagingData.toHistoryUiModels() pagingData.toHistoryUiModels()
} }
.cachedIn(presenterScope) .cachedIn(scope)
.let { uiModelsPagingDataFlow ->
_state.value = HistoryState.Success(uiModelsPagingDataFlow)
}
}
} }
return flow.collectAsLazyPagingItems()
} }
private fun PagingData<HistoryWithRelations>.toHistoryUiModels(): PagingData<HistoryUiModel> { 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) { fun removeFromHistory(history: HistoryWithRelations) {
presenterScope.launchIO { presenterScope.launchIO {
removeHistoryById.await(history) removeHistoryById.await(history)
@ -102,9 +98,7 @@ class HistoryPresenter(
fun getNextChapterForManga(mangaId: Long, chapterId: Long) { fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
presenterScope.launchIO { presenterScope.launchIO {
val chapter = getNextChapter.await(mangaId, chapterId) val chapter = getNextChapter.await(mangaId, chapterId)
launchUI { _events.send(if (chapter != null) Event.OpenChapter(chapter) else Event.NoNextChapterFound)
view?.openChapter(chapter)
}
} }
} }
@ -121,15 +115,33 @@ class HistoryPresenter(
fun resumeLastChapterRead() { fun resumeLastChapterRead() {
presenterScope.launchIO { presenterScope.launchIO {
val chapter = getNextChapter.await() val chapter = getNextChapter.await()
launchUI { _events.send(if (chapter != null) Event.OpenChapter(chapter) else Event.NoNextChapterFound)
view?.openChapter(chapter)
}
}
} }
} }
sealed class HistoryState { sealed class Dialog {
object Loading : HistoryState() object DeleteAll : Dialog()
data class Error(val error: Throwable) : HistoryState() data class Delete(val history: HistoryWithRelations) : Dialog()
data class Success(val uiModels: Flow<PagingData<HistoryUiModel>>) : HistoryState() }
sealed class Event {
object InternalError : Event()
object NoNextChapterFound : Event()
data class OpenChapter(val chapter: Chapter) : 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)
} }

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_next_chapter">Next chapter</string>
<string name="action_retry">Retry</string> <string name="action_retry">Retry</string>
<string name="action_remove">Remove</string> <string name="action_remove">Remove</string>
<string name="action_remove_everything">Remove everything</string>
<string name="action_start">Start</string> <string name="action_start">Start</string>
<string name="action_resume">Resume</string> <string name="action_resume">Resume</string>
<string name="action_open_in_browser">Open in browser</string> <string name="action_open_in_browser">Open in browser</string>