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
62 changed files with 1069 additions and 570 deletions

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
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.GetHistory
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.PagingConfig
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.tachiyomi.data.database.models.MangaChapterHistory
import kotlinx.coroutines.flow.Flow
class GetHistory(
private val repository: HistoryRepository
) {
fun subscribe(query: String): Flow<PagingData<MangaChapterHistory>> {
fun subscribe(query: String): Flow<PagingData<HistoryWithRelations>> {
return Pager(
PagingConfig(pageSize = HistoryPagingSource.PAGE_SIZE)
PagingConfig(pageSize = 25)
) {
repository.getHistory(query)
}.flow

View File

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

View File

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

View File

@@ -6,7 +6,7 @@ class RemoveHistoryByMangaId(
private val repository: HistoryRepository
) {
suspend fun await(mangaId: Long): Boolean {
return repository.resetHistoryByMangaId(mangaId)
suspend fun await(mangaId: Long) {
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
import eu.kanade.data.history.local.HistoryPagingSource
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import androidx.paging.PagingSource
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.domain.history.model.HistoryWithRelations
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): Boolean
suspend fun resetHistoryByMangaId(mangaId: Long)
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.unit.dp
import coil.compose.AsyncImage
import eu.kanade.tachiyomi.data.database.models.Manga
enum class MangaCoverAspect(val ratio: Float) {
SQUARE(1f / 1f),
@@ -19,13 +18,13 @@ enum class MangaCoverAspect(val ratio: Float) {
@Composable
fun MangaCover(
modifier: Modifier = Modifier,
manga: Manga,
data: String?,
aspect: MangaCoverAspect,
contentDescription: String = "",
shape: Shape = RoundedCornerShape(4.dp)
) {
AsyncImage(
model = manga,
model = data,
contentDescription = contentDescription,
modifier = modifier
.aspectRatio(aspect.ratio)

View File

@@ -1,6 +1,7 @@
package eu.kanade.presentation.history
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
@@ -42,13 +43,13 @@ import androidx.core.text.buildSpannedString
import androidx.paging.compose.LazyPagingItems
import androidx.paging.compose.collectAsLazyPagingItems
import androidx.paging.compose.items
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.presentation.components.EmptyScreen
import eu.kanade.presentation.components.MangaCover
import eu.kanade.presentation.components.MangaCoverAspect
import eu.kanade.presentation.theme.TachiyomiTheme
import eu.kanade.presentation.util.horizontalPadding
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter
import eu.kanade.tachiyomi.ui.recent.history.UiModel
@@ -71,9 +72,9 @@ val chapterFormatter = DecimalFormat(
fun HistoryScreen(
composeView: ComposeView,
presenter: HistoryPresenter,
onClickItem: (MangaChapterHistory) -> Unit,
onClickResume: (MangaChapterHistory) -> Unit,
onClickDelete: (MangaChapterHistory, Boolean) -> Unit,
onClickItem: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations, Boolean) -> Unit,
) {
val nestedSrollInterop = rememberNestedScrollInteropConnection(composeView)
TachiyomiTheme {
@@ -104,16 +105,16 @@ fun HistoryScreen(
@Composable
fun HistoryContent(
history: LazyPagingItems<UiModel>,
onClickItem: (MangaChapterHistory) -> Unit,
onClickResume: (MangaChapterHistory) -> Unit,
onClickDelete: (MangaChapterHistory, Boolean) -> Unit,
onClickItem: (HistoryWithRelations) -> Unit,
onClickResume: (HistoryWithRelations) -> Unit,
onClickDelete: (HistoryWithRelations, Boolean) -> Unit,
preferences: PreferencesHelper = Injekt.get(),
nestedScroll: NestedScrollConnection
) {
val relativeTime: Int = remember { preferences.relativeTime().get() }
val dateFormat: DateFormat = remember { preferences.dateFormat() }
val (removeState, setRemoveState) = remember { mutableStateOf<MangaChapterHistory?>(null) }
val (removeState, setRemoveState) = remember { mutableStateOf<HistoryWithRelations?>(null) }
val scrollState = rememberLazyListState()
LazyColumn(
@@ -132,7 +133,7 @@ fun HistoryContent(
dateFormat = dateFormat
)
}
is UiModel.History -> {
is UiModel.Item -> {
val value = item.item
HistoryItem(
modifier = Modifier.animateItemPlacement(),
@@ -189,7 +190,7 @@ fun HistoryHeader(
@Composable
fun HistoryItem(
modifier: Modifier = Modifier,
history: MangaChapterHistory,
history: HistoryWithRelations,
onClickItem: () -> Unit,
onClickResume: () -> Unit,
onClickDelete: () -> Unit,
@@ -203,7 +204,7 @@ fun HistoryItem(
) {
MangaCover(
modifier = Modifier.fillMaxHeight(),
manga = history.manga,
data = history.thumbnailUrl,
aspect = MangaCoverAspect.COVER
)
Column(
@@ -215,7 +216,7 @@ fun HistoryItem(
color = MaterialTheme.colorScheme.onSurface,
)
Text(
text = history.manga.title,
text = history.title,
maxLines = 2,
overflow = TextOverflow.Ellipsis,
style = textStyle.copy(fontWeight = FontWeight.SemiBold)
@@ -223,15 +224,15 @@ fun HistoryItem(
Row {
Text(
text = buildSpannedString {
if (history.chapter.chapter_number > -1) {
if (history.chapterNumber > -1) {
append(
stringResource(
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(),
modifier = Modifier.padding(top = 2.dp),
style = textStyle
@@ -270,14 +271,22 @@ fun RemoveHistoryDialog(
Column {
Text(text = stringResource(id = R.string.dialog_with_checkbox_remove_description))
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
) {
Checkbox(
checked = removeEverything,
onCheckedChange = removeEverythingState,
onCheckedChange = null,
)
Text(
modifier = Modifier.padding(start = 4.dp),
text = stringResource(id = R.string.dialog_with_checkbox_reset)
)
}

View File

@@ -2,9 +2,18 @@ package eu.kanade.tachiyomi
import android.app.Application
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.CoverCache
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.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.saver.ImageSaver
@@ -25,11 +34,37 @@ class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
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 { PreferencesHelper(app) }
addSingletonFactory { DatabaseHelper(app) }
addSingletonFactory { DatabaseHelper(app, get()) }
addSingletonFactory { ChapterCache(app) }
@@ -57,6 +92,8 @@ class AppModule(val app: Application) : InjektModule {
get<SourceManager>()
get<Database>()
get<DatabaseHelper>()
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.
*/
open class DatabaseHelper(context: Context) :
open class DatabaseHelper(
context: Context,
callback: DbOpenCallback
) :
MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME)
.callback(DbOpenCallback())
.callback(callback)
.build()
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.SupportSQLiteOpenHelper
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
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
import com.squareup.sqldelight.android.AndroidSqliteDriver
import eu.kanade.tachiyomi.Database
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(Database.Schema.version) {
companion object {
/**
* Name of the database file.
*/
const val DATABASE_NAME = "tachiyomi.db"
/**
* Version of the database.
*/
const val DATABASE_VERSION = 14
}
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
execSQL(MangaTable.createTableQuery)
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 onCreate(db: SupportSQLiteDatabase) {
Database.Schema.create(AndroidSqliteDriver(database = db, cacheSize = 1))
}
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 2) {
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
// Fix kissmanga covers after supporting cloudflare
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)
}
Database.Schema.migrate(
driver = AndroidSqliteDriver(database = db, cacheSize = 1),
oldVersion = oldVersion,
newVersion = newVersion
)
}
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 eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.database.resolvers.HistoryUpsertResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import java.util.Date
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()
.listOfObjects(History::class.java)
.withQuery(
@@ -79,34 +51,6 @@ interface HistoryQueries : DbProvider {
.withPutResolver(HistoryUpsertResolver())
.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()
.byQuery(
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}
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
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}
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
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_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_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
*/
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_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_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"
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
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
|FROM ${TABLE}_tmp
""".trimMargin()
val dropTempTable: String
get() = "DROP TABLE ${TABLE}_tmp"
}

View File

@@ -166,7 +166,7 @@ class NotificationReceiver : BroadcastReceiver() {
* @param chapterId id of chapter
*/
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 chapter = db.getChapter(chapterId).executeAsBlocking()
if (manga != null && chapter != null) {

View File

@@ -35,6 +35,7 @@ import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -118,6 +119,8 @@ class MangaController :
DownloadCustomChaptersDialog.Listener,
DeleteChaptersDialog.Listener {
constructor(history: HistoryWithRelations) : this(history.mangaId)
constructor(manga: Manga?, fromSource: Boolean = false) : super(
bundleOf(
MANGA_EXTRA to (manga?.id ?: 0),

View File

@@ -6,9 +6,9 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.presentation.history.HistoryScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.databinding.ComposeControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController
@@ -44,16 +44,16 @@ class HistoryController :
HistoryScreen(
composeView = binding.root,
presenter = presenter,
onClickItem = { (manga, _, _) ->
router.pushController(MangaController(manga).withFadeTransaction())
onClickItem = { history ->
router.pushController(MangaController(history).withFadeTransaction())
},
onClickResume = { (manga, chapter, _) ->
presenter.getNextChapterForManga(manga, chapter)
onClickResume = { history ->
presenter.getNextChapterForManga(history.mangaId, history.chapterId)
},
onClickDelete = { (manga, _, history), all ->
onClickDelete = { history, all ->
if (all) {
// Reset last read of chapter to 0L
presenter.removeAllFromHistory(manga.id!!)
presenter.removeAllFromHistory(history.mangaId)
} else {
// Remove all chapters belonging to manga from library
presenter.removeFromHistory(history)
@@ -97,7 +97,7 @@ class HistoryController :
fun openChapter(chapter: Chapter?) {
val activity = activity ?: return
if (chapter != null) {
val intent = ReaderActivity.newIntent(activity, chapter.manga_id, chapter.id)
val intent = ReaderActivity.newIntent(activity, chapter.mangaId, chapter.id)
startActivity(intent)
} else {
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.RemoveHistoryById
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
import eu.kanade.domain.history.model.HistoryWithRelations
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.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
@@ -58,20 +55,13 @@ class HistoryPresenter(
.map { pagingData ->
pagingData
.map {
UiModel.History(it)
UiModel.Item(it)
}
.insertSeparators { before, after ->
val beforeDate =
before?.item?.history?.last_read?.toDateKey()
val afterDate =
after?.item?.history?.last_read?.toDateKey()
val beforeDate = before?.item?.readAt?.time?.toDateKey() ?: Date(0)
val afterDate = after?.item?.readAt?.time?.toDateKey() ?: Date(0)
when {
beforeDate == null && afterDate != null -> UiModel.Header(
afterDate,
)
beforeDate != null && afterDate != null -> UiModel.Header(
afterDate,
)
beforeDate.time != afterDate.time && afterDate.time != 0L -> UiModel.Header(afterDate)
// Return null to avoid adding a separator between two items.
else -> null
}
@@ -90,7 +80,7 @@ class HistoryPresenter(
}
}
fun removeFromHistory(history: History) {
fun removeFromHistory(history: HistoryWithRelations) {
presenterScope.launchIO {
removeHistoryById.await(history)
}
@@ -102,9 +92,9 @@ class HistoryPresenter(
}
}
fun getNextChapterForManga(manga: Manga, chapter: Chapter) {
fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
presenterScope.launchIO {
val chapter = getNextChapterForManga.await(manga, chapter)
val chapter = getNextChapterForManga.await(mangaId, chapterId)
view?.openChapter(chapter)
}
}
@@ -121,7 +111,7 @@ class HistoryPresenter(
}
sealed class UiModel {
data class History(val item: MangaChapterHistory) : UiModel()
data class Item(val item: HistoryWithRelations) : UiModel()
data class Header(val date: Date) : UiModel()
}

View File

@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.setting.database
import android.os.Bundle
import eu.kanade.tachiyomi.Database
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
@@ -13,6 +14,7 @@ import uy.kohesive.injekt.api.get
class ClearDatabasePresenter : BasePresenter<ClearDatabaseController>() {
private val db = Injekt.get<DatabaseHelper>()
private val database = Injekt.get<Database>()
private val sourceManager = Injekt.get<SourceManager>()
@@ -26,7 +28,7 @@ class ClearDatabasePresenter : BasePresenter<ClearDatabaseController>() {
fun clearDatabaseForSourceIds(sources: List<Long>) {
db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking()
db.deleteHistoryNoLastRead().executeAsBlocking()
database.historyQueries.removeResettedHistory()
}
private fun getDatabaseSourcesObservable(): Observable<List<ClearDatabaseSourceItem>> {