Use custom QueryPagingSource (#7321)
* Use custom QueryPagingSource - Adds placeholder to make the list jump around less - Fixes issue where SQLDelight QueryPagingSource would throw IndexOutOfBounds * Review Changes
This commit is contained in:
parent
4c3af7bf36
commit
3fd9e021fa
9 changed files with 301 additions and 119 deletions
|
@ -2,8 +2,6 @@ package eu.kanade.data
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import com.squareup.sqldelight.Query
|
import com.squareup.sqldelight.Query
|
||||||
import com.squareup.sqldelight.Transacter
|
|
||||||
import com.squareup.sqldelight.android.paging3.QueryPagingSource
|
|
||||||
import com.squareup.sqldelight.db.SqlDriver
|
import com.squareup.sqldelight.db.SqlDriver
|
||||||
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
||||||
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
||||||
|
@ -63,13 +61,11 @@ class AndroidDatabaseHandler(
|
||||||
|
|
||||||
override fun <T : Any> subscribeToPagingSource(
|
override fun <T : Any> subscribeToPagingSource(
|
||||||
countQuery: Database.() -> Query<Long>,
|
countQuery: Database.() -> Query<Long>,
|
||||||
transacter: Database.() -> Transacter,
|
|
||||||
queryProvider: Database.(Long, Long) -> Query<T>,
|
queryProvider: Database.(Long, Long) -> Query<T>,
|
||||||
): PagingSource<Long, T> {
|
): PagingSource<Long, T> {
|
||||||
return QueryPagingSource(
|
return QueryPagingSource(
|
||||||
countQuery = countQuery(db),
|
handler = this,
|
||||||
transacter = transacter(db),
|
countQuery = countQuery,
|
||||||
dispatcher = queryDispatcher,
|
|
||||||
queryProvider = { limit, offset ->
|
queryProvider = { limit, offset ->
|
||||||
queryProvider.invoke(db, limit, offset)
|
queryProvider.invoke(db, limit, offset)
|
||||||
},
|
},
|
||||||
|
|
|
@ -2,7 +2,6 @@ package eu.kanade.data
|
||||||
|
|
||||||
import androidx.paging.PagingSource
|
import androidx.paging.PagingSource
|
||||||
import com.squareup.sqldelight.Query
|
import com.squareup.sqldelight.Query
|
||||||
import com.squareup.sqldelight.Transacter
|
|
||||||
import eu.kanade.tachiyomi.Database
|
import eu.kanade.tachiyomi.Database
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
@ -33,7 +32,6 @@ interface DatabaseHandler {
|
||||||
|
|
||||||
fun <T : Any> subscribeToPagingSource(
|
fun <T : Any> subscribeToPagingSource(
|
||||||
countQuery: Database.() -> Query<Long>,
|
countQuery: Database.() -> Query<Long>,
|
||||||
transacter: Database.() -> Transacter,
|
|
||||||
queryProvider: Database.(Long, Long) -> Query<T>,
|
queryProvider: Database.(Long, Long) -> Query<T>,
|
||||||
): PagingSource<Long, T>
|
): PagingSource<Long, T>
|
||||||
}
|
}
|
||||||
|
|
72
app/src/main/java/eu/kanade/data/QueryPagingSource.kt
Normal file
72
app/src/main/java/eu/kanade/data/QueryPagingSource.kt
Normal file
|
@ -0,0 +1,72 @@
|
||||||
|
package eu.kanade.data
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import androidx.paging.PagingState
|
||||||
|
import com.squareup.sqldelight.Query
|
||||||
|
import eu.kanade.tachiyomi.Database
|
||||||
|
import kotlin.properties.Delegates
|
||||||
|
|
||||||
|
class QueryPagingSource<RowType : Any>(
|
||||||
|
val handler: DatabaseHandler,
|
||||||
|
val countQuery: Database.() -> Query<Long>,
|
||||||
|
val queryProvider: Database.(Long, Long) -> Query<RowType>,
|
||||||
|
) : PagingSource<Long, RowType>(), Query.Listener {
|
||||||
|
|
||||||
|
override val jumpingSupported: Boolean = true
|
||||||
|
|
||||||
|
private var currentQuery: Query<RowType>? by Delegates.observable(null) { _, old, new ->
|
||||||
|
old?.removeListener(this)
|
||||||
|
new?.addListener(this)
|
||||||
|
}
|
||||||
|
|
||||||
|
init {
|
||||||
|
registerInvalidatedCallback {
|
||||||
|
currentQuery?.removeListener(this)
|
||||||
|
currentQuery = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun load(params: LoadParams<Long>): LoadResult<Long, RowType> {
|
||||||
|
try {
|
||||||
|
val key = params.key ?: 0L
|
||||||
|
val loadSize = params.loadSize
|
||||||
|
val count = handler.awaitOne { countQuery() }
|
||||||
|
|
||||||
|
val (offset, limit) = when (params) {
|
||||||
|
is LoadParams.Prepend -> key - loadSize to loadSize.toLong()
|
||||||
|
else -> key to loadSize.toLong()
|
||||||
|
}
|
||||||
|
|
||||||
|
val data = handler.awaitList {
|
||||||
|
queryProvider(limit, offset)
|
||||||
|
.also { currentQuery = it }
|
||||||
|
}
|
||||||
|
|
||||||
|
val (prevKey, nextKey) = when (params) {
|
||||||
|
is LoadParams.Append -> { offset - loadSize to offset + loadSize }
|
||||||
|
else -> { offset to offset + loadSize }
|
||||||
|
}
|
||||||
|
|
||||||
|
return LoadResult.Page(
|
||||||
|
data = data,
|
||||||
|
prevKey = if (offset <= 0L || prevKey < 0L) null else prevKey,
|
||||||
|
nextKey = if (offset + loadSize >= count) null else nextKey,
|
||||||
|
itemsBefore = maxOf(0L, offset).toInt(),
|
||||||
|
itemsAfter = maxOf(0L, count - (offset + loadSize)).toInt(),
|
||||||
|
)
|
||||||
|
} catch (e: Exception) {
|
||||||
|
return LoadResult.Error(throwable = e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun getRefreshKey(state: PagingState<Long, RowType>): Long? {
|
||||||
|
return state.anchorPosition?.let { anchorPosition ->
|
||||||
|
val anchorPage = state.closestPageToPosition(anchorPosition)
|
||||||
|
anchorPage?.prevKey ?: anchorPage?.nextKey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun queryResultsChanged() {
|
||||||
|
invalidate()
|
||||||
|
}
|
||||||
|
}
|
|
@ -19,7 +19,6 @@ class HistoryRepositoryImpl(
|
||||||
override fun getHistory(query: String): PagingSource<Long, HistoryWithRelations> {
|
override fun getHistory(query: String): PagingSource<Long, HistoryWithRelations> {
|
||||||
return handler.subscribeToPagingSource(
|
return handler.subscribeToPagingSource(
|
||||||
countQuery = { historyViewQueries.countHistory(query) },
|
countQuery = { historyViewQueries.countHistory(query) },
|
||||||
transacter = { historyViewQueries },
|
|
||||||
queryProvider = { limit, offset ->
|
queryProvider = { limit, offset ->
|
||||||
historyViewQueries.history(query, limit, offset, historyWithRelationsMapper)
|
historyViewQueries.history(query, limit, offset, historyWithRelationsMapper)
|
||||||
},
|
},
|
||||||
|
|
|
@ -14,7 +14,7 @@ import coil.compose.AsyncImage
|
||||||
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
import eu.kanade.presentation.util.rememberResourceBitmapPainter
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
|
|
||||||
enum class MangaCover(private val ratio: Float) {
|
enum class MangaCover(val ratio: Float) {
|
||||||
Square(1f / 1f),
|
Square(1f / 1f),
|
||||||
Book(2f / 3f);
|
Book(2f / 3f);
|
||||||
|
|
||||||
|
|
|
@ -1,24 +1,21 @@
|
||||||
package eu.kanade.presentation.history
|
package eu.kanade.presentation.history
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
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.interaction.MutableInteractionSource
|
||||||
import androidx.compose.foundation.layout.Column
|
import androidx.compose.foundation.layout.Column
|
||||||
import androidx.compose.foundation.layout.Row
|
import androidx.compose.foundation.layout.Row
|
||||||
import androidx.compose.foundation.layout.WindowInsets
|
import androidx.compose.foundation.layout.WindowInsets
|
||||||
import androidx.compose.foundation.layout.asPaddingValues
|
import androidx.compose.foundation.layout.asPaddingValues
|
||||||
import androidx.compose.foundation.layout.fillMaxHeight
|
|
||||||
import androidx.compose.foundation.layout.height
|
|
||||||
import androidx.compose.foundation.layout.navigationBars
|
import androidx.compose.foundation.layout.navigationBars
|
||||||
import androidx.compose.foundation.layout.padding
|
import androidx.compose.foundation.layout.padding
|
||||||
import androidx.compose.foundation.lazy.rememberLazyListState
|
import androidx.compose.foundation.lazy.rememberLazyListState
|
||||||
import androidx.compose.foundation.selection.toggleable
|
import androidx.compose.foundation.selection.toggleable
|
||||||
import androidx.compose.material.icons.Icons
|
|
||||||
import androidx.compose.material.icons.outlined.Delete
|
|
||||||
import androidx.compose.material3.AlertDialog
|
import androidx.compose.material3.AlertDialog
|
||||||
import androidx.compose.material3.Checkbox
|
import androidx.compose.material3.Checkbox
|
||||||
import androidx.compose.material3.Icon
|
|
||||||
import androidx.compose.material3.IconButton
|
|
||||||
import androidx.compose.material3.MaterialTheme
|
|
||||||
import androidx.compose.material3.Text
|
import androidx.compose.material3.Text
|
||||||
import androidx.compose.material3.TextButton
|
import androidx.compose.material3.TextButton
|
||||||
import androidx.compose.runtime.Composable
|
import androidx.compose.runtime.Composable
|
||||||
|
@ -29,12 +26,11 @@ import androidx.compose.runtime.remember
|
||||||
import androidx.compose.runtime.setValue
|
import androidx.compose.runtime.setValue
|
||||||
import androidx.compose.ui.Alignment
|
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.graphics.Brush.Companion.linearGradient
|
||||||
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
|
||||||
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
import androidx.compose.ui.input.nestedscroll.nestedScroll
|
||||||
import androidx.compose.ui.platform.LocalContext
|
|
||||||
import androidx.compose.ui.res.stringResource
|
import androidx.compose.ui.res.stringResource
|
||||||
import androidx.compose.ui.text.font.FontWeight
|
|
||||||
import androidx.compose.ui.text.style.TextOverflow
|
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import androidx.paging.LoadState
|
import androidx.paging.LoadState
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
|
@ -43,22 +39,20 @@ 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.MangaCover
|
|
||||||
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
import eu.kanade.presentation.components.ScrollbarLazyColumn
|
||||||
import eu.kanade.presentation.util.horizontalPadding
|
import eu.kanade.presentation.history.components.HistoryHeader
|
||||||
|
import eu.kanade.presentation.history.components.HistoryItem
|
||||||
|
import eu.kanade.presentation.history.components.HistoryItemShimmer
|
||||||
import eu.kanade.presentation.util.plus
|
import eu.kanade.presentation.util.plus
|
||||||
|
import eu.kanade.presentation.util.shimmerGradient
|
||||||
import eu.kanade.presentation.util.topPaddingValues
|
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.data.preference.PreferencesHelper
|
||||||
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.HistoryState
|
||||||
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
|
||||||
import eu.kanade.tachiyomi.util.lang.toTimestampString
|
|
||||||
import uy.kohesive.injekt.Injekt
|
import uy.kohesive.injekt.Injekt
|
||||||
import uy.kohesive.injekt.api.get
|
import uy.kohesive.injekt.api.get
|
||||||
import java.text.DateFormat
|
import java.text.DateFormat
|
||||||
import java.text.DecimalFormat
|
|
||||||
import java.text.DecimalFormatSymbols
|
|
||||||
import java.util.Date
|
import java.util.Date
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
|
@ -93,10 +87,7 @@ fun HistoryContent(
|
||||||
preferences: PreferencesHelper = Injekt.get(),
|
preferences: PreferencesHelper = Injekt.get(),
|
||||||
nestedScroll: NestedScrollConnection,
|
nestedScroll: NestedScrollConnection,
|
||||||
) {
|
) {
|
||||||
if (history.loadState.refresh is LoadState.Loading) {
|
if (history.loadState.refresh is LoadState.NotLoading && history.itemCount == 0) {
|
||||||
LoadingScreen()
|
|
||||||
return
|
|
||||||
} else if (history.loadState.refresh is LoadState.NotLoading && history.itemCount == 0) {
|
|
||||||
EmptyScreen(textResource = R.string.information_no_recent_manga)
|
EmptyScreen(textResource = R.string.information_no_recent_manga)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -107,6 +98,29 @@ fun HistoryContent(
|
||||||
var removeState by remember { mutableStateOf<HistoryWithRelations?>(null) }
|
var removeState by remember { mutableStateOf<HistoryWithRelations?>(null) }
|
||||||
|
|
||||||
val scrollState = rememberLazyListState()
|
val scrollState = rememberLazyListState()
|
||||||
|
|
||||||
|
val transition = rememberInfiniteTransition()
|
||||||
|
|
||||||
|
val translateAnimation = transition.animateFloat(
|
||||||
|
initialValue = 0f,
|
||||||
|
targetValue = 1000f,
|
||||||
|
animationSpec = infiniteRepeatable(
|
||||||
|
animation = tween(
|
||||||
|
durationMillis = 1000,
|
||||||
|
easing = LinearEasing,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
val brush = linearGradient(
|
||||||
|
colors = shimmerGradient,
|
||||||
|
start = Offset(0f, 0f),
|
||||||
|
end = Offset(
|
||||||
|
x = translateAnimation.value,
|
||||||
|
y = 00f,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
ScrollbarLazyColumn(
|
ScrollbarLazyColumn(
|
||||||
modifier = Modifier
|
modifier = Modifier
|
||||||
.nestedScroll(nestedScroll),
|
.nestedScroll(nestedScroll),
|
||||||
|
@ -134,7 +148,9 @@ fun HistoryContent(
|
||||||
onClickDelete = { removeState = value },
|
onClickDelete = { removeState = value },
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
null -> {}
|
null -> {
|
||||||
|
HistoryItemShimmer(brush = brush)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -150,88 +166,6 @@ fun HistoryContent(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HistoryHeader(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
date: Date,
|
|
||||||
relativeTime: Int,
|
|
||||||
dateFormat: DateFormat,
|
|
||||||
) {
|
|
||||||
Text(
|
|
||||||
modifier = modifier
|
|
||||||
.padding(horizontal = horizontalPadding, vertical = 8.dp),
|
|
||||||
text = date.toRelativeString(
|
|
||||||
LocalContext.current,
|
|
||||||
relativeTime,
|
|
||||||
dateFormat,
|
|
||||||
),
|
|
||||||
style = MaterialTheme.typography.bodyMedium.copy(
|
|
||||||
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
|
||||||
fontWeight = FontWeight.SemiBold,
|
|
||||||
),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
|
||||||
fun HistoryItem(
|
|
||||||
modifier: Modifier = Modifier,
|
|
||||||
history: HistoryWithRelations,
|
|
||||||
onClickCover: () -> Unit,
|
|
||||||
onClickResume: () -> Unit,
|
|
||||||
onClickDelete: () -> Unit,
|
|
||||||
) {
|
|
||||||
Row(
|
|
||||||
modifier = modifier
|
|
||||||
.clickable(onClick = onClickResume)
|
|
||||||
.height(96.dp)
|
|
||||||
.padding(horizontal = horizontalPadding, vertical = 8.dp),
|
|
||||||
verticalAlignment = Alignment.CenterVertically,
|
|
||||||
) {
|
|
||||||
MangaCover.Book(
|
|
||||||
modifier = Modifier
|
|
||||||
.fillMaxHeight()
|
|
||||||
.clickable(onClick = onClickCover),
|
|
||||||
data = history.coverData,
|
|
||||||
)
|
|
||||||
Column(
|
|
||||||
modifier = Modifier
|
|
||||||
.weight(1f)
|
|
||||||
.padding(start = horizontalPadding, end = 8.dp),
|
|
||||||
) {
|
|
||||||
val textStyle = MaterialTheme.typography.bodyMedium
|
|
||||||
Text(
|
|
||||||
text = history.title,
|
|
||||||
maxLines = 2,
|
|
||||||
overflow = TextOverflow.Ellipsis,
|
|
||||||
style = textStyle.copy(fontWeight = FontWeight.SemiBold),
|
|
||||||
)
|
|
||||||
Row {
|
|
||||||
Text(
|
|
||||||
text = if (history.chapterNumber > -1) {
|
|
||||||
stringResource(
|
|
||||||
R.string.recent_manga_time,
|
|
||||||
chapterFormatter.format(history.chapterNumber),
|
|
||||||
history.readAt?.toTimestampString() ?: "",
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
history.readAt?.toTimestampString() ?: ""
|
|
||||||
},
|
|
||||||
modifier = Modifier.padding(top = 4.dp),
|
|
||||||
style = textStyle,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
IconButton(onClick = onClickDelete) {
|
|
||||||
Icon(
|
|
||||||
imageVector = Icons.Outlined.Delete,
|
|
||||||
contentDescription = stringResource(R.string.action_delete),
|
|
||||||
tint = MaterialTheme.colorScheme.onSurface,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Composable
|
@Composable
|
||||||
fun RemoveHistoryDialog(
|
fun RemoveHistoryDialog(
|
||||||
onPositive: (Boolean) -> Unit,
|
onPositive: (Boolean) -> Unit,
|
||||||
|
@ -282,11 +216,6 @@ fun RemoveHistoryDialog(
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
private val chapterFormatter = DecimalFormat(
|
|
||||||
"#.###",
|
|
||||||
DecimalFormatSymbols().apply { decimalSeparator = '.' },
|
|
||||||
)
|
|
||||||
|
|
||||||
sealed class HistoryUiModel {
|
sealed class HistoryUiModel {
|
||||||
data class Header(val date: Date) : HistoryUiModel()
|
data class Header(val date: Date) : HistoryUiModel()
|
||||||
data class Item(val item: HistoryWithRelations) : HistoryUiModel()
|
data class Item(val item: HistoryWithRelations) : HistoryUiModel()
|
||||||
|
|
|
@ -0,0 +1,36 @@
|
||||||
|
package eu.kanade.presentation.history.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.platform.LocalContext
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.presentation.util.horizontalPadding
|
||||||
|
import eu.kanade.tachiyomi.util.lang.toRelativeString
|
||||||
|
import java.text.DateFormat
|
||||||
|
import java.util.Date
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HistoryHeader(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
date: Date,
|
||||||
|
relativeTime: Int,
|
||||||
|
dateFormat: DateFormat,
|
||||||
|
) {
|
||||||
|
Text(
|
||||||
|
modifier = modifier
|
||||||
|
.padding(horizontal = horizontalPadding, vertical = 8.dp),
|
||||||
|
text = date.toRelativeString(
|
||||||
|
LocalContext.current,
|
||||||
|
relativeTime,
|
||||||
|
dateFormat,
|
||||||
|
),
|
||||||
|
style = MaterialTheme.typography.bodyMedium.copy(
|
||||||
|
color = MaterialTheme.colorScheme.onSurfaceVariant,
|
||||||
|
fontWeight = FontWeight.SemiBold,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,143 @@
|
||||||
|
package eu.kanade.presentation.history.components
|
||||||
|
|
||||||
|
import androidx.compose.foundation.clickable
|
||||||
|
import androidx.compose.foundation.layout.Box
|
||||||
|
import androidx.compose.foundation.layout.Column
|
||||||
|
import androidx.compose.foundation.layout.Row
|
||||||
|
import androidx.compose.foundation.layout.aspectRatio
|
||||||
|
import androidx.compose.foundation.layout.fillMaxHeight
|
||||||
|
import androidx.compose.foundation.layout.fillMaxWidth
|
||||||
|
import androidx.compose.foundation.layout.height
|
||||||
|
import androidx.compose.foundation.layout.padding
|
||||||
|
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||||
|
import androidx.compose.material.icons.Icons
|
||||||
|
import androidx.compose.material.icons.outlined.Delete
|
||||||
|
import androidx.compose.material3.Icon
|
||||||
|
import androidx.compose.material3.IconButton
|
||||||
|
import androidx.compose.material3.MaterialTheme
|
||||||
|
import androidx.compose.material3.Text
|
||||||
|
import androidx.compose.runtime.Composable
|
||||||
|
import androidx.compose.ui.Alignment
|
||||||
|
import androidx.compose.ui.Modifier
|
||||||
|
import androidx.compose.ui.draw.clip
|
||||||
|
import androidx.compose.ui.draw.drawBehind
|
||||||
|
import androidx.compose.ui.graphics.Brush
|
||||||
|
import androidx.compose.ui.res.stringResource
|
||||||
|
import androidx.compose.ui.text.font.FontWeight
|
||||||
|
import androidx.compose.ui.text.style.TextOverflow
|
||||||
|
import androidx.compose.ui.unit.dp
|
||||||
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
|
import eu.kanade.presentation.components.MangaCover
|
||||||
|
import eu.kanade.presentation.util.horizontalPadding
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.util.lang.toTimestampString
|
||||||
|
import java.text.DecimalFormat
|
||||||
|
import java.text.DecimalFormatSymbols
|
||||||
|
|
||||||
|
private val HISTORY_ITEM_HEIGHT = 96.dp
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HistoryItem(
|
||||||
|
modifier: Modifier = Modifier,
|
||||||
|
history: HistoryWithRelations,
|
||||||
|
onClickCover: () -> Unit,
|
||||||
|
onClickResume: () -> Unit,
|
||||||
|
onClickDelete: () -> Unit,
|
||||||
|
) {
|
||||||
|
Row(
|
||||||
|
modifier = modifier
|
||||||
|
.clickable(onClick = onClickResume)
|
||||||
|
.height(HISTORY_ITEM_HEIGHT)
|
||||||
|
.padding(horizontal = horizontalPadding, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
MangaCover.Book(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.clickable(onClick = onClickCover),
|
||||||
|
data = history.coverData,
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = horizontalPadding, end = 8.dp),
|
||||||
|
) {
|
||||||
|
val textStyle = MaterialTheme.typography.bodyMedium
|
||||||
|
Text(
|
||||||
|
text = history.title,
|
||||||
|
maxLines = 2,
|
||||||
|
overflow = TextOverflow.Ellipsis,
|
||||||
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold),
|
||||||
|
)
|
||||||
|
Text(
|
||||||
|
text = if (history.chapterNumber > -1) {
|
||||||
|
stringResource(
|
||||||
|
R.string.recent_manga_time,
|
||||||
|
chapterFormatter.format(history.chapterNumber),
|
||||||
|
history.readAt?.toTimestampString() ?: "",
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
history.readAt?.toTimestampString() ?: ""
|
||||||
|
},
|
||||||
|
modifier = Modifier.padding(top = 4.dp),
|
||||||
|
style = textStyle,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
IconButton(onClick = onClickDelete) {
|
||||||
|
Icon(
|
||||||
|
imageVector = Icons.Outlined.Delete,
|
||||||
|
contentDescription = stringResource(R.string.action_delete),
|
||||||
|
tint = MaterialTheme.colorScheme.onSurface,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@Composable
|
||||||
|
fun HistoryItemShimmer(brush: Brush) {
|
||||||
|
Row(
|
||||||
|
modifier = Modifier
|
||||||
|
.height(HISTORY_ITEM_HEIGHT)
|
||||||
|
.padding(horizontal = horizontalPadding, vertical = 8.dp),
|
||||||
|
verticalAlignment = Alignment.CenterVertically,
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.fillMaxHeight()
|
||||||
|
.aspectRatio(MangaCover.Book.ratio)
|
||||||
|
.clip(RoundedCornerShape(4.dp))
|
||||||
|
.drawBehind {
|
||||||
|
drawRect(brush = brush)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
Column(
|
||||||
|
modifier = Modifier
|
||||||
|
.weight(1f)
|
||||||
|
.padding(start = horizontalPadding, end = 8.dp),
|
||||||
|
) {
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.drawBehind {
|
||||||
|
drawRect(brush = brush)
|
||||||
|
}
|
||||||
|
.height(14.dp)
|
||||||
|
.fillMaxWidth(0.70f),
|
||||||
|
)
|
||||||
|
Box(
|
||||||
|
modifier = Modifier
|
||||||
|
.padding(top = 4.dp)
|
||||||
|
.height(14.dp)
|
||||||
|
.fillMaxWidth(0.45f)
|
||||||
|
.drawBehind {
|
||||||
|
drawRect(brush = brush)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private val chapterFormatter = DecimalFormat(
|
||||||
|
"#.###",
|
||||||
|
DecimalFormatSymbols().apply { decimalSeparator = '.' },
|
||||||
|
)
|
9
app/src/main/java/eu/kanade/presentation/util/Shimmer.kt
Normal file
9
app/src/main/java/eu/kanade/presentation/util/Shimmer.kt
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
package eu.kanade.presentation.util
|
||||||
|
|
||||||
|
import androidx.compose.ui.graphics.Color
|
||||||
|
|
||||||
|
val shimmerGradient = listOf(
|
||||||
|
Color.LightGray.copy(alpha = 0.8f),
|
||||||
|
Color.LightGray.copy(alpha = 0.2f),
|
||||||
|
Color.LightGray.copy(alpha = 0.8f),
|
||||||
|
)
|
Reference in a new issue