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:
Andreas 2022-04-21 21:45:56 +02:00 committed by GitHub
parent 6c1565a7d4
commit b1f46ed830
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
62 changed files with 1069 additions and 570 deletions

View file

@ -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)

View 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) }
}
}

View 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)
}

View 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>
}

View 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()
}
}
}

View 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,
)
}

View 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
)
}

View file

@ -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
}
}
}

View file

@ -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
}
}

View file

@ -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
}
}
}
}

View 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,
)
}

View file

@ -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

View 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?
)

View file

@ -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

View file

@ -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)
} }
} }

View file

@ -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)
} }
} }

View file

@ -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)
} }
} }

View file

@ -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?
)

View file

@ -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?
)

View file

@ -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
} }

View 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
}
}

View file

@ -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)

View file

@ -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)
) )
} }

View file

@ -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>()

View file

@ -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()

View file

@ -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) {

View file

@ -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()

View file

@ -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}

View file

@ -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
)"""
} }

View file

@ -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"
} }

View file

@ -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)"
} }

View file

@ -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
)"""
} }

View file

@ -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"
} }

View file

@ -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"
} }

View file

@ -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) {

View file

@ -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),

View file

@ -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)

View file

@ -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()
} }

View file

@ -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>> {

View 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
);

View 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;

View 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;

View 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
);

View 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;

View 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
);

View 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;

View 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
);

View file

@ -0,0 +1,2 @@
ALTER TABLE mangas
ADD COLUMN next_update INTEGER DEFAULT 0;

View 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;

View file

@ -0,0 +1,3 @@
UPDATE chapters
SET date_upload = date_fetch
WHERE date_upload = 0;

View 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;

View 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);

View file

@ -0,0 +1,2 @@
ALTER TABLE chapters
ADD COLUMN bookmark INTEGER DEFAULT 0;

View file

@ -0,0 +1,2 @@
ALTER TABLE chapters
ADD COLUMN scanlator TEXT DEFAULT NULL;

View file

@ -0,0 +1,2 @@
ALTER TABLE manga_sync
ADD COLUMN remote_url TEXT DEFAULT '';

View file

@ -0,0 +1,2 @@
ALTER TABLE manga_sync
ADD COLUMN library_id INTEGER;

View 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;

View 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;

View file

@ -0,0 +1,2 @@
ALTER TABLE mangas
ADD COLUMN cover_last_modified INTEGER NOT NULL DEFAULT 0;

View 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;

View file

@ -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")
} }
} }

View file

@ -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"]