mirror of
https://github.com/aniyomiorg/aniyomi.git
synced 2024-11-25 22:29:45 +03:00
Migrate History screen database calls to SQLDelight (#6933)
* Migrate History screen database call to SQLDelight - Move all migrations to SQLDelight - Move all tables to SQLDelight Co-authored-by: inorichi <3521738+inorichi@users.noreply.github.com> * Changes from review comments * Add adapters to database * Remove logging of database version in App * Change query name for paging source queries * Update migrations * Make SQLite Callback handle migration - To ensure it updates the database * Use SQLDelight Schema version for Callback database version Co-authored-by: inorichi <3521738+inorichi@users.noreply.github.com>
This commit is contained in:
parent
6c1565a7d4
commit
b1f46ed830
62 changed files with 1069 additions and 570 deletions
|
@ -6,6 +6,7 @@ plugins {
|
||||||
kotlin("android")
|
kotlin("android")
|
||||||
kotlin("plugin.serialization")
|
kotlin("plugin.serialization")
|
||||||
id("com.github.zellius.shortcut-helper")
|
id("com.github.zellius.shortcut-helper")
|
||||||
|
id("com.squareup.sqldelight")
|
||||||
}
|
}
|
||||||
|
|
||||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||||
|
@ -147,6 +148,9 @@ dependencies {
|
||||||
implementation(androidx.paging.runtime)
|
implementation(androidx.paging.runtime)
|
||||||
implementation(androidx.paging.compose)
|
implementation(androidx.paging.compose)
|
||||||
|
|
||||||
|
implementation(libs.sqldelight.android.driver)
|
||||||
|
implementation(libs.sqldelight.coroutines)
|
||||||
|
implementation(libs.sqldelight.android.paging)
|
||||||
|
|
||||||
implementation(kotlinx.reflect)
|
implementation(kotlinx.reflect)
|
||||||
|
|
||||||
|
|
94
app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt
Normal file
94
app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt
Normal file
|
@ -0,0 +1,94 @@
|
||||||
|
package eu.kanade.data
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
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.runtime.coroutines.asFlow
|
||||||
|
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
||||||
|
import com.squareup.sqldelight.runtime.coroutines.mapToOne
|
||||||
|
import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull
|
||||||
|
import eu.kanade.tachiyomi.Database
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Dispatchers
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
|
||||||
|
class AndroidDatabaseHandler(
|
||||||
|
val db: Database,
|
||||||
|
private val driver: SqlDriver,
|
||||||
|
val queryDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||||
|
val transactionDispatcher: CoroutineDispatcher = queryDispatcher
|
||||||
|
) : DatabaseHandler {
|
||||||
|
|
||||||
|
val suspendingTransactionId = ThreadLocal<Int>()
|
||||||
|
|
||||||
|
override suspend fun <T> await(inTransaction: Boolean, block: suspend Database.() -> T): T {
|
||||||
|
return dispatch(inTransaction, block)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun <T : Any> awaitList(
|
||||||
|
inTransaction: Boolean,
|
||||||
|
block: suspend Database.() -> Query<T>
|
||||||
|
): List<T> {
|
||||||
|
return dispatch(inTransaction) { block(db).executeAsList() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun <T : Any> awaitOne(
|
||||||
|
inTransaction: Boolean,
|
||||||
|
block: suspend Database.() -> Query<T>
|
||||||
|
): T {
|
||||||
|
return dispatch(inTransaction) { block(db).executeAsOne() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun <T : Any> awaitOneOrNull(
|
||||||
|
inTransaction: Boolean,
|
||||||
|
block: suspend Database.() -> Query<T>
|
||||||
|
): T? {
|
||||||
|
return dispatch(inTransaction) { block(db).executeAsOneOrNull() }
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>> {
|
||||||
|
return block(db).asFlow().mapToList(queryDispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T> {
|
||||||
|
return block(db).asFlow().mapToOne(queryDispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?> {
|
||||||
|
return block(db).asFlow().mapToOneOrNull(queryDispatcher)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun <T : Any> subscribeToPagingSource(
|
||||||
|
countQuery: Database.() -> Query<Long>,
|
||||||
|
transacter: Database.() -> Transacter,
|
||||||
|
queryProvider: Database.(Long, Long) -> Query<T>
|
||||||
|
): PagingSource<Long, T> {
|
||||||
|
return QueryPagingSource(
|
||||||
|
countQuery = countQuery(db),
|
||||||
|
transacter = transacter(db),
|
||||||
|
dispatcher = queryDispatcher,
|
||||||
|
queryProvider = { limit, offset ->
|
||||||
|
queryProvider.invoke(db, limit, offset)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
private suspend fun <T> dispatch(inTransaction: Boolean, block: suspend Database.() -> T): T {
|
||||||
|
// Create a transaction if needed and run the calling block inside it.
|
||||||
|
if (inTransaction) {
|
||||||
|
return withTransaction { block(db) }
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we're currently in the transaction thread, there's no need to dispatch our query.
|
||||||
|
if (driver.currentTransaction() != null) {
|
||||||
|
return block(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the current database context and run the calling block.
|
||||||
|
val context = getCurrentDatabaseContext()
|
||||||
|
return withContext(context) { block(db) }
|
||||||
|
}
|
||||||
|
}
|
20
app/src/main/java/eu/kanade/data/DatabaseAdapter.kt
Normal file
20
app/src/main/java/eu/kanade/data/DatabaseAdapter.kt
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
package eu.kanade.data
|
||||||
|
|
||||||
|
import com.squareup.sqldelight.ColumnAdapter
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
val dateAdapter = object : ColumnAdapter<Date, Long> {
|
||||||
|
override fun decode(databaseValue: Long): Date = Date(databaseValue)
|
||||||
|
override fun encode(value: Date): Long = value.time
|
||||||
|
}
|
||||||
|
|
||||||
|
private const val listOfStringsSeparator = ", "
|
||||||
|
val listOfStringsAdapter = object : ColumnAdapter<List<String>, String> {
|
||||||
|
override fun decode(databaseValue: String) =
|
||||||
|
if (databaseValue.isEmpty()) {
|
||||||
|
listOf()
|
||||||
|
} else {
|
||||||
|
databaseValue.split(listOfStringsSeparator)
|
||||||
|
}
|
||||||
|
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsSeparator)
|
||||||
|
}
|
39
app/src/main/java/eu/kanade/data/DatabaseHandler.kt
Normal file
39
app/src/main/java/eu/kanade/data/DatabaseHandler.kt
Normal file
|
@ -0,0 +1,39 @@
|
||||||
|
package eu.kanade.data
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import com.squareup.sqldelight.Query
|
||||||
|
import com.squareup.sqldelight.Transacter
|
||||||
|
import eu.kanade.tachiyomi.Database
|
||||||
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
|
interface DatabaseHandler {
|
||||||
|
|
||||||
|
suspend fun <T> await(inTransaction: Boolean = false, block: suspend Database.() -> T): T
|
||||||
|
|
||||||
|
suspend fun <T : Any> awaitList(
|
||||||
|
inTransaction: Boolean = false,
|
||||||
|
block: suspend Database.() -> Query<T>
|
||||||
|
): List<T>
|
||||||
|
|
||||||
|
suspend fun <T : Any> awaitOne(
|
||||||
|
inTransaction: Boolean = false,
|
||||||
|
block: suspend Database.() -> Query<T>
|
||||||
|
): T
|
||||||
|
|
||||||
|
suspend fun <T : Any> awaitOneOrNull(
|
||||||
|
inTransaction: Boolean = false,
|
||||||
|
block: suspend Database.() -> Query<T>
|
||||||
|
): T?
|
||||||
|
|
||||||
|
fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>>
|
||||||
|
|
||||||
|
fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T>
|
||||||
|
|
||||||
|
fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?>
|
||||||
|
|
||||||
|
fun <T : Any> subscribeToPagingSource(
|
||||||
|
countQuery: Database.() -> Query<Long>,
|
||||||
|
transacter: Database.() -> Transacter,
|
||||||
|
queryProvider: Database.(Long, Long) -> Query<T>
|
||||||
|
): PagingSource<Long, T>
|
||||||
|
}
|
160
app/src/main/java/eu/kanade/data/TransactionContext.kt
Normal file
160
app/src/main/java/eu/kanade/data/TransactionContext.kt
Normal file
|
@ -0,0 +1,160 @@
|
||||||
|
package eu.kanade.data
|
||||||
|
|
||||||
|
import kotlinx.coroutines.CoroutineDispatcher
|
||||||
|
import kotlinx.coroutines.Job
|
||||||
|
import kotlinx.coroutines.asContextElement
|
||||||
|
import kotlinx.coroutines.runBlocking
|
||||||
|
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||||
|
import kotlinx.coroutines.withContext
|
||||||
|
import java.util.concurrent.RejectedExecutionException
|
||||||
|
import java.util.concurrent.atomic.AtomicInteger
|
||||||
|
import kotlin.coroutines.ContinuationInterceptor
|
||||||
|
import kotlin.coroutines.CoroutineContext
|
||||||
|
import kotlin.coroutines.EmptyCoroutineContext
|
||||||
|
import kotlin.coroutines.coroutineContext
|
||||||
|
import kotlin.coroutines.resume
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the transaction dispatcher if we are on a transaction, or the database dispatchers.
|
||||||
|
*/
|
||||||
|
internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): CoroutineContext {
|
||||||
|
return coroutineContext[TransactionElement]?.transactionDispatcher ?: queryDispatcher
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Calls the specified suspending [block] in a database transaction. The transaction will be
|
||||||
|
* marked as successful unless an exception is thrown in the suspending [block] or the coroutine
|
||||||
|
* is cancelled.
|
||||||
|
*
|
||||||
|
* SQLDelight will only perform at most one transaction at a time, additional transactions are queued
|
||||||
|
* and executed on a first come, first serve order.
|
||||||
|
*
|
||||||
|
* Performing blocking database operations is not permitted in a coroutine scope other than the
|
||||||
|
* one received by the suspending block. It is recommended that all [Dao] function invoked within
|
||||||
|
* the [block] be suspending functions.
|
||||||
|
*
|
||||||
|
* The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor.
|
||||||
|
*/
|
||||||
|
internal suspend fun <T> AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T {
|
||||||
|
// Use inherited transaction context if available, this allows nested suspending transactions.
|
||||||
|
val transactionContext =
|
||||||
|
coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext()
|
||||||
|
return withContext(transactionContext) {
|
||||||
|
val transactionElement = coroutineContext[TransactionElement]!!
|
||||||
|
transactionElement.acquire()
|
||||||
|
try {
|
||||||
|
db.transactionWithResult {
|
||||||
|
runBlocking(transactionContext) {
|
||||||
|
block()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
transactionElement.release()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a [CoroutineContext] for performing database operations within a coroutine transaction.
|
||||||
|
*
|
||||||
|
* The context is a combination of a dispatcher, a [TransactionElement] and a thread local element.
|
||||||
|
*
|
||||||
|
* * The dispatcher will dispatch coroutines to a single thread that is taken over from the SQLDelight
|
||||||
|
* query executor. If the coroutine context is switched, suspending DAO functions will be able to
|
||||||
|
* dispatch to the transaction thread.
|
||||||
|
*
|
||||||
|
* * The [TransactionElement] serves as an indicator for inherited context, meaning, if there is a
|
||||||
|
* switch of context, suspending DAO methods will be able to use the indicator to dispatch the
|
||||||
|
* database operation to the transaction thread.
|
||||||
|
*
|
||||||
|
* * The thread local element serves as a second indicator and marks threads that are used to
|
||||||
|
* execute coroutines within the coroutine transaction, more specifically it allows us to identify
|
||||||
|
* if a blocking DAO method is invoked within the transaction coroutine. Never assign meaning to
|
||||||
|
* this value, for now all we care is if its present or not.
|
||||||
|
*/
|
||||||
|
private suspend fun AndroidDatabaseHandler.createTransactionContext(): CoroutineContext {
|
||||||
|
val controlJob = Job()
|
||||||
|
// make sure to tie the control job to this context to avoid blocking the transaction if
|
||||||
|
// context get cancelled before we can even start using this job. Otherwise, the acquired
|
||||||
|
// transaction thread will forever wait for the controlJob to be cancelled.
|
||||||
|
// see b/148181325
|
||||||
|
coroutineContext[Job]?.invokeOnCompletion {
|
||||||
|
controlJob.cancel()
|
||||||
|
}
|
||||||
|
|
||||||
|
val dispatcher = transactionDispatcher.acquireTransactionThread(controlJob)
|
||||||
|
val transactionElement = TransactionElement(controlJob, dispatcher)
|
||||||
|
val threadLocalElement =
|
||||||
|
suspendingTransactionId.asContextElement(System.identityHashCode(controlJob))
|
||||||
|
return dispatcher + transactionElement + threadLocalElement
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Acquires a thread from the executor and returns a [ContinuationInterceptor] to dispatch
|
||||||
|
* coroutines to the acquired thread. The [controlJob] is used to control the release of the
|
||||||
|
* thread by cancelling the job.
|
||||||
|
*/
|
||||||
|
private suspend fun CoroutineDispatcher.acquireTransactionThread(
|
||||||
|
controlJob: Job
|
||||||
|
): ContinuationInterceptor {
|
||||||
|
return suspendCancellableCoroutine { continuation ->
|
||||||
|
continuation.invokeOnCancellation {
|
||||||
|
// We got cancelled while waiting to acquire a thread, we can't stop our attempt to
|
||||||
|
// acquire a thread, but we can cancel the controlling job so once it gets acquired it
|
||||||
|
// is quickly released.
|
||||||
|
controlJob.cancel()
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
dispatch(EmptyCoroutineContext) {
|
||||||
|
runBlocking {
|
||||||
|
// Thread acquired, resume coroutine.
|
||||||
|
continuation.resume(coroutineContext[ContinuationInterceptor]!!)
|
||||||
|
controlJob.join()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (ex: RejectedExecutionException) {
|
||||||
|
// Couldn't acquire a thread, cancel coroutine.
|
||||||
|
continuation.cancel(
|
||||||
|
IllegalStateException(
|
||||||
|
"Unable to acquire a thread to perform the database transaction.", ex
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A [CoroutineContext.Element] that indicates there is an on-going database transaction.
|
||||||
|
*/
|
||||||
|
private class TransactionElement(
|
||||||
|
private val transactionThreadControlJob: Job,
|
||||||
|
val transactionDispatcher: ContinuationInterceptor
|
||||||
|
) : CoroutineContext.Element {
|
||||||
|
|
||||||
|
companion object Key : CoroutineContext.Key<TransactionElement>
|
||||||
|
|
||||||
|
override val key: CoroutineContext.Key<TransactionElement>
|
||||||
|
get() = TransactionElement
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Number of transactions (including nested ones) started with this element.
|
||||||
|
* Call [acquire] to increase the count and [release] to decrease it. If the count reaches zero
|
||||||
|
* when [release] is invoked then the transaction job is cancelled and the transaction thread
|
||||||
|
* is released.
|
||||||
|
*/
|
||||||
|
private val referenceCount = AtomicInteger(0)
|
||||||
|
|
||||||
|
fun acquire() {
|
||||||
|
referenceCount.incrementAndGet()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun release() {
|
||||||
|
val count = referenceCount.decrementAndGet()
|
||||||
|
if (count < 0) {
|
||||||
|
throw IllegalStateException("Transaction was never started or was already released.")
|
||||||
|
} else if (count == 0) {
|
||||||
|
// Cancel the job that controls the transaction thread, causing it to be released.
|
||||||
|
transactionThreadControlJob.cancel()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
21
app/src/main/java/eu/kanade/data/chapter/ChapterMapper.kt
Normal file
21
app/src/main/java/eu/kanade/data/chapter/ChapterMapper.kt
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
package eu.kanade.data.chapter
|
||||||
|
|
||||||
|
import eu.kanade.domain.chapter.model.Chapter
|
||||||
|
|
||||||
|
val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long, Float, Long, Long, Long) -> Chapter =
|
||||||
|
{ id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload ->
|
||||||
|
Chapter(
|
||||||
|
id = id,
|
||||||
|
mangaId = mangaId,
|
||||||
|
read = read,
|
||||||
|
bookmark = bookmark,
|
||||||
|
lastPageRead = lastPageRead,
|
||||||
|
dateFetch = dateFetch,
|
||||||
|
sourceOrder = sourceOrder,
|
||||||
|
url = url,
|
||||||
|
name = name,
|
||||||
|
dateUpload = dateUpload,
|
||||||
|
chapterNumber = chapterNumber,
|
||||||
|
scanlator = scanlator,
|
||||||
|
)
|
||||||
|
}
|
26
app/src/main/java/eu/kanade/data/history/HistoryMapper.kt
Normal file
26
app/src/main/java/eu/kanade/data/history/HistoryMapper.kt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package eu.kanade.data.history
|
||||||
|
|
||||||
|
import eu.kanade.domain.history.model.History
|
||||||
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
val historyMapper: (Long, Long, Date?, Date?) -> History = { id, chapterId, readAt, _ ->
|
||||||
|
History(
|
||||||
|
id = id,
|
||||||
|
chapterId = chapterId,
|
||||||
|
readAt = readAt,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val historyWithRelationsMapper: (Long, Long, Long, String, String?, Float, Date?) -> HistoryWithRelations = {
|
||||||
|
historyId, mangaId, chapterId, title, thumbnailUrl, chapterNumber, readAt ->
|
||||||
|
HistoryWithRelations(
|
||||||
|
id = historyId,
|
||||||
|
chapterId = chapterId,
|
||||||
|
mangaId = mangaId,
|
||||||
|
title = title,
|
||||||
|
thumbnailUrl = thumbnailUrl ?: "",
|
||||||
|
chapterNumber = chapterNumber,
|
||||||
|
readAt = readAt
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,91 @@
|
||||||
|
package eu.kanade.data.history
|
||||||
|
|
||||||
|
import androidx.paging.PagingSource
|
||||||
|
import eu.kanade.data.DatabaseHandler
|
||||||
|
import eu.kanade.data.chapter.chapterMapper
|
||||||
|
import eu.kanade.data.manga.mangaMapper
|
||||||
|
import eu.kanade.domain.chapter.model.Chapter
|
||||||
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
|
import eu.kanade.domain.history.repository.HistoryRepository
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
import eu.kanade.tachiyomi.util.system.logcat
|
||||||
|
|
||||||
|
class HistoryRepositoryImpl(
|
||||||
|
private val handler: DatabaseHandler
|
||||||
|
) : HistoryRepository {
|
||||||
|
|
||||||
|
override fun getHistory(query: String): PagingSource<Long, HistoryWithRelations> {
|
||||||
|
return handler.subscribeToPagingSource(
|
||||||
|
countQuery = { historyViewQueries.countHistory(query) },
|
||||||
|
transacter = { historyViewQueries },
|
||||||
|
queryProvider = { limit, offset ->
|
||||||
|
historyViewQueries.history(query, limit, offset, historyWithRelationsMapper)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun getNextChapterForManga(mangaId: Long, chapterId: Long): Chapter? {
|
||||||
|
val chapter = handler.awaitOne { chaptersQueries.getChapterById(chapterId, chapterMapper) }
|
||||||
|
val manga = handler.awaitOne { mangasQueries.getMangaById(mangaId, mangaMapper) }
|
||||||
|
|
||||||
|
if (!chapter.read) {
|
||||||
|
return chapter
|
||||||
|
}
|
||||||
|
|
||||||
|
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
|
||||||
|
Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.sourceOrder.compareTo(c1.sourceOrder) }
|
||||||
|
Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapterNumber.compareTo(c2.chapterNumber) }
|
||||||
|
Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.dateUpload.compareTo(c2.dateUpload) }
|
||||||
|
else -> throw NotImplementedError("Unknown sorting method")
|
||||||
|
}
|
||||||
|
|
||||||
|
val chapters = handler.awaitList { chaptersQueries.getChapterByMangaId(mangaId, chapterMapper) }
|
||||||
|
.sortedWith(sortFunction)
|
||||||
|
|
||||||
|
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.chapterNumber
|
||||||
|
|
||||||
|
((currChapterIndex + 1) until chapters.size)
|
||||||
|
.map { chapters[it] }
|
||||||
|
.firstOrNull {
|
||||||
|
it.chapterNumber > chapterNumber &&
|
||||||
|
it.chapterNumber <= chapterNumber + 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Manga.CHAPTER_SORTING_UPLOAD_DATE -> {
|
||||||
|
chapters.drop(currChapterIndex + 1)
|
||||||
|
.firstOrNull { it.dateUpload >= chapter.dateUpload }
|
||||||
|
}
|
||||||
|
else -> throw NotImplementedError("Unknown sorting method")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun resetHistory(historyId: Long) {
|
||||||
|
try {
|
||||||
|
handler.await { historyQueries.resetHistoryById(historyId) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(throwable = e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun resetHistoryByMangaId(mangaId: Long) {
|
||||||
|
try {
|
||||||
|
handler.await { historyQueries.resetHistoryByMangaId(mangaId) }
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(throwable = e)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override suspend fun deleteAllHistory(): Boolean {
|
||||||
|
return try {
|
||||||
|
handler.await { historyQueries.removeAllHistory() }
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
logcat(throwable = e)
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,43 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -1,137 +0,0 @@
|
||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
26
app/src/main/java/eu/kanade/data/manga/MangaMapper.kt
Normal file
26
app/src/main/java/eu/kanade/data/manga/MangaMapper.kt
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
package eu.kanade.data.manga
|
||||||
|
|
||||||
|
import eu.kanade.domain.manga.model.Manga
|
||||||
|
|
||||||
|
val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long) -> Manga =
|
||||||
|
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewer, chapterFlags, coverLastModified, dateAdded ->
|
||||||
|
Manga(
|
||||||
|
id = id,
|
||||||
|
source = source,
|
||||||
|
favorite = favorite,
|
||||||
|
lastUpdate = lastUpdate ?: 0,
|
||||||
|
dateAdded = dateAdded,
|
||||||
|
viewerFlags = viewer,
|
||||||
|
chapterFlags = chapterFlags,
|
||||||
|
coverLastModified = coverLastModified,
|
||||||
|
url = url,
|
||||||
|
title = title,
|
||||||
|
artist = artist,
|
||||||
|
author = author,
|
||||||
|
description = description,
|
||||||
|
genre = genre,
|
||||||
|
status = status,
|
||||||
|
thumbnailUrl = thumbnailUrl,
|
||||||
|
initialized = initialized,
|
||||||
|
)
|
||||||
|
}
|
|
@ -1,6 +1,6 @@
|
||||||
package eu.kanade.domain
|
package eu.kanade.domain
|
||||||
|
|
||||||
import eu.kanade.data.history.repository.HistoryRepositoryImpl
|
import eu.kanade.data.history.HistoryRepositoryImpl
|
||||||
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.GetNextChapterForManga
|
import eu.kanade.domain.history.interactor.GetNextChapterForManga
|
||||||
|
|
16
app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt
Normal file
16
app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt
Normal file
|
@ -0,0 +1,16 @@
|
||||||
|
package eu.kanade.domain.chapter.model
|
||||||
|
|
||||||
|
data class Chapter(
|
||||||
|
val id: Long,
|
||||||
|
val mangaId: Long,
|
||||||
|
val read: Boolean,
|
||||||
|
val bookmark: Boolean,
|
||||||
|
val lastPageRead: Long,
|
||||||
|
val dateFetch: Long,
|
||||||
|
val sourceOrder: Long,
|
||||||
|
val url: String,
|
||||||
|
val name: String,
|
||||||
|
val dateUpload: Long,
|
||||||
|
val chapterNumber: Float,
|
||||||
|
val scanlator: String?
|
||||||
|
)
|
|
@ -3,18 +3,17 @@ package eu.kanade.domain.history.interactor
|
||||||
import androidx.paging.Pager
|
import androidx.paging.Pager
|
||||||
import androidx.paging.PagingConfig
|
import androidx.paging.PagingConfig
|
||||||
import androidx.paging.PagingData
|
import androidx.paging.PagingData
|
||||||
import eu.kanade.data.history.local.HistoryPagingSource
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
import eu.kanade.domain.history.repository.HistoryRepository
|
import eu.kanade.domain.history.repository.HistoryRepository
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
|
||||||
import kotlinx.coroutines.flow.Flow
|
import kotlinx.coroutines.flow.Flow
|
||||||
|
|
||||||
class GetHistory(
|
class GetHistory(
|
||||||
private val repository: HistoryRepository
|
private val repository: HistoryRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
fun subscribe(query: String): Flow<PagingData<MangaChapterHistory>> {
|
fun subscribe(query: String): Flow<PagingData<HistoryWithRelations>> {
|
||||||
return Pager(
|
return Pager(
|
||||||
PagingConfig(pageSize = HistoryPagingSource.PAGE_SIZE)
|
PagingConfig(pageSize = 25)
|
||||||
) {
|
) {
|
||||||
repository.getHistory(query)
|
repository.getHistory(query)
|
||||||
}.flow
|
}.flow
|
||||||
|
|
|
@ -1,14 +1,13 @@
|
||||||
package eu.kanade.domain.history.interactor
|
package eu.kanade.domain.history.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.chapter.model.Chapter
|
||||||
import eu.kanade.domain.history.repository.HistoryRepository
|
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(
|
class GetNextChapterForManga(
|
||||||
private val repository: HistoryRepository
|
private val repository: HistoryRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(manga: Manga, chapter: Chapter): Chapter? {
|
suspend fun await(mangaId: Long, chapterId: Long): Chapter? {
|
||||||
return repository.getNextChapterForManga(manga, chapter)
|
return repository.getNextChapterForManga(mangaId, chapterId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,21 +1,13 @@
|
||||||
package eu.kanade.domain.history.interactor
|
package eu.kanade.domain.history.interactor
|
||||||
|
|
||||||
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
import eu.kanade.domain.history.repository.HistoryRepository
|
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(
|
class RemoveHistoryById(
|
||||||
private val repository: HistoryRepository
|
private val repository: HistoryRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(history: History): Boolean {
|
suspend fun await(history: HistoryWithRelations) {
|
||||||
// Workaround for list not freaking out when changing reference varaible
|
repository.resetHistory(history.id)
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,7 +6,7 @@ class RemoveHistoryByMangaId(
|
||||||
private val repository: HistoryRepository
|
private val repository: HistoryRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
suspend fun await(mangaId: Long): Boolean {
|
suspend fun await(mangaId: Long) {
|
||||||
return repository.resetHistoryByMangaId(mangaId)
|
repository.resetHistoryByMangaId(mangaId)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,9 @@
|
||||||
|
package eu.kanade.domain.history.model
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class History(
|
||||||
|
val id: Long?,
|
||||||
|
val chapterId: Long,
|
||||||
|
val readAt: Date?
|
||||||
|
)
|
|
@ -0,0 +1,13 @@
|
||||||
|
package eu.kanade.domain.history.model
|
||||||
|
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
data class HistoryWithRelations(
|
||||||
|
val id: Long,
|
||||||
|
val chapterId: Long,
|
||||||
|
val mangaId: Long,
|
||||||
|
val title: String,
|
||||||
|
val thumbnailUrl: String,
|
||||||
|
val chapterNumber: Float,
|
||||||
|
val readAt: Date?
|
||||||
|
)
|
|
@ -1,22 +1,18 @@
|
||||||
package eu.kanade.domain.history.repository
|
package eu.kanade.domain.history.repository
|
||||||
|
|
||||||
import eu.kanade.data.history.local.HistoryPagingSource
|
import androidx.paging.PagingSource
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
import eu.kanade.domain.chapter.model.Chapter
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
|
||||||
|
|
||||||
interface HistoryRepository {
|
interface HistoryRepository {
|
||||||
|
|
||||||
fun getHistory(query: String): HistoryPagingSource
|
fun getHistory(query: String): PagingSource<Long, HistoryWithRelations>
|
||||||
|
|
||||||
suspend fun getHistory(limit: Int, page: Int, query: String): List<MangaChapterHistory>
|
suspend fun getNextChapterForManga(mangaId: Long, chapterId: Long): Chapter?
|
||||||
|
|
||||||
suspend fun getNextChapterForManga(manga: Manga, chapter: Chapter): Chapter?
|
suspend fun resetHistory(historyId: Long)
|
||||||
|
|
||||||
suspend fun resetHistory(history: History): Boolean
|
suspend fun resetHistoryByMangaId(mangaId: Long)
|
||||||
|
|
||||||
suspend fun resetHistoryByMangaId(mangaId: Long): Boolean
|
|
||||||
|
|
||||||
suspend fun deleteAllHistory(): Boolean
|
suspend fun deleteAllHistory(): Boolean
|
||||||
}
|
}
|
||||||
|
|
36
app/src/main/java/eu/kanade/domain/manga/model/Manga.kt
Normal file
36
app/src/main/java/eu/kanade/domain/manga/model/Manga.kt
Normal file
|
@ -0,0 +1,36 @@
|
||||||
|
package eu.kanade.domain.manga.model
|
||||||
|
|
||||||
|
data class Manga(
|
||||||
|
val id: Long,
|
||||||
|
val source: Long,
|
||||||
|
val favorite: Boolean,
|
||||||
|
val lastUpdate: Long,
|
||||||
|
val dateAdded: Long,
|
||||||
|
val viewerFlags: Long,
|
||||||
|
val chapterFlags: Long,
|
||||||
|
val coverLastModified: Long,
|
||||||
|
val url: String,
|
||||||
|
val title: String,
|
||||||
|
val artist: String?,
|
||||||
|
val author: String?,
|
||||||
|
val description: String?,
|
||||||
|
val genre: List<String>?,
|
||||||
|
val status: Long,
|
||||||
|
val thumbnailUrl: String?,
|
||||||
|
val initialized: Boolean
|
||||||
|
) {
|
||||||
|
|
||||||
|
val sorting: Long
|
||||||
|
get() = chapterFlags and CHAPTER_SORTING_MASK
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
// Generic filter that does not filter anything
|
||||||
|
const val SHOW_ALL = 0x00000000L
|
||||||
|
|
||||||
|
const val CHAPTER_SORTING_SOURCE = 0x00000000L
|
||||||
|
const val CHAPTER_SORTING_NUMBER = 0x00000100L
|
||||||
|
const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200L
|
||||||
|
const val CHAPTER_SORTING_MASK = 0x00000300L
|
||||||
|
}
|
||||||
|
}
|
|
@ -9,7 +9,6 @@ import androidx.compose.ui.graphics.Shape
|
||||||
import androidx.compose.ui.layout.ContentScale
|
import androidx.compose.ui.layout.ContentScale
|
||||||
import androidx.compose.ui.unit.dp
|
import androidx.compose.ui.unit.dp
|
||||||
import coil.compose.AsyncImage
|
import coil.compose.AsyncImage
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
|
||||||
|
|
||||||
enum class MangaCoverAspect(val ratio: Float) {
|
enum class MangaCoverAspect(val ratio: Float) {
|
||||||
SQUARE(1f / 1f),
|
SQUARE(1f / 1f),
|
||||||
|
@ -19,13 +18,13 @@ enum class MangaCoverAspect(val ratio: Float) {
|
||||||
@Composable
|
@Composable
|
||||||
fun MangaCover(
|
fun MangaCover(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
manga: Manga,
|
data: String?,
|
||||||
aspect: MangaCoverAspect,
|
aspect: MangaCoverAspect,
|
||||||
contentDescription: String = "",
|
contentDescription: String = "",
|
||||||
shape: Shape = RoundedCornerShape(4.dp)
|
shape: Shape = RoundedCornerShape(4.dp)
|
||||||
) {
|
) {
|
||||||
AsyncImage(
|
AsyncImage(
|
||||||
model = manga,
|
model = data,
|
||||||
contentDescription = contentDescription,
|
contentDescription = contentDescription,
|
||||||
modifier = modifier
|
modifier = modifier
|
||||||
.aspectRatio(aspect.ratio)
|
.aspectRatio(aspect.ratio)
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.presentation.history
|
package eu.kanade.presentation.history
|
||||||
|
|
||||||
import androidx.compose.foundation.clickable
|
import androidx.compose.foundation.clickable
|
||||||
|
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.Spacer
|
import androidx.compose.foundation.layout.Spacer
|
||||||
|
@ -42,13 +43,13 @@ import androidx.core.text.buildSpannedString
|
||||||
import androidx.paging.compose.LazyPagingItems
|
import androidx.paging.compose.LazyPagingItems
|
||||||
import androidx.paging.compose.collectAsLazyPagingItems
|
import androidx.paging.compose.collectAsLazyPagingItems
|
||||||
import androidx.paging.compose.items
|
import androidx.paging.compose.items
|
||||||
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
import eu.kanade.presentation.components.EmptyScreen
|
import eu.kanade.presentation.components.EmptyScreen
|
||||||
import eu.kanade.presentation.components.MangaCover
|
import eu.kanade.presentation.components.MangaCover
|
||||||
import eu.kanade.presentation.components.MangaCoverAspect
|
import eu.kanade.presentation.components.MangaCoverAspect
|
||||||
import eu.kanade.presentation.theme.TachiyomiTheme
|
import eu.kanade.presentation.theme.TachiyomiTheme
|
||||||
import eu.kanade.presentation.util.horizontalPadding
|
import eu.kanade.presentation.util.horizontalPadding
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
|
||||||
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.UiModel
|
import eu.kanade.tachiyomi.ui.recent.history.UiModel
|
||||||
|
@ -71,9 +72,9 @@ val chapterFormatter = DecimalFormat(
|
||||||
fun HistoryScreen(
|
fun HistoryScreen(
|
||||||
composeView: ComposeView,
|
composeView: ComposeView,
|
||||||
presenter: HistoryPresenter,
|
presenter: HistoryPresenter,
|
||||||
onClickItem: (MangaChapterHistory) -> Unit,
|
onClickItem: (HistoryWithRelations) -> Unit,
|
||||||
onClickResume: (MangaChapterHistory) -> Unit,
|
onClickResume: (HistoryWithRelations) -> Unit,
|
||||||
onClickDelete: (MangaChapterHistory, Boolean) -> Unit,
|
onClickDelete: (HistoryWithRelations, Boolean) -> Unit,
|
||||||
) {
|
) {
|
||||||
val nestedSrollInterop = rememberNestedScrollInteropConnection(composeView)
|
val nestedSrollInterop = rememberNestedScrollInteropConnection(composeView)
|
||||||
TachiyomiTheme {
|
TachiyomiTheme {
|
||||||
|
@ -104,16 +105,16 @@ fun HistoryScreen(
|
||||||
@Composable
|
@Composable
|
||||||
fun HistoryContent(
|
fun HistoryContent(
|
||||||
history: LazyPagingItems<UiModel>,
|
history: LazyPagingItems<UiModel>,
|
||||||
onClickItem: (MangaChapterHistory) -> Unit,
|
onClickItem: (HistoryWithRelations) -> Unit,
|
||||||
onClickResume: (MangaChapterHistory) -> Unit,
|
onClickResume: (HistoryWithRelations) -> Unit,
|
||||||
onClickDelete: (MangaChapterHistory, Boolean) -> Unit,
|
onClickDelete: (HistoryWithRelations, Boolean) -> Unit,
|
||||||
preferences: PreferencesHelper = Injekt.get(),
|
preferences: PreferencesHelper = Injekt.get(),
|
||||||
nestedScroll: NestedScrollConnection
|
nestedScroll: NestedScrollConnection
|
||||||
) {
|
) {
|
||||||
val relativeTime: Int = remember { preferences.relativeTime().get() }
|
val relativeTime: Int = remember { preferences.relativeTime().get() }
|
||||||
val dateFormat: DateFormat = remember { preferences.dateFormat() }
|
val dateFormat: DateFormat = remember { preferences.dateFormat() }
|
||||||
|
|
||||||
val (removeState, setRemoveState) = remember { mutableStateOf<MangaChapterHistory?>(null) }
|
val (removeState, setRemoveState) = remember { mutableStateOf<HistoryWithRelations?>(null) }
|
||||||
|
|
||||||
val scrollState = rememberLazyListState()
|
val scrollState = rememberLazyListState()
|
||||||
LazyColumn(
|
LazyColumn(
|
||||||
|
@ -132,7 +133,7 @@ fun HistoryContent(
|
||||||
dateFormat = dateFormat
|
dateFormat = dateFormat
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
is UiModel.History -> {
|
is UiModel.Item -> {
|
||||||
val value = item.item
|
val value = item.item
|
||||||
HistoryItem(
|
HistoryItem(
|
||||||
modifier = Modifier.animateItemPlacement(),
|
modifier = Modifier.animateItemPlacement(),
|
||||||
|
@ -189,7 +190,7 @@ fun HistoryHeader(
|
||||||
@Composable
|
@Composable
|
||||||
fun HistoryItem(
|
fun HistoryItem(
|
||||||
modifier: Modifier = Modifier,
|
modifier: Modifier = Modifier,
|
||||||
history: MangaChapterHistory,
|
history: HistoryWithRelations,
|
||||||
onClickItem: () -> Unit,
|
onClickItem: () -> Unit,
|
||||||
onClickResume: () -> Unit,
|
onClickResume: () -> Unit,
|
||||||
onClickDelete: () -> Unit,
|
onClickDelete: () -> Unit,
|
||||||
|
@ -203,7 +204,7 @@ fun HistoryItem(
|
||||||
) {
|
) {
|
||||||
MangaCover(
|
MangaCover(
|
||||||
modifier = Modifier.fillMaxHeight(),
|
modifier = Modifier.fillMaxHeight(),
|
||||||
manga = history.manga,
|
data = history.thumbnailUrl,
|
||||||
aspect = MangaCoverAspect.COVER
|
aspect = MangaCoverAspect.COVER
|
||||||
)
|
)
|
||||||
Column(
|
Column(
|
||||||
|
@ -215,7 +216,7 @@ fun HistoryItem(
|
||||||
color = MaterialTheme.colorScheme.onSurface,
|
color = MaterialTheme.colorScheme.onSurface,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
text = history.manga.title,
|
text = history.title,
|
||||||
maxLines = 2,
|
maxLines = 2,
|
||||||
overflow = TextOverflow.Ellipsis,
|
overflow = TextOverflow.Ellipsis,
|
||||||
style = textStyle.copy(fontWeight = FontWeight.SemiBold)
|
style = textStyle.copy(fontWeight = FontWeight.SemiBold)
|
||||||
|
@ -223,15 +224,15 @@ fun HistoryItem(
|
||||||
Row {
|
Row {
|
||||||
Text(
|
Text(
|
||||||
text = buildSpannedString {
|
text = buildSpannedString {
|
||||||
if (history.chapter.chapter_number > -1) {
|
if (history.chapterNumber > -1) {
|
||||||
append(
|
append(
|
||||||
stringResource(
|
stringResource(
|
||||||
R.string.history_prefix,
|
R.string.history_prefix,
|
||||||
chapterFormatter.format(history.chapter.chapter_number)
|
chapterFormatter.format(history.chapterNumber)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
append(Date(history.history.last_read).toTimestampString())
|
append(history.readAt?.toTimestampString())
|
||||||
}.toString(),
|
}.toString(),
|
||||||
modifier = Modifier.padding(top = 2.dp),
|
modifier = Modifier.padding(top = 2.dp),
|
||||||
style = textStyle
|
style = textStyle
|
||||||
|
@ -270,14 +271,22 @@ fun RemoveHistoryDialog(
|
||||||
Column {
|
Column {
|
||||||
Text(text = stringResource(id = R.string.dialog_with_checkbox_remove_description))
|
Text(text = stringResource(id = R.string.dialog_with_checkbox_remove_description))
|
||||||
Row(
|
Row(
|
||||||
modifier = Modifier.toggleable(value = removeEverything, onValueChange = removeEverythingState),
|
modifier = Modifier
|
||||||
|
.padding(top = 16.dp)
|
||||||
|
.toggleable(
|
||||||
|
interactionSource = remember { MutableInteractionSource() },
|
||||||
|
indication = null,
|
||||||
|
value = removeEverything,
|
||||||
|
onValueChange = removeEverythingState
|
||||||
|
),
|
||||||
verticalAlignment = Alignment.CenterVertically
|
verticalAlignment = Alignment.CenterVertically
|
||||||
) {
|
) {
|
||||||
Checkbox(
|
Checkbox(
|
||||||
checked = removeEverything,
|
checked = removeEverything,
|
||||||
onCheckedChange = removeEverythingState,
|
onCheckedChange = null,
|
||||||
)
|
)
|
||||||
Text(
|
Text(
|
||||||
|
modifier = Modifier.padding(start = 4.dp),
|
||||||
text = stringResource(id = R.string.dialog_with_checkbox_reset)
|
text = stringResource(id = R.string.dialog_with_checkbox_reset)
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,9 +2,18 @@ package eu.kanade.tachiyomi
|
||||||
|
|
||||||
import android.app.Application
|
import android.app.Application
|
||||||
import androidx.core.content.ContextCompat
|
import androidx.core.content.ContextCompat
|
||||||
|
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||||
|
import com.squareup.sqldelight.db.SqlDriver
|
||||||
|
import data.History
|
||||||
|
import data.Mangas
|
||||||
|
import eu.kanade.data.AndroidDatabaseHandler
|
||||||
|
import eu.kanade.data.DatabaseHandler
|
||||||
|
import eu.kanade.data.dateAdapter
|
||||||
|
import eu.kanade.data.listOfStringsAdapter
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
import eu.kanade.tachiyomi.data.database.DbOpenCallback
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
import eu.kanade.tachiyomi.data.saver.ImageSaver
|
||||||
|
@ -25,11 +34,37 @@ class AppModule(val app: Application) : InjektModule {
|
||||||
override fun InjektRegistrar.registerInjectables() {
|
override fun InjektRegistrar.registerInjectables() {
|
||||||
addSingleton(app)
|
addSingleton(app)
|
||||||
|
|
||||||
|
addSingletonFactory { DbOpenCallback() }
|
||||||
|
|
||||||
|
addSingletonFactory<SqlDriver> {
|
||||||
|
AndroidSqliteDriver(
|
||||||
|
schema = Database.Schema,
|
||||||
|
context = app,
|
||||||
|
name = DbOpenCallback.DATABASE_NAME,
|
||||||
|
callback = get<DbOpenCallback>()
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addSingletonFactory {
|
||||||
|
Database(
|
||||||
|
driver = get(),
|
||||||
|
historyAdapter = History.Adapter(
|
||||||
|
history_last_readAdapter = dateAdapter,
|
||||||
|
history_time_readAdapter = dateAdapter
|
||||||
|
),
|
||||||
|
mangasAdapter = Mangas.Adapter(
|
||||||
|
genreAdapter = listOfStringsAdapter
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
addSingletonFactory<DatabaseHandler> { AndroidDatabaseHandler(get(), get()) }
|
||||||
|
|
||||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||||
|
|
||||||
addSingletonFactory { PreferencesHelper(app) }
|
addSingletonFactory { PreferencesHelper(app) }
|
||||||
|
|
||||||
addSingletonFactory { DatabaseHelper(app) }
|
addSingletonFactory { DatabaseHelper(app, get()) }
|
||||||
|
|
||||||
addSingletonFactory { ChapterCache(app) }
|
addSingletonFactory { ChapterCache(app) }
|
||||||
|
|
||||||
|
@ -57,6 +92,8 @@ class AppModule(val app: Application) : InjektModule {
|
||||||
|
|
||||||
get<SourceManager>()
|
get<SourceManager>()
|
||||||
|
|
||||||
|
get<Database>()
|
||||||
|
|
||||||
get<DatabaseHelper>()
|
get<DatabaseHelper>()
|
||||||
|
|
||||||
get<DownloadManager>()
|
get<DownloadManager>()
|
||||||
|
|
|
@ -26,12 +26,15 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||||
/**
|
/**
|
||||||
* This class provides operations to manage the database through its interfaces.
|
* This class provides operations to manage the database through its interfaces.
|
||||||
*/
|
*/
|
||||||
open class DatabaseHelper(context: Context) :
|
open class DatabaseHelper(
|
||||||
|
context: Context,
|
||||||
|
callback: DbOpenCallback
|
||||||
|
) :
|
||||||
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
|
||||||
|
|
||||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||||
.name(DbOpenCallback.DATABASE_NAME)
|
.name(DbOpenCallback.DATABASE_NAME)
|
||||||
.callback(DbOpenCallback())
|
.callback(callback)
|
||||||
.build()
|
.build()
|
||||||
|
|
||||||
override val db = DefaultStorIOSQLite.builder()
|
override val db = DefaultStorIOSQLite.builder()
|
||||||
|
|
|
@ -2,98 +2,28 @@ package eu.kanade.tachiyomi.data.database
|
||||||
|
|
||||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
|
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
|
import eu.kanade.tachiyomi.Database
|
||||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.TrackTable
|
|
||||||
|
|
||||||
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
|
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(Database.Schema.version) {
|
||||||
|
|
||||||
companion object {
|
companion object {
|
||||||
/**
|
/**
|
||||||
* Name of the database file.
|
* Name of the database file.
|
||||||
*/
|
*/
|
||||||
const val DATABASE_NAME = "tachiyomi.db"
|
const val DATABASE_NAME = "tachiyomi.db"
|
||||||
|
|
||||||
/**
|
|
||||||
* Version of the database.
|
|
||||||
*/
|
|
||||||
const val DATABASE_VERSION = 14
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
|
override fun onCreate(db: SupportSQLiteDatabase) {
|
||||||
execSQL(MangaTable.createTableQuery)
|
Database.Schema.create(AndroidSqliteDriver(database = db, cacheSize = 1))
|
||||||
execSQL(ChapterTable.createTableQuery)
|
|
||||||
execSQL(TrackTable.createTableQuery)
|
|
||||||
execSQL(CategoryTable.createTableQuery)
|
|
||||||
execSQL(MangaCategoryTable.createTableQuery)
|
|
||||||
execSQL(HistoryTable.createTableQuery)
|
|
||||||
|
|
||||||
// DB indexes
|
|
||||||
execSQL(MangaTable.createUrlIndexQuery)
|
|
||||||
execSQL(MangaTable.createLibraryIndexQuery)
|
|
||||||
execSQL(ChapterTable.createMangaIdIndexQuery)
|
|
||||||
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
|
||||||
execSQL(HistoryTable.createChapterIdIndexQuery)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||||
if (oldVersion < 2) {
|
Database.Schema.migrate(
|
||||||
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
|
driver = AndroidSqliteDriver(database = db, cacheSize = 1),
|
||||||
|
oldVersion = oldVersion,
|
||||||
// Fix kissmanga covers after supporting cloudflare
|
newVersion = newVersion
|
||||||
db.execSQL(
|
)
|
||||||
"""UPDATE mangas SET thumbnail_url =
|
|
||||||
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""",
|
|
||||||
)
|
|
||||||
}
|
|
||||||
if (oldVersion < 3) {
|
|
||||||
// Initialize history tables
|
|
||||||
db.execSQL(HistoryTable.createTableQuery)
|
|
||||||
db.execSQL(HistoryTable.createChapterIdIndexQuery)
|
|
||||||
}
|
|
||||||
if (oldVersion < 4) {
|
|
||||||
db.execSQL(ChapterTable.bookmarkUpdateQuery)
|
|
||||||
}
|
|
||||||
if (oldVersion < 5) {
|
|
||||||
db.execSQL(ChapterTable.addScanlator)
|
|
||||||
}
|
|
||||||
if (oldVersion < 6) {
|
|
||||||
db.execSQL(TrackTable.addTrackingUrl)
|
|
||||||
}
|
|
||||||
if (oldVersion < 7) {
|
|
||||||
db.execSQL(TrackTable.addLibraryId)
|
|
||||||
}
|
|
||||||
if (oldVersion < 8) {
|
|
||||||
db.execSQL("DROP INDEX IF EXISTS mangas_favorite_index")
|
|
||||||
db.execSQL(MangaTable.createLibraryIndexQuery)
|
|
||||||
db.execSQL(ChapterTable.createUnreadChaptersIndexQuery)
|
|
||||||
}
|
|
||||||
if (oldVersion < 9) {
|
|
||||||
db.execSQL(TrackTable.addStartDate)
|
|
||||||
db.execSQL(TrackTable.addFinishDate)
|
|
||||||
}
|
|
||||||
if (oldVersion < 10) {
|
|
||||||
db.execSQL(MangaTable.addCoverLastModified)
|
|
||||||
}
|
|
||||||
if (oldVersion < 11) {
|
|
||||||
db.execSQL(MangaTable.addDateAdded)
|
|
||||||
db.execSQL(MangaTable.backfillDateAdded)
|
|
||||||
}
|
|
||||||
if (oldVersion < 12) {
|
|
||||||
db.execSQL(MangaTable.addNextUpdateCol)
|
|
||||||
}
|
|
||||||
if (oldVersion < 13) {
|
|
||||||
db.execSQL(TrackTable.renameTableToTemp)
|
|
||||||
db.execSQL(TrackTable.createTableQuery)
|
|
||||||
db.execSQL(TrackTable.insertFromTempTable)
|
|
||||||
db.execSQL(TrackTable.dropTempTable)
|
|
||||||
}
|
|
||||||
if (oldVersion < 14) {
|
|
||||||
db.execSQL(ChapterTable.fixDateUploadIfNeeded)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||||
|
|
|
@ -4,39 +4,11 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||||
import eu.kanade.tachiyomi.data.database.models.History
|
import eu.kanade.tachiyomi.data.database.models.History
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryUpsertResolver
|
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 eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||||
import java.util.Date
|
|
||||||
|
|
||||||
interface HistoryQueries : DbProvider {
|
interface HistoryQueries : DbProvider {
|
||||||
|
|
||||||
/**
|
|
||||||
* Insert history into database
|
|
||||||
* @param history object containing history information
|
|
||||||
*/
|
|
||||||
fun insertHistory(history: History) = db.put().`object`(history).prepare()
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Returns history of recent manga containing last read chapter
|
|
||||||
* @param date recent date range
|
|
||||||
* @param limit the limit of manga to grab
|
|
||||||
* @param offset offset the db by
|
|
||||||
* @param search what to search in the db history
|
|
||||||
*/
|
|
||||||
fun getRecentManga(date: Date, limit: Int = 25, offset: Int = 0, search: String = "") = db.get()
|
|
||||||
.listOfObjects(MangaChapterHistory::class.java)
|
|
||||||
.withQuery(
|
|
||||||
RawQuery.builder()
|
|
||||||
.query(getRecentMangasQuery(search))
|
|
||||||
.args(date.time, limit, offset)
|
|
||||||
.observesTables(HistoryTable.TABLE)
|
|
||||||
.build(),
|
|
||||||
)
|
|
||||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
|
||||||
.prepare()
|
|
||||||
|
|
||||||
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
||||||
.listOfObjects(History::class.java)
|
.listOfObjects(History::class.java)
|
||||||
.withQuery(
|
.withQuery(
|
||||||
|
@ -79,34 +51,6 @@ interface HistoryQueries : DbProvider {
|
||||||
.withPutResolver(HistoryUpsertResolver())
|
.withPutResolver(HistoryUpsertResolver())
|
||||||
.prepare()
|
.prepare()
|
||||||
|
|
||||||
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()
|
fun dropHistoryTable() = db.delete()
|
||||||
.byQuery(
|
.byQuery(
|
||||||
DeleteQuery.builder()
|
DeleteQuery.builder()
|
||||||
|
|
|
@ -70,7 +70,8 @@ fun getRecentMangasQuery(search: String = "") =
|
||||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
|
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
|
||||||
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
|
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
|
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||||
|
) AS max_last_read
|
||||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
||||||
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
|
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
|
||||||
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||||
|
|
|
@ -11,13 +11,4 @@ object CategoryTable {
|
||||||
const val COL_ORDER = "sort"
|
const val COL_ORDER = "sort"
|
||||||
|
|
||||||
const val COL_FLAGS = "flags"
|
const val COL_FLAGS = "flags"
|
||||||
|
|
||||||
val createTableQuery: String
|
|
||||||
get() =
|
|
||||||
"""CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_NAME TEXT NOT NULL,
|
|
||||||
$COL_ORDER INTEGER NOT NULL,
|
|
||||||
$COL_FLAGS INTEGER NOT NULL
|
|
||||||
)"""
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,42 +27,4 @@ object ChapterTable {
|
||||||
const val COL_CHAPTER_NUMBER = "chapter_number"
|
const val COL_CHAPTER_NUMBER = "chapter_number"
|
||||||
|
|
||||||
const val COL_SOURCE_ORDER = "source_order"
|
const val COL_SOURCE_ORDER = "source_order"
|
||||||
|
|
||||||
val createTableQuery: String
|
|
||||||
get() =
|
|
||||||
"""CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
|
||||||
$COL_URL TEXT NOT NULL,
|
|
||||||
$COL_NAME TEXT NOT NULL,
|
|
||||||
$COL_SCANLATOR TEXT,
|
|
||||||
$COL_READ BOOLEAN NOT NULL,
|
|
||||||
$COL_BOOKMARK BOOLEAN NOT NULL,
|
|
||||||
$COL_LAST_PAGE_READ INT NOT NULL,
|
|
||||||
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
|
|
||||||
$COL_SOURCE_ORDER INTEGER NOT NULL,
|
|
||||||
$COL_DATE_FETCH LONG NOT NULL,
|
|
||||||
$COL_DATE_UPLOAD LONG NOT NULL,
|
|
||||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
|
||||||
ON DELETE CASCADE
|
|
||||||
)"""
|
|
||||||
|
|
||||||
val createMangaIdIndexQuery: String
|
|
||||||
get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)"
|
|
||||||
|
|
||||||
val createUnreadChaptersIndexQuery: String
|
|
||||||
get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " +
|
|
||||||
"WHERE $COL_READ = 0"
|
|
||||||
|
|
||||||
val sourceOrderUpdateQuery: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
|
|
||||||
|
|
||||||
val bookmarkUpdateQuery: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
|
|
||||||
|
|
||||||
val addScanlator: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
|
||||||
|
|
||||||
val fixDateUploadIfNeeded: String
|
|
||||||
get() = "UPDATE $TABLE SET $COL_DATE_UPLOAD = $COL_DATE_FETCH WHERE $COL_DATE_UPLOAD = 0"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -26,24 +26,4 @@ object HistoryTable {
|
||||||
* Time read column name
|
* Time read column name
|
||||||
*/
|
*/
|
||||||
const val COL_TIME_READ = "${TABLE}_time_read"
|
const val COL_TIME_READ = "${TABLE}_time_read"
|
||||||
|
|
||||||
/**
|
|
||||||
* query to create history table
|
|
||||||
*/
|
|
||||||
val createTableQuery: String
|
|
||||||
get() =
|
|
||||||
"""CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
|
|
||||||
$COL_LAST_READ LONG,
|
|
||||||
$COL_TIME_READ LONG,
|
|
||||||
FOREIGN KEY($COL_CHAPTER_ID) REFERENCES ${ChapterTable.TABLE} (${ChapterTable.COL_ID})
|
|
||||||
ON DELETE CASCADE
|
|
||||||
)"""
|
|
||||||
|
|
||||||
/**
|
|
||||||
* query to index history chapter id
|
|
||||||
*/
|
|
||||||
val createChapterIdIndexQuery: String
|
|
||||||
get() = "CREATE INDEX ${TABLE}_${COL_CHAPTER_ID}_index ON $TABLE($COL_CHAPTER_ID)"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -9,16 +9,4 @@ object MangaCategoryTable {
|
||||||
const val COL_MANGA_ID = "manga_id"
|
const val COL_MANGA_ID = "manga_id"
|
||||||
|
|
||||||
const val COL_CATEGORY_ID = "category_id"
|
const val COL_CATEGORY_ID = "category_id"
|
||||||
|
|
||||||
val createTableQuery: String
|
|
||||||
get() =
|
|
||||||
"""CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
|
||||||
$COL_CATEGORY_ID INTEGER NOT NULL,
|
|
||||||
FOREIGN KEY($COL_CATEGORY_ID) REFERENCES ${CategoryTable.TABLE} (${CategoryTable.COL_ID})
|
|
||||||
ON DELETE CASCADE,
|
|
||||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
|
||||||
ON DELETE CASCADE
|
|
||||||
)"""
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -47,53 +47,4 @@ object MangaTable {
|
||||||
const val COMPUTED_COL_UNREAD_COUNT = "unread_count"
|
const val COMPUTED_COL_UNREAD_COUNT = "unread_count"
|
||||||
|
|
||||||
const val COMPUTED_COL_READ_COUNT = "read_count"
|
const val COMPUTED_COL_READ_COUNT = "read_count"
|
||||||
|
|
||||||
val createTableQuery: String
|
|
||||||
get() =
|
|
||||||
"""CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_SOURCE INTEGER NOT NULL,
|
|
||||||
$COL_URL TEXT NOT NULL,
|
|
||||||
$COL_ARTIST TEXT,
|
|
||||||
$COL_AUTHOR TEXT,
|
|
||||||
$COL_DESCRIPTION TEXT,
|
|
||||||
$COL_GENRE TEXT,
|
|
||||||
$COL_TITLE TEXT NOT NULL,
|
|
||||||
$COL_STATUS INTEGER NOT NULL,
|
|
||||||
$COL_THUMBNAIL_URL TEXT,
|
|
||||||
$COL_FAVORITE INTEGER NOT NULL,
|
|
||||||
$COL_LAST_UPDATE LONG,
|
|
||||||
$COL_NEXT_UPDATE LONG,
|
|
||||||
$COL_INITIALIZED BOOLEAN NOT NULL,
|
|
||||||
$COL_VIEWER INTEGER NOT NULL,
|
|
||||||
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
|
|
||||||
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
|
|
||||||
$COL_DATE_ADDED LONG NOT NULL
|
|
||||||
)"""
|
|
||||||
|
|
||||||
val createUrlIndexQuery: String
|
|
||||||
get() = "CREATE INDEX ${TABLE}_${COL_URL}_index ON $TABLE($COL_URL)"
|
|
||||||
|
|
||||||
val createLibraryIndexQuery: String
|
|
||||||
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
|
|
||||||
"WHERE $COL_FAVORITE = 1"
|
|
||||||
|
|
||||||
val addCoverLastModified: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0"
|
|
||||||
|
|
||||||
val addDateAdded: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG NOT NULL DEFAULT 0"
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Used with addDateAdded to populate it with the oldest chapter fetch date.
|
|
||||||
*/
|
|
||||||
val backfillDateAdded: String
|
|
||||||
get() = "UPDATE $TABLE SET $COL_DATE_ADDED = " +
|
|
||||||
"(SELECT MIN(${ChapterTable.COL_DATE_FETCH}) " +
|
|
||||||
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
|
|
||||||
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
|
|
||||||
"GROUP BY $TABLE.$COL_ID)"
|
|
||||||
|
|
||||||
val addNextUpdateCol: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_NEXT_UPDATE LONG DEFAULT 0"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,43 +30,6 @@ object TrackTable {
|
||||||
|
|
||||||
const val COL_FINISH_DATE = "finish_date"
|
const val COL_FINISH_DATE = "finish_date"
|
||||||
|
|
||||||
val createTableQuery: String
|
|
||||||
get() =
|
|
||||||
"""CREATE TABLE $TABLE(
|
|
||||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
|
||||||
$COL_MANGA_ID INTEGER NOT NULL,
|
|
||||||
$COL_SYNC_ID INTEGER NOT NULL,
|
|
||||||
$COL_MEDIA_ID INTEGER NOT NULL,
|
|
||||||
$COL_LIBRARY_ID INTEGER,
|
|
||||||
$COL_TITLE TEXT NOT NULL,
|
|
||||||
$COL_LAST_CHAPTER_READ REAL NOT NULL,
|
|
||||||
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
|
|
||||||
$COL_STATUS INTEGER NOT NULL,
|
|
||||||
$COL_SCORE FLOAT NOT NULL,
|
|
||||||
$COL_TRACKING_URL TEXT NOT NULL,
|
|
||||||
$COL_START_DATE LONG NOT NULL,
|
|
||||||
$COL_FINISH_DATE LONG NOT NULL,
|
|
||||||
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
|
|
||||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
|
||||||
ON DELETE CASCADE
|
|
||||||
)"""
|
|
||||||
|
|
||||||
val addTrackingUrl: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_TRACKING_URL TEXT DEFAULT ''"
|
|
||||||
|
|
||||||
val addLibraryId: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL"
|
|
||||||
|
|
||||||
val addStartDate: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_START_DATE LONG NOT NULL DEFAULT 0"
|
|
||||||
|
|
||||||
val addFinishDate: String
|
|
||||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
|
|
||||||
|
|
||||||
val renameTableToTemp: String
|
|
||||||
get() =
|
|
||||||
"ALTER TABLE $TABLE RENAME TO ${TABLE}_tmp"
|
|
||||||
|
|
||||||
val insertFromTempTable: String
|
val insertFromTempTable: String
|
||||||
get() =
|
get() =
|
||||||
"""
|
"""
|
||||||
|
@ -74,7 +37,4 @@ object TrackTable {
|
||||||
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|
||||||
|FROM ${TABLE}_tmp
|
|FROM ${TABLE}_tmp
|
||||||
""".trimMargin()
|
""".trimMargin()
|
||||||
|
|
||||||
val dropTempTable: String
|
|
||||||
get() = "DROP TABLE ${TABLE}_tmp"
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -166,7 +166,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||||
* @param chapterId id of chapter
|
* @param chapterId id of chapter
|
||||||
*/
|
*/
|
||||||
private fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
private fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
||||||
val db = DatabaseHelper(context)
|
val db = Injekt.get<DatabaseHelper>()
|
||||||
val manga = db.getManga(mangaId).executeAsBlocking()
|
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||||
val chapter = db.getChapter(chapterId).executeAsBlocking()
|
val chapter = db.getChapter(chapterId).executeAsBlocking()
|
||||||
if (manga != null && chapter != null) {
|
if (manga != null && chapter != null) {
|
||||||
|
|
|
@ -35,6 +35,7 @@ import com.google.android.material.snackbar.Snackbar
|
||||||
import dev.chrisbanes.insetter.applyInsetter
|
import dev.chrisbanes.insetter.applyInsetter
|
||||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||||
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
|
@ -118,6 +119,8 @@ class MangaController :
|
||||||
DownloadCustomChaptersDialog.Listener,
|
DownloadCustomChaptersDialog.Listener,
|
||||||
DeleteChaptersDialog.Listener {
|
DeleteChaptersDialog.Listener {
|
||||||
|
|
||||||
|
constructor(history: HistoryWithRelations) : this(history.mangaId)
|
||||||
|
|
||||||
constructor(manga: Manga?, fromSource: Boolean = false) : super(
|
constructor(manga: Manga?, fromSource: Boolean = false) : super(
|
||||||
bundleOf(
|
bundleOf(
|
||||||
MANGA_EXTRA to (manga?.id ?: 0),
|
MANGA_EXTRA to (manga?.id ?: 0),
|
||||||
|
|
|
@ -6,9 +6,9 @@ import android.view.MenuInflater
|
||||||
import android.view.MenuItem
|
import android.view.MenuItem
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import androidx.appcompat.widget.SearchView
|
import androidx.appcompat.widget.SearchView
|
||||||
|
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.R
|
||||||
import eu.kanade.tachiyomi.data.database.models.Chapter
|
|
||||||
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
|
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||||
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
import eu.kanade.tachiyomi.ui.base.controller.RootController
|
||||||
|
@ -44,16 +44,16 @@ class HistoryController :
|
||||||
HistoryScreen(
|
HistoryScreen(
|
||||||
composeView = binding.root,
|
composeView = binding.root,
|
||||||
presenter = presenter,
|
presenter = presenter,
|
||||||
onClickItem = { (manga, _, _) ->
|
onClickItem = { history ->
|
||||||
router.pushController(MangaController(manga).withFadeTransaction())
|
router.pushController(MangaController(history).withFadeTransaction())
|
||||||
},
|
},
|
||||||
onClickResume = { (manga, chapter, _) ->
|
onClickResume = { history ->
|
||||||
presenter.getNextChapterForManga(manga, chapter)
|
presenter.getNextChapterForManga(history.mangaId, history.chapterId)
|
||||||
},
|
},
|
||||||
onClickDelete = { (manga, _, history), all ->
|
onClickDelete = { history, all ->
|
||||||
if (all) {
|
if (all) {
|
||||||
// Reset last read of chapter to 0L
|
// Reset last read of chapter to 0L
|
||||||
presenter.removeAllFromHistory(manga.id!!)
|
presenter.removeAllFromHistory(history.mangaId)
|
||||||
} else {
|
} else {
|
||||||
// Remove all chapters belonging to manga from library
|
// Remove all chapters belonging to manga from library
|
||||||
presenter.removeFromHistory(history)
|
presenter.removeFromHistory(history)
|
||||||
|
@ -97,7 +97,7 @@ class HistoryController :
|
||||||
fun openChapter(chapter: Chapter?) {
|
fun openChapter(chapter: Chapter?) {
|
||||||
val activity = activity ?: return
|
val activity = activity ?: return
|
||||||
if (chapter != null) {
|
if (chapter != null) {
|
||||||
val intent = ReaderActivity.newIntent(activity, chapter.manga_id, chapter.id)
|
val intent = ReaderActivity.newIntent(activity, chapter.mangaId, chapter.id)
|
||||||
startActivity(intent)
|
startActivity(intent)
|
||||||
} else {
|
} else {
|
||||||
activity.toast(R.string.no_next_chapter)
|
activity.toast(R.string.no_next_chapter)
|
||||||
|
|
|
@ -10,11 +10,8 @@ import eu.kanade.domain.history.interactor.GetHistory
|
||||||
import eu.kanade.domain.history.interactor.GetNextChapterForManga
|
import eu.kanade.domain.history.interactor.GetNextChapterForManga
|
||||||
import eu.kanade.domain.history.interactor.RemoveHistoryById
|
import eu.kanade.domain.history.interactor.RemoveHistoryById
|
||||||
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
|
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
|
||||||
|
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||||
import eu.kanade.tachiyomi.R
|
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.ui.base.presenter.BasePresenter
|
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
|
||||||
|
@ -58,20 +55,13 @@ class HistoryPresenter(
|
||||||
.map { pagingData ->
|
.map { pagingData ->
|
||||||
pagingData
|
pagingData
|
||||||
.map {
|
.map {
|
||||||
UiModel.History(it)
|
UiModel.Item(it)
|
||||||
}
|
}
|
||||||
.insertSeparators { before, after ->
|
.insertSeparators { before, after ->
|
||||||
val beforeDate =
|
val beforeDate = before?.item?.readAt?.time?.toDateKey() ?: Date(0)
|
||||||
before?.item?.history?.last_read?.toDateKey()
|
val afterDate = after?.item?.readAt?.time?.toDateKey() ?: Date(0)
|
||||||
val afterDate =
|
|
||||||
after?.item?.history?.last_read?.toDateKey()
|
|
||||||
when {
|
when {
|
||||||
beforeDate == null && afterDate != null -> UiModel.Header(
|
beforeDate.time != afterDate.time && afterDate.time != 0L -> UiModel.Header(afterDate)
|
||||||
afterDate,
|
|
||||||
)
|
|
||||||
beforeDate != null && afterDate != null -> UiModel.Header(
|
|
||||||
afterDate,
|
|
||||||
)
|
|
||||||
// Return null to avoid adding a separator between two items.
|
// Return null to avoid adding a separator between two items.
|
||||||
else -> null
|
else -> null
|
||||||
}
|
}
|
||||||
|
@ -90,7 +80,7 @@ class HistoryPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun removeFromHistory(history: History) {
|
fun removeFromHistory(history: HistoryWithRelations) {
|
||||||
presenterScope.launchIO {
|
presenterScope.launchIO {
|
||||||
removeHistoryById.await(history)
|
removeHistoryById.await(history)
|
||||||
}
|
}
|
||||||
|
@ -102,9 +92,9 @@ class HistoryPresenter(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fun getNextChapterForManga(manga: Manga, chapter: Chapter) {
|
fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
|
||||||
presenterScope.launchIO {
|
presenterScope.launchIO {
|
||||||
val chapter = getNextChapterForManga.await(manga, chapter)
|
val chapter = getNextChapterForManga.await(mangaId, chapterId)
|
||||||
view?.openChapter(chapter)
|
view?.openChapter(chapter)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -121,7 +111,7 @@ class HistoryPresenter(
|
||||||
}
|
}
|
||||||
|
|
||||||
sealed class UiModel {
|
sealed class UiModel {
|
||||||
data class History(val item: MangaChapterHistory) : UiModel()
|
data class Item(val item: HistoryWithRelations) : UiModel()
|
||||||
data class Header(val date: Date) : UiModel()
|
data class Header(val date: Date) : UiModel()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
package eu.kanade.tachiyomi.ui.setting.database
|
package eu.kanade.tachiyomi.ui.setting.database
|
||||||
|
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import eu.kanade.tachiyomi.Database
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||||
import eu.kanade.tachiyomi.source.SourceManager
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||||
|
@ -13,6 +14,7 @@ import uy.kohesive.injekt.api.get
|
||||||
class ClearDatabasePresenter : BasePresenter<ClearDatabaseController>() {
|
class ClearDatabasePresenter : BasePresenter<ClearDatabaseController>() {
|
||||||
|
|
||||||
private val db = Injekt.get<DatabaseHelper>()
|
private val db = Injekt.get<DatabaseHelper>()
|
||||||
|
private val database = Injekt.get<Database>()
|
||||||
|
|
||||||
private val sourceManager = Injekt.get<SourceManager>()
|
private val sourceManager = Injekt.get<SourceManager>()
|
||||||
|
|
||||||
|
@ -26,7 +28,7 @@ class ClearDatabasePresenter : BasePresenter<ClearDatabaseController>() {
|
||||||
|
|
||||||
fun clearDatabaseForSourceIds(sources: List<Long>) {
|
fun clearDatabaseForSourceIds(sources: List<Long>) {
|
||||||
db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking()
|
db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking()
|
||||||
db.deleteHistoryNoLastRead().executeAsBlocking()
|
database.historyQueries.removeResettedHistory()
|
||||||
}
|
}
|
||||||
|
|
||||||
private fun getDatabaseSourcesObservable(): Observable<List<ClearDatabaseSourceItem>> {
|
private fun getDatabaseSourcesObservable(): Observable<List<ClearDatabaseSourceItem>> {
|
||||||
|
|
6
app/src/main/sqldelight/data/categories.sq
Normal file
6
app/src/main/sqldelight/data/categories.sq
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
CREATE TABLE categories(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
sort INTEGER NOT NULL,
|
||||||
|
flags INTEGER NOT NULL
|
||||||
|
);
|
26
app/src/main/sqldelight/data/chapters.sq
Normal file
26
app/src/main/sqldelight/data/chapters.sq
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
CREATE TABLE chapters(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
manga_id INTEGER NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
scanlator TEXT,
|
||||||
|
read INTEGER AS Boolean NOT NULL,
|
||||||
|
bookmark INTEGER AS Boolean NOT NULL,
|
||||||
|
last_page_read INTEGER NOT NULL,
|
||||||
|
chapter_number REAL AS Float NOT NULL,
|
||||||
|
source_order INTEGER NOT NULL,
|
||||||
|
date_fetch INTEGER AS Long NOT NULL,
|
||||||
|
date_upload INTEGER AS Long NOT NULL,
|
||||||
|
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
getChapterById:
|
||||||
|
SELECT *
|
||||||
|
FROM chapters
|
||||||
|
WHERE _id = :id;
|
||||||
|
|
||||||
|
getChapterByMangaId:
|
||||||
|
SELECT *
|
||||||
|
FROM chapters
|
||||||
|
WHERE manga_id = :mangaId;
|
35
app/src/main/sqldelight/data/history.sq
Normal file
35
app/src/main/sqldelight/data/history.sq
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
CREATE TABLE history(
|
||||||
|
history_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
history_chapter_id INTEGER NOT NULL UNIQUE,
|
||||||
|
history_last_read INTEGER AS Date,
|
||||||
|
history_time_read INTEGER AS Date,
|
||||||
|
FOREIGN KEY(history_chapter_id) REFERENCES chapters (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
resetHistoryById:
|
||||||
|
UPDATE history
|
||||||
|
SET history_last_read = 0
|
||||||
|
WHERE history_id = :historyId;
|
||||||
|
|
||||||
|
resetHistoryByMangaId:
|
||||||
|
UPDATE history
|
||||||
|
SET history_last_read = 0
|
||||||
|
WHERE history_id IN (
|
||||||
|
SELECT H.history_id
|
||||||
|
FROM mangas M
|
||||||
|
INNER JOIN chapters C
|
||||||
|
ON M._id = C.manga_id
|
||||||
|
INNER JOIN history H
|
||||||
|
ON C._id = H.history_chapter_id
|
||||||
|
WHERE M._id = :mangaId
|
||||||
|
);
|
||||||
|
|
||||||
|
removeAllHistory:
|
||||||
|
DELETE FROM history;
|
||||||
|
|
||||||
|
removeResettedHistory:
|
||||||
|
DELETE FROM history
|
||||||
|
WHERE history_last_read = 0;
|
18
app/src/main/sqldelight/data/manga_sync.sq
Normal file
18
app/src/main/sqldelight/data/manga_sync.sq
Normal file
|
@ -0,0 +1,18 @@
|
||||||
|
CREATE TABLE manga_sync(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
manga_id INTEGER NOT NULL,
|
||||||
|
sync_id INTEGER NOT NULL,
|
||||||
|
remote_id INTEGER NOT NULL,
|
||||||
|
library_id INTEGER,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
last_chapter_read REAL NOT NULL,
|
||||||
|
total_chapters INTEGER NOT NULL,
|
||||||
|
status INTEGER NOT NULL,
|
||||||
|
score REAL AS Float NOT NULL,
|
||||||
|
remote_url TEXT NOT NULL,
|
||||||
|
start_date INTEGER AS Long NOT NULL,
|
||||||
|
finish_date INTEGER AS Long NOT NULL,
|
||||||
|
UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE,
|
||||||
|
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
28
app/src/main/sqldelight/data/mangas.sq
Normal file
28
app/src/main/sqldelight/data/mangas.sq
Normal file
|
@ -0,0 +1,28 @@
|
||||||
|
import java.lang.String;
|
||||||
|
import kotlin.collections.List;
|
||||||
|
|
||||||
|
CREATE TABLE mangas(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
source INTEGER NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
artist TEXT,
|
||||||
|
author TEXT,
|
||||||
|
description TEXT,
|
||||||
|
genre TEXT AS List<String>,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status INTEGER NOT NULL,
|
||||||
|
thumbnail_url TEXT,
|
||||||
|
favorite INTEGER AS Boolean NOT NULL,
|
||||||
|
last_update INTEGER AS Long,
|
||||||
|
next_update INTEGER AS Long,
|
||||||
|
initialized INTEGER AS Boolean NOT NULL,
|
||||||
|
viewer INTEGER NOT NULL,
|
||||||
|
chapter_flags INTEGER NOT NULL,
|
||||||
|
cover_last_modified INTEGER AS Long NOT NULL,
|
||||||
|
date_added INTEGER AS Long NOT NULL
|
||||||
|
);
|
||||||
|
|
||||||
|
getMangaById:
|
||||||
|
SELECT *
|
||||||
|
FROM mangas
|
||||||
|
WHERE _id = :id;
|
9
app/src/main/sqldelight/data/mangas_categories.sq
Normal file
9
app/src/main/sqldelight/data/mangas_categories.sq
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
CREATE TABLE mangas_categories(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
manga_id INTEGER NOT NULL,
|
||||||
|
category_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(category_id) REFERENCES categories (_id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
6
app/src/main/sqldelight/migrations/1.sqm
Normal file
6
app/src/main/sqldelight/migrations/1.sqm
Normal file
|
@ -0,0 +1,6 @@
|
||||||
|
ALTER TABLE chapters
|
||||||
|
ADD COLUMN source_order INTEGER DEFAULT 0;
|
||||||
|
|
||||||
|
UPDATE mangas
|
||||||
|
SET thumbnail_url = replace(thumbnail_url, '93.174.95.110', 'kissmanga.com')
|
||||||
|
WHERE source = 4;
|
11
app/src/main/sqldelight/migrations/10.sqm
Normal file
11
app/src/main/sqldelight/migrations/10.sqm
Normal file
|
@ -0,0 +1,11 @@
|
||||||
|
ALTER TABLE mangas
|
||||||
|
ADD COLUMN date_added INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
UPDATE mangas
|
||||||
|
SET date_added = (
|
||||||
|
SELECT MIN(date_fetch)
|
||||||
|
FROM mangas M
|
||||||
|
INNER JOIN chapters C
|
||||||
|
ON M._id = C.manga_id
|
||||||
|
GROUP BY M._id
|
||||||
|
);
|
2
app/src/main/sqldelight/migrations/11.sqm
Normal file
2
app/src/main/sqldelight/migrations/11.sqm
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE mangas
|
||||||
|
ADD COLUMN next_update INTEGER DEFAULT 0;
|
9
app/src/main/sqldelight/migrations/12.sqm
Normal file
9
app/src/main/sqldelight/migrations/12.sqm
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
ALTER TABLE manga_sync
|
||||||
|
RENAME TO manga_sync_tmp;
|
||||||
|
|
||||||
|
INSERT INTO manga_sync(_id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date)
|
||||||
|
SELECT _id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date
|
||||||
|
FROM manga_sync_tmp;
|
||||||
|
|
||||||
|
|
||||||
|
DROP TABLE manga_sync_tmp;
|
3
app/src/main/sqldelight/migrations/13.sqm
Normal file
3
app/src/main/sqldelight/migrations/13.sqm
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
UPDATE chapters
|
||||||
|
SET date_upload = date_fetch
|
||||||
|
WHERE date_upload = 0;
|
149
app/src/main/sqldelight/migrations/14.sqm
Normal file
149
app/src/main/sqldelight/migrations/14.sqm
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
DROP INDEX IF EXISTS chapters_manga_id_index;
|
||||||
|
DROP INDEX IF EXISTS chapters_unread_by_manga_index;
|
||||||
|
DROP INDEX IF EXISTS history_history_chapter_id_index;
|
||||||
|
DROP INDEX IF EXISTS library_favorite_index;
|
||||||
|
DROP INDEX IF EXISTS mangas_url_index;
|
||||||
|
|
||||||
|
ALTER TABLE mangas RENAME TO manga_temp;
|
||||||
|
CREATE TABLE mangas(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
source INTEGER NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
artist TEXT,
|
||||||
|
author TEXT,
|
||||||
|
description TEXT,
|
||||||
|
genre TEXT,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
status INTEGER NOT NULL,
|
||||||
|
thumbnail_url TEXT,
|
||||||
|
favorite INTEGER NOT NULL,
|
||||||
|
last_update INTEGER AS Long,
|
||||||
|
next_update INTEGER AS Long,
|
||||||
|
initialized INTEGER AS Boolean NOT NULL,
|
||||||
|
viewer INTEGER NOT NULL,
|
||||||
|
chapter_flags INTEGER NOT NULL,
|
||||||
|
cover_last_modified INTEGER AS Long NOT NULL,
|
||||||
|
date_added INTEGER AS Long NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO mangas
|
||||||
|
SELECT _id,source,url,artist,author,description,genre,title,status,thumbnail_url,favorite,last_update,next_update,initialized,viewer,chapter_flags,cover_last_modified,date_added
|
||||||
|
FROM manga_temp;
|
||||||
|
|
||||||
|
ALTER TABLE categories RENAME TO categories_temp;
|
||||||
|
CREATE TABLE categories(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
sort INTEGER NOT NULL,
|
||||||
|
flags INTEGER NOT NULL
|
||||||
|
);
|
||||||
|
INSERT INTO categories
|
||||||
|
SELECT _id,name,sort,flags
|
||||||
|
FROM categories_temp;
|
||||||
|
|
||||||
|
ALTER TABLE chapters RENAME TO chapters_temp;
|
||||||
|
CREATE TABLE chapters(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
manga_id INTEGER NOT NULL,
|
||||||
|
url TEXT NOT NULL,
|
||||||
|
name TEXT NOT NULL,
|
||||||
|
scanlator TEXT,
|
||||||
|
read INTEGER AS Boolean NOT NULL,
|
||||||
|
bookmark INTEGER AS Boolean NOT NULL,
|
||||||
|
last_page_read INTEGER NOT NULL,
|
||||||
|
chapter_number REAL AS Float NOT NULL,
|
||||||
|
source_order INTEGER NOT NULL,
|
||||||
|
date_fetch INTEGER AS Long NOT NULL,
|
||||||
|
date_upload INTEGER AS Long NOT NULL,
|
||||||
|
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO chapters
|
||||||
|
SELECT _id,manga_id,url,name,scanlator,read,bookmark,last_page_read,chapter_number,source_order,date_fetch,date_upload
|
||||||
|
FROM chapters_temp;
|
||||||
|
|
||||||
|
ALTER TABLE history RENAME TO history_temp;
|
||||||
|
CREATE TABLE history(
|
||||||
|
history_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
history_chapter_id INTEGER NOT NULL UNIQUE,
|
||||||
|
history_last_read INTEGER AS Long,
|
||||||
|
history_time_read INTEGER AS Long,
|
||||||
|
FOREIGN KEY(history_chapter_id) REFERENCES chapters (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO history
|
||||||
|
SELECT history_id, history_chapter_id, history_last_read, history_time_read
|
||||||
|
FROM history_temp;
|
||||||
|
|
||||||
|
ALTER TABLE mangas_categories RENAME TO mangas_categories_temp;
|
||||||
|
CREATE TABLE mangas_categories(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
manga_id INTEGER NOT NULL,
|
||||||
|
category_id INTEGER NOT NULL,
|
||||||
|
FOREIGN KEY(category_id) REFERENCES categories (_id)
|
||||||
|
ON DELETE CASCADE,
|
||||||
|
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO mangas_categories
|
||||||
|
SELECT _id, manga_id, category_id
|
||||||
|
FROM mangas_categories_temp;
|
||||||
|
|
||||||
|
ALTER TABLE manga_sync RENAME TO manga_sync_temp;
|
||||||
|
CREATE TABLE manga_sync(
|
||||||
|
_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
manga_id INTEGER NOT NULL,
|
||||||
|
sync_id INTEGER NOT NULL,
|
||||||
|
remote_id INTEGER NOT NULL,
|
||||||
|
library_id INTEGER,
|
||||||
|
title TEXT NOT NULL,
|
||||||
|
last_chapter_read REAL NOT NULL,
|
||||||
|
total_chapters INTEGER NOT NULL,
|
||||||
|
status INTEGER NOT NULL,
|
||||||
|
score REAL AS Float NOT NULL,
|
||||||
|
remote_url TEXT NOT NULL,
|
||||||
|
start_date INTEGER AS Long NOT NULL,
|
||||||
|
finish_date INTEGER AS Long NOT NULL,
|
||||||
|
UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE,
|
||||||
|
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
INSERT INTO manga_sync
|
||||||
|
SELECT _id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date
|
||||||
|
FROM manga_sync_temp;
|
||||||
|
|
||||||
|
CREATE INDEX chapters_manga_id_index ON chapters(manga_id);
|
||||||
|
CREATE INDEX chapters_unread_by_manga_index ON chapters(manga_id, read) WHERE read = 0;
|
||||||
|
CREATE INDEX history_history_chapter_id_index ON history(history_chapter_id);
|
||||||
|
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
|
||||||
|
CREATE INDEX mangas_url_index ON mangas(url);
|
||||||
|
|
||||||
|
CREATE VIEW IF NOT EXISTS historyView AS
|
||||||
|
SELECT
|
||||||
|
history.history_id AS id,
|
||||||
|
mangas._id AS mangaId,
|
||||||
|
chapters._id AS chapterId,
|
||||||
|
mangas.title,
|
||||||
|
mangas.thumbnail_url AS thumnailUrl,
|
||||||
|
chapters.chapter_number AS chapterNumber,
|
||||||
|
history.history_last_read AS readAt,
|
||||||
|
max_last_read.history_last_read AS maxReadAt,
|
||||||
|
max_last_read.history_chapter_id AS maxReadAtChapterId
|
||||||
|
FROM mangas
|
||||||
|
JOIN chapters
|
||||||
|
ON mangas._id = chapters.manga_id
|
||||||
|
JOIN history
|
||||||
|
ON chapters._id = history.history_chapter_id
|
||||||
|
JOIN (
|
||||||
|
SELECT chapters.manga_id,chapters._id AS history_chapter_id, MAX(history.history_last_read) AS history_last_read
|
||||||
|
FROM chapters JOIN history
|
||||||
|
ON chapters._id = history.history_chapter_id
|
||||||
|
GROUP BY chapters.manga_id
|
||||||
|
) AS max_last_read
|
||||||
|
ON chapters.manga_id = max_last_read.manga_id;
|
||||||
|
|
||||||
|
DROP TABLE IF EXISTS manga_sync_temp;
|
||||||
|
DROP TABLE IF EXISTS mangas_categories_temp;
|
||||||
|
DROP TABLE IF EXISTS history_temp;
|
||||||
|
DROP TABLE IF EXISTS chapters_temp;
|
||||||
|
DROP TABLE IF EXISTS categories_temp;
|
||||||
|
DROP TABLE IF EXISTS manga_temp;
|
10
app/src/main/sqldelight/migrations/2.sqm
Normal file
10
app/src/main/sqldelight/migrations/2.sqm
Normal file
|
@ -0,0 +1,10 @@
|
||||||
|
CREATE TABLE history(
|
||||||
|
history_id INTEGER NOT NULL PRIMARY KEY,
|
||||||
|
history_chapter_id INTEGER NOT NULL UNIQUE,
|
||||||
|
history_last_read INTEGER,
|
||||||
|
history_time_read INTEGER,
|
||||||
|
FOREIGN KEY(history_chapter_id) REFERENCES chapters (_id)
|
||||||
|
ON DELETE CASCADE
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE INDEX history_history_chapter_id_index ON history(history_chapter_id);
|
2
app/src/main/sqldelight/migrations/3.sqm
Normal file
2
app/src/main/sqldelight/migrations/3.sqm
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE chapters
|
||||||
|
ADD COLUMN bookmark INTEGER DEFAULT 0;
|
2
app/src/main/sqldelight/migrations/4.sqm
Normal file
2
app/src/main/sqldelight/migrations/4.sqm
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE chapters
|
||||||
|
ADD COLUMN scanlator TEXT DEFAULT NULL;
|
2
app/src/main/sqldelight/migrations/5.sqm
Normal file
2
app/src/main/sqldelight/migrations/5.sqm
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE manga_sync
|
||||||
|
ADD COLUMN remote_url TEXT DEFAULT '';
|
2
app/src/main/sqldelight/migrations/6.sqm
Normal file
2
app/src/main/sqldelight/migrations/6.sqm
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE manga_sync
|
||||||
|
ADD COLUMN library_id INTEGER;
|
9
app/src/main/sqldelight/migrations/7.sqm
Normal file
9
app/src/main/sqldelight/migrations/7.sqm
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
DROP INDEX IF EXISTS mangas_favorite_index;
|
||||||
|
|
||||||
|
CREATE INDEX library_favorite_index
|
||||||
|
ON mangas(favorite)
|
||||||
|
WHERE favorite = 1;
|
||||||
|
|
||||||
|
CREATE INDEX chapters_unread_by_manga_index
|
||||||
|
ON chapters(manga_id, read)
|
||||||
|
WHERE read = 0;
|
5
app/src/main/sqldelight/migrations/8.sqm
Normal file
5
app/src/main/sqldelight/migrations/8.sqm
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
ALTER TABLE manga_sync
|
||||||
|
ADD COLUMN start_date INTEGER NOT NULL DEFAULT 0;
|
||||||
|
|
||||||
|
ALTER TABLE manga_sync
|
||||||
|
ADD COLUMN finish_date INTEGER NOT NULL DEFAULT 0;
|
2
app/src/main/sqldelight/migrations/9.sqm
Normal file
2
app/src/main/sqldelight/migrations/9.sqm
Normal file
|
@ -0,0 +1,2 @@
|
||||||
|
ALTER TABLE mangas
|
||||||
|
ADD COLUMN cover_last_modified INTEGER NOT NULL DEFAULT 0;
|
46
app/src/main/sqldelight/view/historyView.sq
Normal file
46
app/src/main/sqldelight/view/historyView.sq
Normal file
|
@ -0,0 +1,46 @@
|
||||||
|
CREATE VIEW historyView AS
|
||||||
|
SELECT
|
||||||
|
history.history_id AS id,
|
||||||
|
mangas._id AS mangaId,
|
||||||
|
chapters._id AS chapterId,
|
||||||
|
mangas.title,
|
||||||
|
mangas.thumbnail_url AS thumnailUrl,
|
||||||
|
chapters.chapter_number AS chapterNumber,
|
||||||
|
history.history_last_read AS readAt,
|
||||||
|
max_last_read.history_last_read AS maxReadAt,
|
||||||
|
max_last_read.history_chapter_id AS maxReadAtChapterId
|
||||||
|
FROM mangas
|
||||||
|
JOIN chapters
|
||||||
|
ON mangas._id = chapters.manga_id
|
||||||
|
JOIN history
|
||||||
|
ON chapters._id = history.history_chapter_id
|
||||||
|
JOIN (
|
||||||
|
SELECT chapters.manga_id,chapters._id AS history_chapter_id, MAX(history.history_last_read) AS history_last_read
|
||||||
|
FROM chapters JOIN history
|
||||||
|
ON chapters._id = history.history_chapter_id
|
||||||
|
GROUP BY chapters.manga_id
|
||||||
|
) AS max_last_read
|
||||||
|
ON chapters.manga_id = max_last_read.manga_id;
|
||||||
|
|
||||||
|
countHistory:
|
||||||
|
SELECT count(*)
|
||||||
|
FROM historyView
|
||||||
|
WHERE historyView.readAt > 0
|
||||||
|
AND maxReadAtChapterId = historyView.chapterId
|
||||||
|
AND lower(historyView.title) LIKE ('%' || :query || '%');
|
||||||
|
|
||||||
|
history:
|
||||||
|
SELECT
|
||||||
|
id,
|
||||||
|
mangaId,
|
||||||
|
chapterId,
|
||||||
|
title,
|
||||||
|
thumnailUrl,
|
||||||
|
chapterNumber,
|
||||||
|
readAt
|
||||||
|
FROM historyView
|
||||||
|
WHERE historyView.readAt > 0
|
||||||
|
AND maxReadAtChapterId = historyView.chapterId
|
||||||
|
AND lower(historyView.title) LIKE ('%' || :query || '%')
|
||||||
|
ORDER BY readAt DESC
|
||||||
|
LIMIT :limit OFFSET :offset;
|
|
@ -4,6 +4,7 @@ buildscript {
|
||||||
classpath(libs.google.services.gradle)
|
classpath(libs.google.services.gradle)
|
||||||
classpath(libs.aboutlibraries.gradle)
|
classpath(libs.aboutlibraries.gradle)
|
||||||
classpath(kotlinx.serialization.gradle)
|
classpath(kotlinx.serialization.gradle)
|
||||||
|
classpath("com.squareup.sqldelight:gradle-plugin:1.5.3")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -7,6 +7,7 @@ conductor_version = "3.1.2"
|
||||||
flowbinding_version = "1.2.0"
|
flowbinding_version = "1.2.0"
|
||||||
shizuku_version = "12.1.0"
|
shizuku_version = "12.1.0"
|
||||||
robolectric_version = "3.1.4"
|
robolectric_version = "3.1.4"
|
||||||
|
sqldelight = "1.5.3"
|
||||||
|
|
||||||
[libraries]
|
[libraries]
|
||||||
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
|
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
|
||||||
|
@ -97,6 +98,10 @@ robolectric-playservices = { module = "org.robolectric:shadows-play-services", v
|
||||||
|
|
||||||
leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.7"
|
leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.7"
|
||||||
|
|
||||||
|
sqldelight-android-driver = { module = "com.squareup.sqldelight:android-driver", version.ref ="sqldelight" }
|
||||||
|
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions-jvm", version.ref ="sqldelight" }
|
||||||
|
sqldelight-android-paging = { module = "com.squareup.sqldelight:android-paging3-extensions", version.ref ="sqldelight" }
|
||||||
|
|
||||||
[bundles]
|
[bundles]
|
||||||
reactivex = ["rxandroid","rxjava","rxrelay"]
|
reactivex = ["rxandroid","rxjava","rxrelay"]
|
||||||
okhttp = ["okhttp-core","okhttp-logging","okhttp-dnsoverhttps"]
|
okhttp = ["okhttp-core","okhttp-logging","okhttp-dnsoverhttps"]
|
||||||
|
|
Loading…
Reference in a new issue