mirror of
https://git.mihon.tech/mihonapp/mihon
synced 2024-11-27 09:44:55 +03:00
Use Stable interface for History screen (#7586)
- Adds Stable interface - Move last Dialog into Compose - Make History screen be full Compose screen
This commit is contained in:
parent
9f2ddaadde
commit
c751851941
9 changed files with 416 additions and 349 deletions
|
@ -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()
|
||||||
|
|
|
@ -0,0 +1,95 @@
|
||||||
|
package eu.kanade.presentation.history.components
|
||||||
|
|
||||||
|
import androidx.compose.animation.core.LinearEasing
|
||||||
|
import androidx.compose.animation.core.animateFloat
|
||||||
|
import androidx.compose.animation.core.infiniteRepeatable
|
||||||
|
import androidx.compose.animation.core.rememberInfiniteTransition
|
||||||
|
import androidx.compose.animation.core.tween
|
||||||
|
import androidx.compose.foundation.layout.PaddingValues
|
||||||
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.geometry.Offset
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
import androidx.paging.compose.items
|
||||||
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
|
import eu.kanade.presentation.components.RelativeDateHeader
|
||||||
|
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||||
|
import eu.kanade.presentation.history.HistoryUiModel
|
||||||
|
import eu.kanade.presentation.util.bottomNavPaddingValues
|
||||||
|
import eu.kanade.presentation.util.plus
|
||||||
|
import eu.kanade.presentation.util.shimmerGradient
|
||||||
|
import eu.kanade.presentation.util.topPaddingValues
|
||||||
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
|
import uy.kohesive.injekt.Injekt
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import java.text.DateFormat
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HistoryContent(
|
||||||
|
history: LazyPagingItems<HistoryUiModel>,
|
||||||
|
contentPadding: PaddingValues,
|
||||||
|
onClickCover: (HistoryWithRelations) -> Unit,
|
||||||
|
onClickResume: (HistoryWithRelations) -> Unit,
|
||||||
|
onClickDelete: (HistoryWithRelations) -> Unit,
|
||||||
|
preferences: PreferencesHelper = Injekt.get(),
|
||||||
|
) {
|
||||||
|
val relativeTime: Int = remember { preferences.relativeTime().get() }
|
||||||
|
val dateFormat: DateFormat = remember { preferences.dateFormat() }
|
||||||
|
|
||||||
|
ScrollbarLazyColumn(
|
||||||
|
contentPadding = contentPadding + bottomNavPaddingValues + topPaddingValues,
|
||||||
|
state = rememberLazyListState(),
|
||||||
|
) {
|
||||||
|
items(history) { item ->
|
||||||
|
when (item) {
|
||||||
|
is HistoryUiModel.Header -> {
|
||||||
|
RelativeDateHeader(
|
||||||
|
modifier = Modifier
|
||||||
|
.animateItemPlacement(),
|
||||||
|
date = item.date,
|
||||||
|
relativeTime = relativeTime,
|
||||||
|
dateFormat = dateFormat,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
is HistoryUiModel.Item -> {
|
||||||
|
val value = item.item
|
||||||
|
HistoryItem(
|
||||||
|
modifier = Modifier.animateItemPlacement(),
|
||||||
|
history = value,
|
||||||
|
onClickCover = { onClickCover(value) },
|
||||||
|
onClickResume = { onClickResume(value) },
|
||||||
|
onClickDelete = { onClickDelete(value) },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
null -> {
|
||||||
|
val transition = rememberInfiniteTransition()
|
||||||
|
val translateAnimation = transition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 1000f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 1000,
|
||||||
|
easing = LinearEasing,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val brush = remember {
|
||||||
|
Brush.linearGradient(
|
||||||
|
colors = shimmerGradient,
|
||||||
|
start = Offset(0f, 0f),
|
||||||
|
end = Offset(
|
||||||
|
x = translateAnimation.value,
|
||||||
|
y = 00f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
HistoryItemShimmer(brush = brush)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,103 @@
|
||||||
|
package eu.kanade.presentation.history.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.interaction.MutableInteractionSource
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.selection.toggleable
|
||||||
|
import androidx.compose.material3.AlertDialog
|
||||||
|
import androidx.compose.material3.Checkbox
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.material3.TextButton
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.getValue
|
||||||
|
import androidx.compose.runtime.mutableStateOf
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.runtime.setValue
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HistoryDeleteDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onDelete: (Boolean) -> Unit,
|
||||||
|
) {
|
||||||
|
var removeEverything by remember { mutableStateOf(false) }
|
||||||
|
|
||||||
|
AlertDialog(
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.action_remove))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Column {
|
||||||
|
Text(text = stringResource(R.string.dialog_with_checkbox_remove_description))
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
.toggleable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
value = removeEverything,
|
||||||
|
onValueChange = { removeEverything = it },
|
||||||
|
),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Checkbox(
|
||||||
|
checked = removeEverything,
|
||||||
|
onCheckedChange = null,
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
|
text = stringResource(R.string.dialog_with_checkbox_reset),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onDelete(removeEverything)
|
||||||
|
onDismissRequest()
|
||||||
|
},) {
|
||||||
|
Text(text = stringResource(R.string.action_remove))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HistoryDeleteAllDialog(
|
||||||
|
onDismissRequest: () -> Unit,
|
||||||
|
onDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
AlertDialog(
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(R.string.action_remove_everything))
|
||||||
|
},
|
||||||
|
text = {
|
||||||
|
Text(text = stringResource(R.string.clear_history_confirmation))
|
||||||
|
},
|
||||||
|
onDismissRequest = onDismissRequest,
|
||||||
|
confirmButton = {
|
||||||
|
TextButton(onClick = {
|
||||||
|
onDelete()
|
||||||
|
onDismissRequest()
|
||||||
|
},) {
|
||||||
|
Text(text = stringResource(android.R.string.ok))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
dismissButton = {
|
||||||
|
TextButton(onClick = onDismissRequest) {
|
||||||
|
Text(text = stringResource(android.R.string.cancel))
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
package eu.kanade.presentation.history.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.text.BasicTextField
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.ArrowBack
|
||||||
|
import androidx.compose.material.icons.outlined.DeleteSweep
|
||||||
|
import androidx.compose.material.icons.outlined.Search
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.SmallTopAppBar
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.runtime.LaunchedEffect
|
||||||
|
import androidx.compose.runtime.remember
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.focus.FocusRequester
|
||||||
|
import androidx.compose.ui.focus.focusRequester
|
||||||
|
import androidx.compose.ui.graphics.SolidColor
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter
|
||||||
|
import eu.kanade.tachiyomi.ui.recent.history.HistoryState
|
||||||
|
import kotlinx.coroutines.delay
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HistoryToolbar(
|
||||||
|
state: HistoryState,
|
||||||
|
) {
|
||||||
|
if (state.searchQuery == null) {
|
||||||
|
HistoryRegularToolbar(
|
||||||
|
onClickSearch = { state.searchQuery = "" },
|
||||||
|
onClickDelete = { state.dialog = HistoryPresenter.Dialog.DeleteAll },
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
HistorySearchToolbar(
|
||||||
|
searchQuery = state.searchQuery!!,
|
||||||
|
onChangeSearchQuery = { state.searchQuery = it },
|
||||||
|
onClickCloseSearch = { state.searchQuery = null },
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HistoryRegularToolbar(
|
||||||
|
onClickSearch: () -> Unit,
|
||||||
|
onClickDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
SmallTopAppBar(
|
||||||
|
title = {
|
||||||
|
Text(text = stringResource(id = R.string.history))
|
||||||
|
},
|
||||||
|
actions = {
|
||||||
|
IconButton(onClick = onClickSearch) {
|
||||||
|
Icon(Icons.Outlined.Search, contentDescription = "search")
|
||||||
|
}
|
||||||
|
IconButton(onClick = onClickDelete) {
|
||||||
|
Icon(Icons.Outlined.DeleteSweep, contentDescription = "delete")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HistorySearchToolbar(
|
||||||
|
searchQuery: String,
|
||||||
|
onChangeSearchQuery: (String) -> Unit,
|
||||||
|
onClickCloseSearch: () -> Unit,
|
||||||
|
) {
|
||||||
|
val focusRequester = remember { FocusRequester.Default }
|
||||||
|
SmallTopAppBar(
|
||||||
|
navigationIcon = {
|
||||||
|
IconButton(onClick = onClickCloseSearch) {
|
||||||
|
Icon(Icons.Outlined.ArrowBack, contentDescription = "delete")
|
||||||
|
}
|
||||||
|
},
|
||||||
|
title = {
|
||||||
|
BasicTextField(
|
||||||
|
value = searchQuery,
|
||||||
|
onValueChange = onChangeSearchQuery,
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxWidth()
|
||||||
|
.focusRequester(focusRequester),
|
||||||
|
textStyle = MaterialTheme.typography.bodyMedium.copy(color = MaterialTheme.colorScheme.onBackground),
|
||||||
|
singleLine = true,
|
||||||
|
cursorBrush = SolidColor(MaterialTheme.colorScheme.onBackground),
|
||||||
|
)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
LaunchedEffect(focusRequester) {
|
||||||
|
// TODO: https://issuetracker.google.com/issues/204502668
|
||||||
|
delay(100)
|
||||||
|
focusRequester.requestFocus()
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,21 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.ui.recent.history
|
|
||||||
|
|
||||||
import android.app.Dialog
|
|
||||||
import android.os.Bundle
|
|
||||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
|
||||||
import eu.kanade.tachiyomi.R
|
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
|
||||||
|
|
||||||
class ClearHistoryDialogController : DialogController() {
|
|
||||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
|
||||||
return MaterialAlertDialogBuilder(activity!!)
|
|
||||||
.setMessage(R.string.clear_history_confirmation)
|
|
||||||
.setPositiveButton(android.R.string.ok) { _, _ ->
|
|
||||||
(targetController as? HistoryController)
|
|
||||||
?.presenter
|
|
||||||
?.deleteAllHistory()
|
|
||||||
}
|
|
||||||
.setNegativeButton(android.R.string.cancel, null)
|
|
||||||
.create()
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,37 +1,19 @@
|
||||||
package eu.kanade.tachiyomi.ui.recent.history
|
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()
|
||||||
}
|
}
|
||||||
|
|
|
@ -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)
|
||||||
}
|
}
|
||||||
|
|
|
@ -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>
|
|
|
@ -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>
|
||||||
|
|
Loading…
Reference in a new issue