Migrate History screen to Compose (#6922)

* Migrate History screen to Compose

- Migrate screen
- Strip logic from presenter into use cases and repository
- Setup for other screen being able to migrate to Compose with Theme

* Changes from review comments
This commit is contained in:
Andreas 2022-04-17 16:36:22 +02:00 committed by GitHub
parent 7d50d7ff52
commit c475acd1ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
40 changed files with 986 additions and 668 deletions

View File

@ -109,6 +109,7 @@ android {
buildFeatures {
viewBinding = true
compose = true
// Disable some unused things
aidl = false
@ -122,6 +123,10 @@ android {
checkReleaseBuilds = false
}
composeOptions {
kotlinCompilerExtensionVersion = compose.versions.compose.get()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
@ -133,6 +138,16 @@ android {
}
dependencies {
implementation(compose.foundation)
implementation(compose.material3.core)
implementation(compose.material3.adapter)
implementation(compose.animation)
implementation(compose.ui.tooling)
implementation(androidx.paging.runtime)
implementation(androidx.paging.compose)
implementation(kotlinx.reflect)
implementation(kotlinx.bundles.coroutines)
@ -262,6 +277,9 @@ tasks {
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
"-Xopt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-Xopt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi"
)
}

View File

@ -0,0 +1,43 @@
package eu.kanade.data.history.local
import androidx.paging.PagingSource
import androidx.paging.PagingState
import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import logcat.logcat
class HistoryPagingSource(
private val repository: HistoryRepository,
private val query: String
) : PagingSource<Int, MangaChapterHistory>() {
override fun getRefreshKey(state: PagingState<Int, MangaChapterHistory>): Int? {
return state.anchorPosition?.let { anchorPosition ->
val anchorPage = state.closestPageToPosition(anchorPosition)
anchorPage?.prevKey?.plus(1) ?: anchorPage?.nextKey?.minus(1)
}
}
override suspend fun load(params: LoadParams<Int>): LoadResult.Page<Int, MangaChapterHistory> {
val nextPageNumber = params.key ?: 0
logcat { "Loading page $nextPageNumber" }
val response = repository.getHistory(PAGE_SIZE, nextPageNumber, query)
val nextKey = if (response.size == 25) {
nextPageNumber + 1
} else {
null
}
return LoadResult.Page(
data = response,
prevKey = null,
nextKey = nextKey
)
}
companion object {
const val PAGE_SIZE = 25
}
}

View File

@ -0,0 +1,137 @@
package eu.kanade.data.history.repository
import eu.kanade.data.history.local.HistoryPagingSource
import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.withContext
import rx.Subscription
import rx.schedulers.Schedulers
import java.util.*
class HistoryRepositoryImpl(
private val db: DatabaseHelper
) : HistoryRepository {
/**
* Used to observe changes in the History table
* as RxJava isn't supported in Paging 3
*/
private var subscription: Subscription? = null
/**
* Paging Source for history table
*/
override fun getHistory(query: String): HistoryPagingSource {
subscription?.unsubscribe()
val pagingSource = HistoryPagingSource(this, query)
subscription = db.db
.observeChangesInTable(HistoryTable.TABLE)
.observeOn(Schedulers.io())
.subscribe {
pagingSource.invalidate()
}
return pagingSource
}
override suspend fun getHistory(limit: Int, page: Int, query: String) = coroutineScope {
withContext(Dispatchers.IO) {
// Set date limit for recent manga
val calendar = Calendar.getInstance().apply {
time = Date()
add(Calendar.YEAR, -50)
}
db.getRecentManga(calendar.time, limit, page * limit, query)
.executeAsBlocking()
}
}
override suspend fun getNextChapterForManga(manga: Manga, chapter: Chapter): Chapter? = coroutineScope {
withContext(Dispatchers.IO) {
if (!chapter.read) {
return@withContext chapter
}
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
else -> throw NotImplementedError("Unknown sorting method")
}
val chapters = db.getChapters(manga)
.executeAsBlocking()
.sortedWith { c1, c2 -> sortFunction(c1, c2) }
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
return@withContext when (manga.sorting) {
Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1)
Manga.CHAPTER_SORTING_NUMBER -> {
val chapterNumber = chapter.chapter_number
((currChapterIndex + 1) until chapters.size)
.map { chapters[it] }
.firstOrNull {
it.chapter_number > chapterNumber &&
it.chapter_number <= chapterNumber + 1
}
}
Manga.CHAPTER_SORTING_UPLOAD_DATE -> {
chapters.drop(currChapterIndex + 1)
.firstOrNull { it.date_upload >= chapter.date_upload }
}
else -> throw NotImplementedError("Unknown sorting method")
}
}
}
override suspend fun resetHistory(history: History): Boolean = coroutineScope {
withContext(Dispatchers.IO) {
try {
history.last_read = 0
db.upsertHistoryLastRead(history)
.executeAsBlocking()
true
} catch (e: Throwable) {
logcat(throwable = e)
false
}
}
}
override suspend fun resetHistoryByMangaId(mangaId: Long): Boolean = coroutineScope {
withContext(Dispatchers.IO) {
try {
val history = db.getHistoryByMangaId(mangaId)
.executeAsBlocking()
history.forEach { it.last_read = 0 }
db.upsertHistoryLastRead(history)
.executeAsBlocking()
true
} catch (e: Throwable) {
logcat(throwable = e)
false
}
}
}
override suspend fun deleteAllHistory(): Boolean = coroutineScope {
withContext(Dispatchers.IO) {
try {
db.dropHistoryTable()
.executeAsBlocking()
true
} catch (e: Throwable) {
logcat(throwable = e)
false
}
}
}
}

View File

@ -0,0 +1,26 @@
package eu.kanade.domain
import eu.kanade.data.history.repository.HistoryRepositoryImpl
import eu.kanade.domain.history.interactor.DeleteHistoryTable
import eu.kanade.domain.history.interactor.GetHistory
import eu.kanade.domain.history.interactor.GetNextChapterForManga
import eu.kanade.domain.history.interactor.RemoveHistoryById
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
import eu.kanade.domain.history.repository.HistoryRepository
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addFactory
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class DomainModule : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingletonFactory<HistoryRepository> { HistoryRepositoryImpl(get()) }
addFactory { DeleteHistoryTable(get()) }
addFactory { GetHistory(get()) }
addFactory { GetNextChapterForManga(get()) }
addFactory { RemoveHistoryById(get()) }
addFactory { RemoveHistoryByMangaId(get()) }
}
}

View File

@ -0,0 +1,12 @@
package eu.kanade.domain.history.interactor
import eu.kanade.domain.history.repository.HistoryRepository
class DeleteHistoryTable(
private val repository: HistoryRepository
) {
suspend fun await(): Boolean {
return repository.deleteAllHistory()
}
}

View File

@ -0,0 +1,22 @@
package eu.kanade.domain.history.interactor
import androidx.paging.Pager
import androidx.paging.PagingConfig
import androidx.paging.PagingData
import eu.kanade.data.history.local.HistoryPagingSource
import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import kotlinx.coroutines.flow.Flow
class GetHistory(
private val repository: HistoryRepository
) {
fun subscribe(query: String): Flow<PagingData<MangaChapterHistory>> {
return Pager(
PagingConfig(pageSize = HistoryPagingSource.PAGE_SIZE)
) {
repository.getHistory(query)
}.flow
}
}

View File

@ -0,0 +1,14 @@
package eu.kanade.domain.history.interactor
import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
class GetNextChapterForManga(
private val repository: HistoryRepository
) {
suspend fun await(manga: Manga, chapter: Chapter): Chapter? {
return repository.getNextChapterForManga(manga, chapter)
}
}

View File

@ -0,0 +1,21 @@
package eu.kanade.domain.history.interactor
import eu.kanade.domain.history.repository.HistoryRepository
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.HistoryImpl
class RemoveHistoryById(
private val repository: HistoryRepository
) {
suspend fun await(history: History): Boolean {
// Workaround for list not freaking out when changing reference varaible
val history = HistoryImpl().apply {
id = history.id
chapter_id = history.chapter_id
last_read = history.last_read
time_read = history.time_read
}
return repository.resetHistory(history)
}
}

View File

@ -0,0 +1,12 @@
package eu.kanade.domain.history.interactor
import eu.kanade.domain.history.repository.HistoryRepository
class RemoveHistoryByMangaId(
private val repository: HistoryRepository
) {
suspend fun await(mangaId: Long): Boolean {
return repository.resetHistoryByMangaId(mangaId)
}
}

View File

@ -0,0 +1,22 @@
package eu.kanade.domain.history.repository
import eu.kanade.data.history.local.HistoryPagingSource
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
interface HistoryRepository {
fun getHistory(query: String): HistoryPagingSource
suspend fun getHistory(limit: Int, page: Int, query: String): List<MangaChapterHistory>
suspend fun getNextChapterForManga(manga: Manga, chapter: Chapter): Chapter?
suspend fun resetHistory(history: History): Boolean
suspend fun resetHistoryByMangaId(mangaId: Long): Boolean
suspend fun deleteAllHistory(): Boolean
}

View File

@ -0,0 +1,49 @@
package eu.kanade.presentation.components
import android.view.ViewGroup
import androidx.annotation.StringRes
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.viewinterop.AndroidView
import eu.kanade.tachiyomi.widget.EmptyView
@Composable
fun EmptyScreen(
@StringRes textResource: Int,
actions: List<EmptyView.Action>? = null,
) {
EmptyScreen(
message = stringResource(id = textResource),
actions = actions,
)
}
@Composable
fun EmptyScreen(
message: String,
actions: List<EmptyView.Action>? = null,
) {
Box(
modifier = Modifier
.fillMaxSize()
) {
AndroidView(
factory = { context ->
EmptyView(context).apply {
layoutParams = ViewGroup.LayoutParams(
ViewGroup.LayoutParams.WRAP_CONTENT,
ViewGroup.LayoutParams.WRAP_CONTENT,
)
}
},
modifier = Modifier
.align(Alignment.Center),
) { view ->
view.show(message, actions)
}
}
}

View File

@ -0,0 +1,35 @@
package eu.kanade.presentation.components
import androidx.compose.foundation.layout.aspectRatio
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clip
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.layout.ContentScale
import androidx.compose.ui.unit.dp
import coil.compose.AsyncImage
import eu.kanade.tachiyomi.data.database.models.Manga
enum class MangaCoverAspect(val ratio: Float) {
SQUARE(1f / 1f),
COVER(2f / 3f)
}
@Composable
fun MangaCover(
modifier: Modifier = Modifier,
manga: Manga,
aspect: MangaCoverAspect,
contentDescription: String = "",
shape: Shape = RoundedCornerShape(4.dp)
) {
AsyncImage(
model = manga,
contentDescription = contentDescription,
modifier = modifier
.aspectRatio(aspect.ratio)
.clip(shape),
contentScale = ContentScale.Crop
)
}

View File

@ -0,0 +1,298 @@
package eu.kanade.presentation.history
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBarsPadding
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.selection.toggleable
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.PlayArrow
import androidx.compose.material.icons.outlined.Delete
import androidx.compose.material3.AlertDialog
import androidx.compose.material3.Checkbox
import androidx.compose.material3.CircularProgressIndicator
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.nestedscroll.nestedScroll
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.rememberNestedScrollInteropConnection
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.core.text.buildSpannedString
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.components.MangaCoverAspect
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter
import eu.kanade.tachiyomi.ui.recent.history.UiModel
import eu.kanade.tachiyomi.util.lang.toRelativeString
import eu.kanade.tachiyomi.util.lang.toTimestampString
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.text.DateFormat
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.*
val chapterFormatter = DecimalFormat(
"#.###",
DecimalFormatSymbols()
.apply { decimalSeparator = '.' },
)
@Composable
fun HistoryScreen(
composeView: ComposeView,
presenter: HistoryPresenter,
onClickItem: (MangaChapterHistory) -> Unit,
onClickResume: (MangaChapterHistory) -> Unit,
onClickDelete: (MangaChapterHistory, Boolean) -> Unit,
) {
val nestedSrollInterop = rememberNestedScrollInteropConnection(composeView)
TachiyomiTheme {
val state by presenter.state.collectAsState()
val history = state.list?.collectAsLazyPagingItems()
when {
history == null -> {
CircularProgressIndicator()
}
history.itemCount == 0 -> {
EmptyScreen(
textResource = R.string.information_no_recent_manga
)
}
else -> {
HistoryContent(
nestedScroll = nestedSrollInterop,
history = history,
onClickItem = onClickItem,
onClickResume = onClickResume,
onClickDelete = onClickDelete,
)
}
}
}
}
@Composable
fun HistoryContent(
history: LazyPagingItems<UiModel>,
onClickItem: (MangaChapterHistory) -> Unit,
onClickResume: (MangaChapterHistory) -> Unit,
onClickDelete: (MangaChapterHistory, Boolean) -> Unit,
preferences: PreferencesHelper = Injekt.get(),
nestedScroll: NestedScrollConnection
) {
val relativeTime: Int = remember { preferences.relativeTime().get() }
val dateFormat: DateFormat = remember { preferences.dateFormat() }
val (removeState, setRemoveState) = remember { mutableStateOf<MangaChapterHistory?>(null) }
val scrollState = rememberLazyListState()
LazyColumn(
modifier = Modifier
.nestedScroll(nestedScroll),
state = scrollState,
) {
items(history) { item ->
when (item) {
is UiModel.Header -> {
HistoryHeader(
modifier = Modifier
.animateItemPlacement(),
date = item.date,
relativeTime = relativeTime,
dateFormat = dateFormat
)
}
is UiModel.History -> {
val value = item.item
HistoryItem(
modifier = Modifier.animateItemPlacement(),
history = value,
onClickItem = { onClickItem(value) },
onClickResume = { onClickResume(value) },
onClickDelete = { setRemoveState(value) },
)
}
null -> {}
}
}
item {
Spacer(
modifier = Modifier
.navigationBarsPadding()
)
}
}
if (removeState != null) {
RemoveHistoryDialog(
onPositive = { all ->
onClickDelete(removeState, all)
setRemoveState(null)
},
onNegative = { setRemoveState(null) }
)
}
}
@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: MangaChapterHistory,
onClickItem: () -> Unit,
onClickResume: () -> Unit,
onClickDelete: () -> Unit,
) {
Row(
modifier = modifier
.clickable(onClick = onClickItem)
.height(96.dp)
.padding(horizontal = horizontalPadding, vertical = 8.dp),
verticalAlignment = Alignment.CenterVertically,
) {
MangaCover(
modifier = Modifier.fillMaxHeight(),
manga = history.manga,
aspect = MangaCoverAspect.COVER
)
Column(
modifier = Modifier
.weight(1f)
.padding(start = horizontalPadding, end = 8.dp),
) {
val textStyle = MaterialTheme.typography.bodyMedium.copy(
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = history.manga.title,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = textStyle.copy(fontWeight = FontWeight.SemiBold)
)
Row {
Text(
text = buildSpannedString {
if (history.chapter.chapter_number > -1) {
append(
stringResource(
R.string.history_prefix,
chapterFormatter.format(history.chapter.chapter_number)
)
)
}
append(Date(history.history.last_read).toTimestampString())
}.toString(),
modifier = Modifier.padding(top = 2.dp),
style = textStyle
)
}
}
IconButton(onClick = onClickDelete) {
Icon(
imageVector = Icons.Outlined.Delete,
contentDescription = stringResource(id = R.string.action_delete),
tint = MaterialTheme.colorScheme.onSurface,
)
}
IconButton(onClick = onClickResume) {
Icon(
imageVector = Icons.Filled.PlayArrow,
contentDescription = stringResource(id = R.string.action_resume),
tint = MaterialTheme.colorScheme.onSurface,
)
}
}
}
@Composable
fun RemoveHistoryDialog(
onPositive: (Boolean) -> Unit,
onNegative: () -> Unit
) {
val (removeEverything, removeEverythingState) = remember { mutableStateOf(false) }
AlertDialog(
title = {
Text(text = stringResource(id = R.string.action_remove))
},
text = {
Column {
Text(text = stringResource(id = R.string.dialog_with_checkbox_remove_description))
Row(
modifier = Modifier.toggleable(value = removeEverything, onValueChange = removeEverythingState),
verticalAlignment = Alignment.CenterVertically
) {
Checkbox(
checked = removeEverything,
onCheckedChange = removeEverythingState,
)
Text(
text = stringResource(id = R.string.dialog_with_checkbox_reset)
)
}
}
},
onDismissRequest = onNegative,
confirmButton = {
TextButton(onClick = { onPositive(removeEverything) }) {
Text(text = stringResource(id = R.string.action_remove))
}
},
dismissButton = {
TextButton(onClick = onNegative) {
Text(text = stringResource(id = R.string.action_cancel))
}
},
)
}

View File

@ -0,0 +1,20 @@
package eu.kanade.presentation.theme
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.ui.platform.LocalContext
import com.google.android.material.composethemeadapter3.createMdc3Theme
@Composable
fun TachiyomiTheme(content: @Composable () -> Unit) {
val context = LocalContext.current
var (colorScheme, typography) = createMdc3Theme(
context = context
)
MaterialTheme(
colorScheme = colorScheme!!,
typography = typography!!,
content = content
)
}

View File

@ -0,0 +1,5 @@
package eu.kanade.presentation.util
import androidx.compose.ui.unit.dp
val horizontalPadding = 16.dp

View File

@ -0,0 +1,5 @@
package eu.kanade.presentation.util
import androidx.compose.foundation.lazy.LazyListState
fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1

View File

@ -24,6 +24,7 @@ import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.disk.DiskCache
import coil.util.DebugLogger
import eu.kanade.domain.DomainModule
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
@ -74,6 +75,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
}
Injekt.importModule(AppModule(this))
Injekt.importModule(DomainModule())
setupAcra()
setupNotificationChannels()

View File

@ -294,7 +294,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
}
}
}
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
}
/**

View File

@ -168,7 +168,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
}
}
}
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
}
/**

View File

@ -5,7 +5,7 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.HistoryUpsertResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import java.util.Date
@ -64,9 +64,9 @@ interface HistoryQueries : DbProvider {
* Inserts history object if not yet in database
* @param history history object
*/
fun updateHistoryLastRead(history: History) = db.put()
fun upsertHistoryLastRead(history: History) = db.put()
.`object`(history)
.withPutResolver(HistoryLastReadPutResolver())
.withPutResolver(HistoryUpsertResolver())
.prepare()
/**
@ -74,12 +74,40 @@ interface HistoryQueries : DbProvider {
* Inserts history object if not yet in database
* @param historyList history object list
*/
fun updateHistoryLastRead(historyList: List<History>) = db.put()
fun upsertHistoryLastRead(historyList: List<History>) = db.put()
.objects(historyList)
.withPutResolver(HistoryLastReadPutResolver())
.withPutResolver(HistoryUpsertResolver())
.prepare()
fun deleteHistory() = db.delete()
fun resetHistoryLastRead(historyId: Long) = db.executeSQL()
.withQuery(
RawQuery.builder()
.query(
"""
UPDATE ${HistoryTable.TABLE}
SET history_last_read = 0
WHERE ${HistoryTable.COL_ID} = $historyId
""".trimIndent()
)
.build()
)
.prepare()
fun resetHistoryLastRead(historyIds: List<Long>) = db.executeSQL()
.withQuery(
RawQuery.builder()
.query(
"""
UPDATE ${HistoryTable.TABLE}
SET history_last_read = 0
WHERE ${HistoryTable.COL_ID} in ${historyIds.joinToString(",", "(", ")")}
""".trimIndent()
)
.build()
)
.prepare()
fun dropHistoryTable() = db.delete()
.byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE)

View File

@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
class HistoryLastReadPutResolver : HistoryPutResolver() {
class HistoryUpsertResolver : HistoryPutResolver() {
/**
* Updates last_read time of chapter

View File

@ -97,14 +97,19 @@ import kotlin.math.max
class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
companion object {
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
fun newIntent(context: Context, mangaId: Long?, chapterId: Long?): Intent {
return Intent(context, ReaderActivity::class.java).apply {
putExtra("manga", manga.id)
putExtra("chapter", chapter.id)
putExtra("manga", mangaId)
putExtra("chapter", chapterId)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
return newIntent(context, manga.id, chapter.id)
}
private const val ENABLED_BUTTON_IMAGE_ALPHA = 255
private const val DISABLED_BUTTON_IMAGE_ALPHA = 64

View File

@ -449,7 +449,7 @@ class ReaderPresenter(
private fun saveChapterHistory(chapter: ReaderChapter) {
if (!incognitoMode) {
val history = History.create(chapter.chapter).apply { last_read = Date().time }
db.updateHistoryLastRead(history).asRxCompletable()
db.upsertHistoryLastRead(history).asRxCompletable()
.onErrorComplete()
.subscribeOn(Schedulers.io())
.subscribe()

View File

@ -0,0 +1,21 @@
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,51 +0,0 @@
package eu.kanade.tachiyomi.ui.recent.history
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.source.SourceManager
import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
/**
* Adapter of HistoryHolder.
* Connection between Fragment and Holder
* Holder updates should be called from here.
*
* @param controller a HistoryController object
* @constructor creates an instance of the adapter.
*/
class HistoryAdapter(controller: HistoryController) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val sourceManager: SourceManager by injectLazy()
val resumeClickListener: OnResumeClickListener = controller
val removeClickListener: OnRemoveClickListener = controller
val itemClickListener: OnItemClickListener = controller
/**
* DecimalFormat used to display correct chapter number
*/
val decimalFormat = DecimalFormat(
"#.###",
DecimalFormatSymbols()
.apply { decimalSeparator = '.' },
)
init {
setDisplayHeadersAtStartUp(true)
}
interface OnResumeClickListener {
fun onResumeClick(position: Int)
}
interface OnRemoveClickListener {
fun onRemoveClick(position: Int)
}
interface OnItemClickListener {
fun onItemClick(position: Int)
}
}

View File

@ -1,192 +1,65 @@
package eu.kanade.tachiyomi.ui.recent.history
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.presentation.history.HistoryScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.HistoryControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.ProgressItem
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import logcat.LogPriority
import reactivecircus.flowbinding.appcompat.queryTextChanges
import uy.kohesive.injekt.injectLazy
/**
* Fragment that shows recently read manga.
*/
class HistoryController :
NucleusController<HistoryControllerBinding, HistoryPresenter>(),
RootController,
FlexibleAdapter.OnUpdateListener,
FlexibleAdapter.EndlessScrollListener,
HistoryAdapter.OnRemoveClickListener,
HistoryAdapter.OnResumeClickListener,
HistoryAdapter.OnItemClickListener,
RemoveHistoryDialog.Listener {
NucleusController<ComposeControllerBinding, HistoryPresenter>(),
RootController {
private val db: DatabaseHelper by injectLazy()
/**
* Adapter containing the recent manga.
*/
var adapter: HistoryAdapter? = null
private set
/**
* Endless loading item.
*/
private var progressItem: ProgressItem? = null
/**
* Search query.
*/
private var query = ""
override fun getTitle(): String? {
return resources?.getString(R.string.label_recent_manga)
}
override fun getTitle(): String? = resources?.getString(R.string.label_recent_manga)
override fun createPresenter(): HistoryPresenter {
return HistoryPresenter()
}
override fun createPresenter(): HistoryPresenter = HistoryPresenter()
override fun createBinding(inflater: LayoutInflater) = HistoryControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding =
ComposeControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
// Initialize adapter
binding.recycler.layoutManager = LinearLayoutManager(view.context)
adapter = HistoryAdapter(this@HistoryController)
binding.recycler.setHasFixedSize(true)
binding.recycler.adapter = adapter
adapter?.fastScroller = binding.fastScroller
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
/**
* Populate adapter with chapters
*
* @param mangaHistory list of manga history
*/
fun onNextManga(mangaHistory: List<HistoryItem>, cleanBatch: Boolean = false) {
if (adapter?.itemCount ?: 0 == 0) {
resetProgressItem()
}
if (cleanBatch) {
adapter?.updateDataSet(mangaHistory)
} else {
adapter?.onLoadMoreComplete(mangaHistory)
}
binding.recycler.onAnimationsFinished {
(activity as? MainActivity)?.ready = true
}
}
/**
* Safely error if next page load fails
*/
fun onAddPageError(error: Throwable) {
adapter?.onLoadMoreComplete(null)
adapter?.endlessTargetCount = 1
logcat(LogPriority.ERROR, error)
}
override fun onUpdateEmptyView(size: Int) {
if (size > 0) {
binding.emptyView.hide()
} else {
binding.emptyView.show(R.string.information_no_recent_manga)
}
}
/**
* Sets a new progress item and reenables the scroll listener.
*/
private fun resetProgressItem() {
progressItem = ProgressItem()
adapter?.endlessTargetCount = 0
adapter?.setEndlessScrollListener(this, progressItem!!)
}
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
val view = view ?: return
if (BackupRestoreService.isRunning(view.context.applicationContext)) {
onAddPageError(Throwable())
return
}
val adapter = adapter ?: return
presenter.requestNext(adapter.itemCount - adapter.headerItems.size, query)
}
override fun noMoreLoad(newItemsSize: Int) {}
override fun onResumeClick(position: Int) {
val activity = activity ?: return
val (manga, chapter, _) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return
val nextChapter = presenter.getNextChapter(chapter, manga)
if (nextChapter != null) {
val intent = ReaderActivity.newIntent(activity, manga, nextChapter)
startActivity(intent)
} else {
activity.toast(R.string.no_next_chapter)
}
}
override fun onRemoveClick(position: Int) {
val (manga, _, history) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return
RemoveHistoryDialog(this, manga, history).showDialog(router)
}
override fun onItemClick(position: Int) {
val manga = (adapter?.getItem(position) as? HistoryItem)?.mch?.manga ?: return
router.pushController(MangaController(manga).withFadeTransaction())
}
override fun removeHistory(manga: Manga, history: History, all: Boolean) {
if (all) {
// Reset last read of chapter to 0L
presenter.removeAllFromHistory(manga.id!!)
} else {
// Remove all chapters belonging to manga from library
presenter.removeFromHistory(history)
binding.root.setContent {
HistoryScreen(
composeView = binding.root,
presenter = presenter,
onClickItem = { (manga, _, _) ->
router.pushController(MangaController(manga).withFadeTransaction())
},
onClickResume = { (manga, chapter, _) ->
presenter.getNextChapterForManga(manga, chapter)
},
onClickDelete = { (manga, _, history), all ->
if (all) {
// Reset last read of chapter to 0L
presenter.removeAllFromHistory(manga.id!!)
} else {
// Remove all chapters belonging to manga from library
presenter.removeFromHistory(history)
}
},
)
}
}
@ -201,46 +74,33 @@ class HistoryController :
searchView.clearFocus()
}
searchView.queryTextChanges()
.drop(1) // Drop first event after subscribed
.filter { router.backstack.lastOrNull()?.controller == this }
.onEach {
query = it.toString()
presenter.updateList(query)
presenter.search(query)
}
.launchIn(viewScope)
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.fixExpand(
onExpand = { invalidateMenuOnExpand() },
)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
return when (item.itemId) {
R.id.action_clear_history -> {
val ctrl = ClearHistoryDialogController()
ctrl.targetController = this@HistoryController
ctrl.showDialog(router)
val dialog = ClearHistoryDialogController()
dialog.targetController = this@HistoryController
dialog.showDialog(router)
true
}
}
return super.onOptionsItemSelected(item)
}
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)?.clearHistory()
}
.setNegativeButton(android.R.string.cancel, null)
.create()
else -> super.onOptionsItemSelected(item)
}
}
private fun clearHistory() {
db.deleteHistory().executeAsBlocking()
activity?.toast(R.string.clear_history_completed)
fun openChapter(chapter: Chapter?) {
val activity = activity ?: return
if (chapter != null) {
val intent = ReaderActivity.newIntent(activity, chapter.manga_id, chapter.id)
startActivity(intent)
} else {
activity.toast(R.string.no_next_chapter)
}
}
}

View File

@ -1,71 +0,0 @@
package eu.kanade.tachiyomi.ui.recent.history
import android.view.View
import coil.dispose
import coil.load
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.databinding.HistoryItemBinding
import eu.kanade.tachiyomi.util.lang.toTimestampString
import java.util.Date
/**
* Holder that contains recent manga item
* Uses R.layout.item_recently_read.
* UI related actions should be called from here.
*
* @param view the inflated view for this holder.
* @param adapter the adapter handling this holder.
* @constructor creates a new recent chapter holder.
*/
class HistoryHolder(
view: View,
val adapter: HistoryAdapter,
) : FlexibleViewHolder(view, adapter) {
private val binding = HistoryItemBinding.bind(view)
init {
binding.holder.setOnClickListener {
adapter.itemClickListener.onItemClick(bindingAdapterPosition)
}
binding.remove.setOnClickListener {
adapter.removeClickListener.onRemoveClick(bindingAdapterPosition)
}
binding.resume.setOnClickListener {
adapter.resumeClickListener.onResumeClick(bindingAdapterPosition)
}
}
/**
* Set values of view
*
* @param item item containing history information
*/
fun bind(item: MangaChapterHistory) {
// Retrieve objects
val (manga, chapter, history) = item
// Set manga title
binding.mangaTitle.text = manga.title
// Set chapter number + timestamp
if (chapter.chapter_number > -1f) {
val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
binding.mangaSubtitle.text = itemView.context.getString(
R.string.recent_manga_time,
formattedNumber,
Date(history.last_read).toTimestampString(),
)
} else {
binding.mangaSubtitle.text = Date(history.last_read).toTimestampString()
}
// Set cover
binding.cover.dispose()
binding.cover.load(item.manga)
}
}

View File

@ -1,42 +0,0 @@
package eu.kanade.tachiyomi.ui.recent.history
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.AbstractSectionableItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
class HistoryItem(val mch: MangaChapterHistory, header: DateSectionItem) :
AbstractSectionableItem<HistoryHolder, DateSectionItem>(header) {
override fun getLayoutRes(): Int {
return R.layout.history_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): HistoryHolder {
return HistoryHolder(view, adapter as HistoryAdapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: HistoryHolder,
position: Int,
payloads: List<Any?>?,
) {
holder.bind(mch)
}
override fun equals(other: Any?): Boolean {
if (other is HistoryItem) {
return mch.manga.id == other.mch.manga.id
}
return false
}
override fun hashCode(): Int {
return mch.manga.id!!.hashCode()
}
}

View File

@ -1,157 +1,135 @@
package eu.kanade.tachiyomi.ui.recent.history
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.insertSeparators
import androidx.paging.map
import eu.kanade.domain.history.interactor.DeleteHistoryTable
import eu.kanade.domain.history.interactor.GetHistory
import eu.kanade.domain.history.interactor.GetNextChapterForManga
import eu.kanade.domain.history.interactor.RemoveHistoryById
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.toDateKey
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.injectLazy
import java.text.DateFormat
import java.util.Calendar
import java.util.Date
import java.util.TreeMap
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
/**
* Presenter of HistoryFragment.
* Contains information and data for fragment.
* Observable updates should be called from here.
*/
class HistoryPresenter : BasePresenter<HistoryController>() {
class HistoryPresenter(
private val getHistory: GetHistory = Injekt.get(),
private val getNextChapterForManga: GetNextChapterForManga = Injekt.get(),
private val deleteHistoryTable: DeleteHistoryTable = Injekt.get(),
private val removeHistoryById: RemoveHistoryById = Injekt.get(),
private val removeHistoryByMangaId: RemoveHistoryByMangaId = Injekt.get(),
) : BasePresenter<HistoryController>() {
private val db: DatabaseHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
private val relativeTime: Int = preferences.relativeTime().get()
private val dateFormat: DateFormat = preferences.dateFormat()
private var recentMangaSubscription: Subscription? = null
private var _query: MutableStateFlow<String> = MutableStateFlow("")
private var _state: MutableStateFlow<HistoryState> = MutableStateFlow(HistoryState.EMPTY)
val state: StateFlow<HistoryState> = _state
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
// Used to get a list of recently read manga
updateList()
}
fun requestNext(offset: Int, search: String = "") {
getRecentMangaObservable(offset = offset, search = search)
.subscribeLatestCache(
{ view, mangas ->
view.onNextManga(mangas)
},
HistoryController::onAddPageError,
)
}
/**
* Get recent manga observable
* @return list of history
*/
private fun getRecentMangaObservable(limit: Int = 25, offset: Int = 0, search: String = ""): Observable<List<HistoryItem>> {
// Set date limit for recent manga
val cal = Calendar.getInstance().apply {
time = Date()
add(Calendar.YEAR, -50)
}
return db.getRecentManga(cal.time, limit, offset, search).asRxObservable()
.map { recents ->
val map = TreeMap<Date, MutableList<MangaChapterHistory>> { d1, d2 -> d2.compareTo(d1) }
val byDay = recents
.groupByTo(map) { it.history.last_read.toDateKey() }
byDay.flatMap { entry ->
val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat)
entry.value.map { HistoryItem(it, dateItem) }
}
}
.observeOn(AndroidSchedulers.mainThread())
}
/**
* Reset last read of chapter to 0L
* @param history history belonging to chapter
*/
fun removeFromHistory(history: History) {
history.last_read = 0L
db.updateHistoryLastRead(history).asRxObservable()
.subscribe()
}
/**
* Pull a list of history from the db
* @param search a search query to use for filtering
*/
fun updateList(search: String = "") {
recentMangaSubscription?.unsubscribe()
recentMangaSubscription = getRecentMangaObservable(search = search)
.subscribeLatestCache(
{ view, mangas ->
view.onNextManga(mangas, true)
},
HistoryController::onAddPageError,
)
}
/**
* Removes all chapters belonging to manga from history.
* @param mangaId id of manga
*/
fun removeAllFromHistory(mangaId: Long) {
db.getHistoryByMangaId(mangaId).asRxSingle()
.map { list ->
list.forEach { it.last_read = 0L }
db.updateHistoryLastRead(list).executeAsBlocking()
}
.subscribe()
}
/**
* Retrieves the next chapter of the given one.
*
* @param chapter the chapter of the history object.
* @param manga the manga of the chapter.
*/
fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? {
if (!chapter.read) {
return chapter
}
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
else -> throw NotImplementedError("Unknown sorting method")
}
val chapters = db.getChapters(manga).executeAsBlocking()
.sortedWith { c1, c2 -> sortFunction(c1, c2) }
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
return when (manga.sorting) {
Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1)
Manga.CHAPTER_SORTING_NUMBER -> {
val chapterNumber = chapter.chapter_number
((currChapterIndex + 1) until chapters.size)
.map { chapters[it] }
.firstOrNull {
it.chapter_number > chapterNumber &&
it.chapter_number <= chapterNumber + 1
presenterScope.launchIO {
_state.update { state ->
state.copy(
list = _query.flatMapLatest { query ->
getHistory.subscribe(query)
.map { pagingData ->
pagingData
.map {
UiModel.History(it)
}
.insertSeparators { before, after ->
val beforeDate =
before?.item?.history?.last_read?.toDateKey()
val afterDate =
after?.item?.history?.last_read?.toDateKey()
when {
beforeDate == null && afterDate != null -> UiModel.Header(
afterDate,
)
beforeDate != null && afterDate != null -> UiModel.Header(
afterDate,
)
// Return null to avoid adding a separator between two items.
else -> null
}
}
}
}
.cachedIn(presenterScope),
)
}
Manga.CHAPTER_SORTING_UPLOAD_DATE -> {
chapters.drop(currChapterIndex + 1)
.firstOrNull { it.date_upload >= chapter.date_upload }
}
}
fun search(query: String) {
presenterScope.launchIO {
_query.emit(query)
}
}
fun removeFromHistory(history: History) {
presenterScope.launchIO {
removeHistoryById.await(history)
}
}
fun removeAllFromHistory(mangaId: Long) {
presenterScope.launchIO {
removeHistoryByMangaId.await(mangaId)
}
}
fun getNextChapterForManga(manga: Manga, chapter: Chapter) {
presenterScope.launchIO {
val chapter = getNextChapterForManga.await(manga, chapter)
view?.openChapter(chapter)
}
}
fun deleteAllHistory() {
presenterScope.launchIO {
val result = deleteHistoryTable.await()
if (!result) return@launchIO
launchUI {
view?.activity?.toast(R.string.clear_history_completed)
}
else -> throw NotImplementedError("Unknown sorting method")
}
}
}
sealed class UiModel {
data class History(val item: MangaChapterHistory) : UiModel()
data class Header(val date: Date) : UiModel()
}
data class HistoryState(
val list: Flow<PagingData<UiModel>>? = null,
) {
companion object {
val EMPTY = HistoryState(null)
}
}

View File

@ -1,54 +0,0 @@
package eu.kanade.tachiyomi.ui.recent.history
import android.app.Dialog
import android.os.Bundle
import com.bluelinelabs.conductor.Controller
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.widget.DialogCheckboxView
class RemoveHistoryDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
where T : Controller, T : RemoveHistoryDialog.Listener {
private var manga: Manga? = null
private var history: History? = null
constructor(target: T, manga: Manga, history: History) : this() {
this.manga = manga
this.history = history
targetController = target
}
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val activity = activity!!
// Create custom view
val dialogCheckboxView = DialogCheckboxView(activity).apply {
setDescription(R.string.dialog_with_checkbox_remove_description)
setOptionDescription(R.string.dialog_with_checkbox_reset)
}
return MaterialAlertDialogBuilder(activity)
.setTitle(R.string.action_remove)
.setView(dialogCheckboxView)
.setPositiveButton(R.string.action_remove) { _, _ -> onPositive(dialogCheckboxView.isChecked()) }
.setNegativeButton(android.R.string.cancel, null)
.create()
}
private fun onPositive(checked: Boolean) {
val target = targetController as? Listener ?: return
val manga = manga ?: return
val history = history ?: return
target.removeHistory(manga, history, checked)
}
interface Listener {
fun removeHistory(manga: Manga, history: History, all: Boolean)
}
}

View File

@ -0,0 +1,4 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent" />

View File

@ -1,33 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView
android:id="@+id/recycler"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:clipToPadding="false"
android:paddingTop="4dp"
android:paddingBottom="@dimen/action_toolbar_list_padding"
tools:listitem="@layout/history_item" />
<eu.kanade.tachiyomi.widget.MaterialFastScroll
android:id="@+id/fast_scroller"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:layout_gravity="end"
app:fastScrollerBubbleEnabled="false"
tools:visibility="visible" />
<eu.kanade.tachiyomi.widget.EmptyView
android:id="@+id/empty_view"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:visibility="gone" />
</FrameLayout>

View File

@ -1,85 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:id="@+id/holder"
android:layout_width="match_parent"
android:layout_height="96dp"
android:paddingStart="16dp"
android:paddingTop="8dp"
android:paddingEnd="8dp"
android:paddingBottom="8dp"
android:background="?attr/selectableItemBackground"
android:orientation="horizontal">
<com.google.android.material.imageview.ShapeableImageView
android:id="@+id/cover"
android:layout_width="0dp"
android:layout_height="match_parent"
android:contentDescription="@string/description_cover"
android:scaleType="centerCrop"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintDimensionRatio="h,3:2"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:shapeAppearance="@style/ShapeAppearanceOverlay.Cover"
tools:src="@mipmap/ic_launcher" />
<LinearLayout
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginEnd="8dp"
android:orientation="vertical"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/remove"
app:layout_constraintStart_toEndOf="@+id/cover"
app:layout_constraintTop_toTopOf="parent">
<TextView
android:id="@+id/manga_title"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:ellipsize="end"
android:maxLines="2"
android:textAppearance="?attr/textAppearanceTitleSmall"
tools:text="Title" />
<TextView
android:id="@+id/manga_subtitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="2dp"
android:textAppearance="?attr/textAppearanceBodyMedium"
android:textColor="?android:attr/textColorSecondary"
tools:text="Subtitle" />
</LinearLayout>
<ImageButton
android:id="@+id/remove"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_resume"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toStartOf="@+id/resume"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_delete_24dp"
app:tint="?android:attr/textColorPrimary" />
<ImageButton
android:id="@+id/resume"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:background="?attr/selectableItemBackgroundBorderless"
android:contentDescription="@string/action_resume"
android:padding="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:srcCompat="@drawable/ic_play_arrow_24dp"
app:tint="?android:attr/textColorPrimary" />
</androidx.constraintlayout.widget.ConstraintLayout>

View File

@ -702,6 +702,7 @@
<string name="updating_library">Updating library</string>
<!-- History fragment -->
<string name="history_prefix">Ch. %1$s - </string>
<string name="recent_manga_time">Ch. %1$s - %2$s</string>
<string name="pref_clear_history">Clear history</string>
<string name="clear_history_completed">History deleted</string>

View File

@ -344,7 +344,7 @@ class BackupTest {
private fun clearDatabase() {
db.deleteMangas().executeAsBlocking()
db.deleteHistory().executeAsBlocking()
db.dropHistoryTable().executeAsBlocking()
}
private fun getSingleHistory(chapter: Chapter): DHistory {

View File

@ -21,6 +21,9 @@ lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", ve
work-runtime = "androidx.work:work-runtime-ktx:2.6.0"
guava = "com.google.guava:guava:31.1-android"
paging-runtime = "androidx.paging:paging-runtime:3.1.1"
paging-compose = "androidx.paging:paging-compose:1.0.0-alpha14"
[bundles]
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
workmanager = ["work-runtime", "guava"]

View File

@ -0,0 +1,9 @@
[versions]
compose = "1.2.0-alpha07"
[libraries]
foundation = { module = "androidx.compose.foundation:foundation", version.ref="compose" }
material3-core = "androidx.compose.material3:material3:1.0.0-alpha09"
material3-adapter = "com.google.android.material:compose-theme-adapter-3:1.0.6"
animation = { module = "androidx.compose.animation:animation", version.ref="compose" }
ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref="compose" }

View File

@ -1,5 +1,5 @@
[versions]
kotlin_version = "1.6.20"
kotlin_version = "1.6.10"
coroutines_version = "1.6.1"
serialization_version = "1.3.2"

View File

@ -49,6 +49,7 @@ injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
coil-core = { module = "io.coil-kt:coil", version.ref = "coil_version" }
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil_version" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil_version" }
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:846abe0"
image-decoder = "com.github.tachiyomiorg:image-decoder:7481a4a"
@ -100,12 +101,12 @@ okhttp = ["okhttp-core","okhttp-logging","okhttp-dnsoverhttps"]
js-engine = ["quickjs-android", "duktape-android"]
sqlite = ["sqlitektx", "sqlite-android"]
nucleus = ["nucleus-core","nucleus-supportv7"]
coil = ["coil-core","coil-gif",]
coil = ["coil-core","coil-gif","coil-compose"]
flowbinding = ["flowbinding-android","flowbinding-appcompat","flowbinding-recyclerview","flowbinding-swiperefreshlayout","flowbinding-viewpager"]
conductor = ["conductor-core","conductor-viewpager","conductor-support-preference"]
shizuku = ["shizuku-api","shizuku-provider"]
robolectric = ["robolectric-core","robolectric-playservices"]
[plugins]
kotlinter = { id = "org.jmailen.kotlinter", version = "3.10.0"}
kotlinter = { id = "org.jmailen.kotlinter", version = "3.6.0"}
versionsx = { id = "com.github.ben-manes.versions", version = "0.42.0"}

View File

@ -22,6 +22,9 @@ dependencyResolutionManagement {
create("androidx") {
from(files("gradle/androidx.versions.toml"))
}
create("compose") {
from(files("gradle/compose.versions.toml"))
}
}
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {