diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 32adbe100..55162b0ed 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -6,6 +6,7 @@ plugins { kotlin("android") kotlin("plugin.serialization") id("com.github.zellius.shortcut-helper") + id("com.squareup.sqldelight") } if (gradle.startParameter.taskRequests.toString().contains("Standard")) { @@ -147,6 +148,9 @@ dependencies { implementation(androidx.paging.runtime) implementation(androidx.paging.compose) + implementation(libs.sqldelight.android.driver) + implementation(libs.sqldelight.coroutines) + implementation(libs.sqldelight.android.paging) implementation(kotlinx.reflect) diff --git a/app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt b/app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt new file mode 100644 index 000000000..bd4d99fde --- /dev/null +++ b/app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt @@ -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() + + override suspend fun await(inTransaction: Boolean, block: suspend Database.() -> T): T { + return dispatch(inTransaction, block) + } + + override suspend fun awaitList( + inTransaction: Boolean, + block: suspend Database.() -> Query + ): List { + return dispatch(inTransaction) { block(db).executeAsList() } + } + + override suspend fun awaitOne( + inTransaction: Boolean, + block: suspend Database.() -> Query + ): T { + return dispatch(inTransaction) { block(db).executeAsOne() } + } + + override suspend fun awaitOneOrNull( + inTransaction: Boolean, + block: suspend Database.() -> Query + ): T? { + return dispatch(inTransaction) { block(db).executeAsOneOrNull() } + } + + override fun subscribeToList(block: Database.() -> Query): Flow> { + return block(db).asFlow().mapToList(queryDispatcher) + } + + override fun subscribeToOne(block: Database.() -> Query): Flow { + return block(db).asFlow().mapToOne(queryDispatcher) + } + + override fun subscribeToOneOrNull(block: Database.() -> Query): Flow { + return block(db).asFlow().mapToOneOrNull(queryDispatcher) + } + + override fun subscribeToPagingSource( + countQuery: Database.() -> Query, + transacter: Database.() -> Transacter, + queryProvider: Database.(Long, Long) -> Query + ): PagingSource { + return QueryPagingSource( + countQuery = countQuery(db), + transacter = transacter(db), + dispatcher = queryDispatcher, + queryProvider = { limit, offset -> + queryProvider.invoke(db, limit, offset) + } + ) + } + + private suspend fun 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) } + } +} diff --git a/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt b/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt new file mode 100644 index 000000000..445428b8b --- /dev/null +++ b/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt @@ -0,0 +1,20 @@ +package eu.kanade.data + +import com.squareup.sqldelight.ColumnAdapter +import java.util.* + +val dateAdapter = object : ColumnAdapter { + override fun decode(databaseValue: Long): Date = Date(databaseValue) + override fun encode(value: Date): Long = value.time +} + +private const val listOfStringsSeparator = ", " +val listOfStringsAdapter = object : ColumnAdapter, String> { + override fun decode(databaseValue: String) = + if (databaseValue.isEmpty()) { + listOf() + } else { + databaseValue.split(listOfStringsSeparator) + } + override fun encode(value: List) = value.joinToString(separator = listOfStringsSeparator) +} diff --git a/app/src/main/java/eu/kanade/data/DatabaseHandler.kt b/app/src/main/java/eu/kanade/data/DatabaseHandler.kt new file mode 100644 index 000000000..a528b7010 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/DatabaseHandler.kt @@ -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 await(inTransaction: Boolean = false, block: suspend Database.() -> T): T + + suspend fun awaitList( + inTransaction: Boolean = false, + block: suspend Database.() -> Query + ): List + + suspend fun awaitOne( + inTransaction: Boolean = false, + block: suspend Database.() -> Query + ): T + + suspend fun awaitOneOrNull( + inTransaction: Boolean = false, + block: suspend Database.() -> Query + ): T? + + fun subscribeToList(block: Database.() -> Query): Flow> + + fun subscribeToOne(block: Database.() -> Query): Flow + + fun subscribeToOneOrNull(block: Database.() -> Query): Flow + + fun subscribeToPagingSource( + countQuery: Database.() -> Query, + transacter: Database.() -> Transacter, + queryProvider: Database.(Long, Long) -> Query + ): PagingSource +} diff --git a/app/src/main/java/eu/kanade/data/TransactionContext.kt b/app/src/main/java/eu/kanade/data/TransactionContext.kt new file mode 100644 index 000000000..156b4cdba --- /dev/null +++ b/app/src/main/java/eu/kanade/data/TransactionContext.kt @@ -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 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 + + override val key: CoroutineContext.Key + 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() + } + } +} diff --git a/app/src/main/java/eu/kanade/data/chapter/ChapterMapper.kt b/app/src/main/java/eu/kanade/data/chapter/ChapterMapper.kt new file mode 100644 index 000000000..05de96ef2 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/chapter/ChapterMapper.kt @@ -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, + ) + } diff --git a/app/src/main/java/eu/kanade/data/history/HistoryMapper.kt b/app/src/main/java/eu/kanade/data/history/HistoryMapper.kt new file mode 100644 index 000000000..ff94b1d61 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/history/HistoryMapper.kt @@ -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 + ) +} diff --git a/app/src/main/java/eu/kanade/data/history/HistoryRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/history/HistoryRepositoryImpl.kt new file mode 100644 index 000000000..15d2d2633 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/history/HistoryRepositoryImpl.kt @@ -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 { + 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 + } + } +} diff --git a/app/src/main/java/eu/kanade/data/history/local/HistoryPagingSource.kt b/app/src/main/java/eu/kanade/data/history/local/HistoryPagingSource.kt deleted file mode 100644 index 95a0d6e52..000000000 --- a/app/src/main/java/eu/kanade/data/history/local/HistoryPagingSource.kt +++ /dev/null @@ -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() { - - override fun getRefreshKey(state: PagingState): 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): LoadResult.Page { - 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 - } -} diff --git a/app/src/main/java/eu/kanade/data/history/repository/HistoryRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/history/repository/HistoryRepositoryImpl.kt deleted file mode 100644 index af610d684..000000000 --- a/app/src/main/java/eu/kanade/data/history/repository/HistoryRepositoryImpl.kt +++ /dev/null @@ -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 - } - } - } -} diff --git a/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt b/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt new file mode 100644 index 000000000..96ede4e5b --- /dev/null +++ b/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt @@ -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, 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, + ) + } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt index d54c52c2a..9462ae7f9 100644 --- a/app/src/main/java/eu/kanade/domain/DomainModule.kt +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -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 diff --git a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt new file mode 100644 index 000000000..6eff7c580 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt @@ -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? +) diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/GetHistory.kt b/app/src/main/java/eu/kanade/domain/history/interactor/GetHistory.kt index d376e0a1b..d2f8302b7 100644 --- a/app/src/main/java/eu/kanade/domain/history/interactor/GetHistory.kt +++ b/app/src/main/java/eu/kanade/domain/history/interactor/GetHistory.kt @@ -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> { + fun subscribe(query: String): Flow> { return Pager( - PagingConfig(pageSize = HistoryPagingSource.PAGE_SIZE) + PagingConfig(pageSize = 25) ) { repository.getHistory(query) }.flow diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/GetNextChapterForManga.kt b/app/src/main/java/eu/kanade/domain/history/interactor/GetNextChapterForManga.kt index ecaf53af7..477408ca3 100644 --- a/app/src/main/java/eu/kanade/domain/history/interactor/GetNextChapterForManga.kt +++ b/app/src/main/java/eu/kanade/domain/history/interactor/GetNextChapterForManga.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt index a0f022fd6..93012c266 100644 --- a/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt +++ b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryByMangaId.kt b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryByMangaId.kt index 1868a1ba2..f32fa5f7b 100644 --- a/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryByMangaId.kt +++ b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryByMangaId.kt @@ -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) } } diff --git a/app/src/main/java/eu/kanade/domain/history/model/History.kt b/app/src/main/java/eu/kanade/domain/history/model/History.kt new file mode 100644 index 000000000..b0ba50695 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/model/History.kt @@ -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? +) diff --git a/app/src/main/java/eu/kanade/domain/history/model/HistoryWithRelations.kt b/app/src/main/java/eu/kanade/domain/history/model/HistoryWithRelations.kt new file mode 100644 index 000000000..5faa8eb24 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/model/HistoryWithRelations.kt @@ -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? +) diff --git a/app/src/main/java/eu/kanade/domain/history/repository/HistoryRepository.kt b/app/src/main/java/eu/kanade/domain/history/repository/HistoryRepository.kt index 5846b2e16..38e0f4192 100644 --- a/app/src/main/java/eu/kanade/domain/history/repository/HistoryRepository.kt +++ b/app/src/main/java/eu/kanade/domain/history/repository/HistoryRepository.kt @@ -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 - suspend fun getHistory(limit: Int, page: Int, query: String): List + 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 } diff --git a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt new file mode 100644 index 000000000..9226c3f61 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt @@ -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?, + 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 + } +} diff --git a/app/src/main/java/eu/kanade/presentation/components/MangaCover.kt b/app/src/main/java/eu/kanade/presentation/components/MangaCover.kt index 524b5744f..ca9332d94 100644 --- a/app/src/main/java/eu/kanade/presentation/components/MangaCover.kt +++ b/app/src/main/java/eu/kanade/presentation/components/MangaCover.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt index a0d00cd65..0b687d363 100644 --- a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -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, - 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(null) } + val (removeState, setRemoveState) = remember { mutableStateOf(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) ) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index 39c42dd64..c9147b6c6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -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 { + AndroidSqliteDriver( + schema = Database.Schema, + context = app, + name = DbOpenCallback.DATABASE_NAME, + callback = get() + ) + } + + addSingletonFactory { + Database( + driver = get(), + historyAdapter = History.Adapter( + history_last_readAdapter = dateAdapter, + history_time_readAdapter = dateAdapter + ), + mangasAdapter = Mangas.Adapter( + genreAdapter = listOfStringsAdapter + ) + ) + } + + addSingletonFactory { 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() + get() + get() get() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt index 474d2d6b8..36c1d3d49 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt @@ -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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt index 31702be03..ef541ac42 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt @@ -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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt index b1fe42904..fd9349789 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt @@ -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) = 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() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index 6c1a424a0..348811939 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -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} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt index 76ffd7187..3697ee60e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt @@ -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 - )""" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt index 793349119..8914e6f3c 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt @@ -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" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/HistoryTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/HistoryTable.kt index 9d19544a4..4dfe9f0dd 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/HistoryTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/HistoryTable.kt @@ -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)" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt index 578a85bbc..d39b32adf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt @@ -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 - )""" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt index fe9e6845f..5c32e16bc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt @@ -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" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt index 5a9a8f239..90c38d537 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt @@ -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" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 26fc7dea7..594325527 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -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() val manga = db.getManga(mangaId).executeAsBlocking() val chapter = db.getChapter(chapterId).executeAsBlocking() if (manga != null && chapter != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index c381f1194..89f8c7295 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -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), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt index 70d5aefbe..c5facc3b4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt @@ -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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt index aa726176c..65f452c48 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt @@ -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() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt index d209779a3..192bd81a9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt @@ -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() { private val db = Injekt.get() + private val database = Injekt.get() private val sourceManager = Injekt.get() @@ -26,7 +28,7 @@ class ClearDatabasePresenter : BasePresenter() { fun clearDatabaseForSourceIds(sources: List) { db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking() - db.deleteHistoryNoLastRead().executeAsBlocking() + database.historyQueries.removeResettedHistory() } private fun getDatabaseSourcesObservable(): Observable> { diff --git a/app/src/main/sqldelight/data/categories.sq b/app/src/main/sqldelight/data/categories.sq new file mode 100644 index 000000000..628b6df7f --- /dev/null +++ b/app/src/main/sqldelight/data/categories.sq @@ -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 +); \ No newline at end of file diff --git a/app/src/main/sqldelight/data/chapters.sq b/app/src/main/sqldelight/data/chapters.sq new file mode 100644 index 000000000..e368d56f2 --- /dev/null +++ b/app/src/main/sqldelight/data/chapters.sq @@ -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; \ No newline at end of file diff --git a/app/src/main/sqldelight/data/history.sq b/app/src/main/sqldelight/data/history.sq new file mode 100644 index 000000000..1eb68f1e8 --- /dev/null +++ b/app/src/main/sqldelight/data/history.sq @@ -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; diff --git a/app/src/main/sqldelight/data/manga_sync.sq b/app/src/main/sqldelight/data/manga_sync.sq new file mode 100644 index 000000000..dcd18442b --- /dev/null +++ b/app/src/main/sqldelight/data/manga_sync.sq @@ -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 +); \ No newline at end of file diff --git a/app/src/main/sqldelight/data/mangas.sq b/app/src/main/sqldelight/data/mangas.sq new file mode 100644 index 000000000..09c57ae49 --- /dev/null +++ b/app/src/main/sqldelight/data/mangas.sq @@ -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, + 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; \ No newline at end of file diff --git a/app/src/main/sqldelight/data/mangas_categories.sq b/app/src/main/sqldelight/data/mangas_categories.sq new file mode 100644 index 000000000..6db91fe16 --- /dev/null +++ b/app/src/main/sqldelight/data/mangas_categories.sq @@ -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 +); \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/1.sqm b/app/src/main/sqldelight/migrations/1.sqm new file mode 100644 index 000000000..7ae4198c7 --- /dev/null +++ b/app/src/main/sqldelight/migrations/1.sqm @@ -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; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/10.sqm b/app/src/main/sqldelight/migrations/10.sqm new file mode 100644 index 000000000..20a2c8444 --- /dev/null +++ b/app/src/main/sqldelight/migrations/10.sqm @@ -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 +); \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/11.sqm b/app/src/main/sqldelight/migrations/11.sqm new file mode 100644 index 000000000..23b429acd --- /dev/null +++ b/app/src/main/sqldelight/migrations/11.sqm @@ -0,0 +1,2 @@ +ALTER TABLE mangas +ADD COLUMN next_update INTEGER DEFAULT 0; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/12.sqm b/app/src/main/sqldelight/migrations/12.sqm new file mode 100644 index 000000000..c80623439 --- /dev/null +++ b/app/src/main/sqldelight/migrations/12.sqm @@ -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; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/13.sqm b/app/src/main/sqldelight/migrations/13.sqm new file mode 100644 index 000000000..78e1ece21 --- /dev/null +++ b/app/src/main/sqldelight/migrations/13.sqm @@ -0,0 +1,3 @@ +UPDATE chapters +SET date_upload = date_fetch +WHERE date_upload = 0; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/14.sqm b/app/src/main/sqldelight/migrations/14.sqm new file mode 100644 index 000000000..918958392 --- /dev/null +++ b/app/src/main/sqldelight/migrations/14.sqm @@ -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; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/2.sqm b/app/src/main/sqldelight/migrations/2.sqm new file mode 100644 index 000000000..345e24c6e --- /dev/null +++ b/app/src/main/sqldelight/migrations/2.sqm @@ -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); \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/3.sqm b/app/src/main/sqldelight/migrations/3.sqm new file mode 100644 index 000000000..42ffba899 --- /dev/null +++ b/app/src/main/sqldelight/migrations/3.sqm @@ -0,0 +1,2 @@ +ALTER TABLE chapters +ADD COLUMN bookmark INTEGER DEFAULT 0; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/4.sqm b/app/src/main/sqldelight/migrations/4.sqm new file mode 100644 index 000000000..7fc06a612 --- /dev/null +++ b/app/src/main/sqldelight/migrations/4.sqm @@ -0,0 +1,2 @@ +ALTER TABLE chapters +ADD COLUMN scanlator TEXT DEFAULT NULL; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/5.sqm b/app/src/main/sqldelight/migrations/5.sqm new file mode 100644 index 000000000..a1e3b8378 --- /dev/null +++ b/app/src/main/sqldelight/migrations/5.sqm @@ -0,0 +1,2 @@ +ALTER TABLE manga_sync +ADD COLUMN remote_url TEXT DEFAULT ''; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/6.sqm b/app/src/main/sqldelight/migrations/6.sqm new file mode 100644 index 000000000..00ee92e22 --- /dev/null +++ b/app/src/main/sqldelight/migrations/6.sqm @@ -0,0 +1,2 @@ +ALTER TABLE manga_sync +ADD COLUMN library_id INTEGER; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/7.sqm b/app/src/main/sqldelight/migrations/7.sqm new file mode 100644 index 000000000..03492df76 --- /dev/null +++ b/app/src/main/sqldelight/migrations/7.sqm @@ -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; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/8.sqm b/app/src/main/sqldelight/migrations/8.sqm new file mode 100644 index 000000000..dc47263a8 --- /dev/null +++ b/app/src/main/sqldelight/migrations/8.sqm @@ -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; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/9.sqm b/app/src/main/sqldelight/migrations/9.sqm new file mode 100644 index 000000000..6eb647300 --- /dev/null +++ b/app/src/main/sqldelight/migrations/9.sqm @@ -0,0 +1,2 @@ +ALTER TABLE mangas +ADD COLUMN cover_last_modified INTEGER NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/app/src/main/sqldelight/view/historyView.sq b/app/src/main/sqldelight/view/historyView.sq new file mode 100644 index 000000000..2471f85aa --- /dev/null +++ b/app/src/main/sqldelight/view/historyView.sq @@ -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; diff --git a/build.gradle.kts b/build.gradle.kts index f1a44de2b..efb20b7e9 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ buildscript { classpath(libs.google.services.gradle) classpath(libs.aboutlibraries.gradle) classpath(kotlinx.serialization.gradle) + classpath("com.squareup.sqldelight:gradle-plugin:1.5.3") } } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index c2c0f7b6d..3f332967a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ conductor_version = "3.1.2" flowbinding_version = "1.2.0" shizuku_version = "12.1.0" robolectric_version = "3.1.4" +sqldelight = "1.5.3" [libraries] 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" +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] reactivex = ["rxandroid","rxjava","rxrelay"] okhttp = ["okhttp-core","okhttp-logging","okhttp-dnsoverhttps"]