From a8cb77cc7e11c72995449c6009fc07251b72a216 Mon Sep 17 00:00:00 2001 From: Andreas Date: Thu, 21 Apr 2022 21:45:56 +0200 Subject: [PATCH] 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> (cherry picked from commit b1f46ed8302411fbd884bbc3c26fe28a378fd91a) # Conflicts: # app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt # app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt # app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt # app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt # app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt # app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt # app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt # build.gradle.kts --- app/build.gradle.kts | 4 + .../eu/kanade/data/AndroidDatabaseHandler.kt | 94 ++++++ .../java/eu/kanade/data/DatabaseAdapter.kt | 20 ++ .../java/eu/kanade/data/DatabaseHandler.kt | 39 +++ .../java/eu/kanade/data/TransactionContext.kt | 160 ++++++++++ .../eu/kanade/data/chapter/ChapterMapper.kt | 21 ++ .../eu/kanade/data/history/HistoryMapper.kt | 26 ++ .../data/history/HistoryRepositoryImpl.kt | 91 ++++++ .../data/history/local/HistoryPagingSource.kt | 43 --- .../repository/HistoryRepositoryImpl.kt | 137 --------- .../java/eu/kanade/data/manga/MangaMapper.kt | 27 ++ .../java/eu/kanade/domain/DomainModule.kt | 2 +- .../eu/kanade/domain/chapter/model/Chapter.kt | 16 + .../domain/history/interactor/GetHistory.kt | 7 +- .../interactor/GetNextChapterForManga.kt | 7 +- .../history/interactor/RemoveHistoryById.kt | 14 +- .../interactor/RemoveHistoryByMangaId.kt | 4 +- .../eu/kanade/domain/history/model/History.kt | 9 + .../history/model/HistoryWithRelations.kt | 13 + .../history/repository/HistoryRepository.kt | 18 +- .../eu/kanade/domain/manga/model/Manga.kt | 39 +++ .../presentation/components/MangaCover.kt | 5 +- .../presentation/history/HistoryScreen.kt | 45 +-- .../java/eu/kanade/tachiyomi/AppModule.kt | 39 ++- .../tachiyomi/data/database/DatabaseHelper.kt | 7 +- .../tachiyomi/data/database/DbOpenCallback.kt | 107 +------ .../data/database/queries/HistoryQueries.kt | 56 ---- .../data/database/queries/RawQueries.kt | 3 +- .../data/database/tables/CategoryTable.kt | 15 - .../data/database/tables/ChapterTable.kt | 38 --- .../data/database/tables/HistoryTable.kt | 20 -- .../database/tables/MangaCategoryTable.kt | 12 - .../data/database/tables/MangaTable.kt | 55 ---- .../data/database/tables/TrackTable.kt | 40 --- .../data/notification/NotificationReceiver.kt | 2 +- .../process/MigrationProcessAdapter.kt | 2 +- .../migration/search/SearchController.kt | 4 +- .../tachiyomi/ui/manga/MangaController.kt | 3 + .../ui/recent/history/HistoryController.kt | 16 +- .../ui/recent/history/HistoryPresenter.kt | 34 +-- .../database/ClearDatabasePresenter.kt | 4 +- .../sql/tables/FavoriteEntryTable.kt | 16 - .../java/exh/merged/sql/tables/MergedTable.kt | 28 -- .../sql/tables/SearchMetadataTable.kt | 21 -- .../exh/metadata/sql/tables/SearchTagTable.kt | 20 -- .../metadata/sql/tables/SearchTitleTable.kt | 19 -- .../tables/FeedSavedSearchTable.kt | 14 - .../savedsearches/tables/SavedSearchTable.kt | 10 - app/src/main/sqldelight/data/categories.sq | 7 + app/src/main/sqldelight/data/chapters.sq | 26 ++ app/src/main/sqldelight/data/eh_favorites.sq | 7 + .../main/sqldelight/data/feed_saved_search.sq | 10 + app/src/main/sqldelight/data/history.sq | 35 +++ app/src/main/sqldelight/data/manga_sync.sq | 18 ++ app/src/main/sqldelight/data/mangas.sq | 29 ++ .../main/sqldelight/data/mangas_categories.sq | 9 + app/src/main/sqldelight/data/merged.sq | 19 ++ app/src/main/sqldelight/data/saved_search.sq | 7 + .../main/sqldelight/data/search_metadata.sq | 12 + app/src/main/sqldelight/data/search_tags.sq | 12 + app/src/main/sqldelight/data/search_titles.sq | 11 + app/src/main/sqldelight/migrations/1.sqm | 2 + app/src/main/sqldelight/migrations/10.sqm | 7 + app/src/main/sqldelight/migrations/11.sqm | 7 + app/src/main/sqldelight/migrations/12.sqm | 16 + app/src/main/sqldelight/migrations/13.sqm | 275 ++++++++++++++++++ app/src/main/sqldelight/migrations/2.sqm | 11 + app/src/main/sqldelight/migrations/3.sqm | 19 ++ app/src/main/sqldelight/migrations/4.sqm | 0 app/src/main/sqldelight/migrations/5.sqm | 2 + app/src/main/sqldelight/migrations/6.sqm | 1 + app/src/main/sqldelight/migrations/7.sqm | 2 + app/src/main/sqldelight/migrations/8.sqm | 9 + app/src/main/sqldelight/migrations/9.sqm | 3 + app/src/main/sqldelight/view/historyView.sq | 46 +++ build.gradle.kts | 1 + gradle/libs.versions.toml | 5 + 77 files changed, 1301 insertions(+), 733 deletions(-) create mode 100644 app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt create mode 100644 app/src/main/java/eu/kanade/data/DatabaseAdapter.kt create mode 100644 app/src/main/java/eu/kanade/data/DatabaseHandler.kt create mode 100644 app/src/main/java/eu/kanade/data/TransactionContext.kt create mode 100644 app/src/main/java/eu/kanade/data/chapter/ChapterMapper.kt create mode 100644 app/src/main/java/eu/kanade/data/history/HistoryMapper.kt create mode 100644 app/src/main/java/eu/kanade/data/history/HistoryRepositoryImpl.kt delete mode 100644 app/src/main/java/eu/kanade/data/history/local/HistoryPagingSource.kt delete mode 100644 app/src/main/java/eu/kanade/data/history/repository/HistoryRepositoryImpl.kt create mode 100644 app/src/main/java/eu/kanade/data/manga/MangaMapper.kt create mode 100644 app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt create mode 100644 app/src/main/java/eu/kanade/domain/history/model/History.kt create mode 100644 app/src/main/java/eu/kanade/domain/history/model/HistoryWithRelations.kt create mode 100644 app/src/main/java/eu/kanade/domain/manga/model/Manga.kt create mode 100644 app/src/main/sqldelight/data/categories.sq create mode 100644 app/src/main/sqldelight/data/chapters.sq create mode 100644 app/src/main/sqldelight/data/eh_favorites.sq create mode 100644 app/src/main/sqldelight/data/feed_saved_search.sq create mode 100644 app/src/main/sqldelight/data/history.sq create mode 100644 app/src/main/sqldelight/data/manga_sync.sq create mode 100644 app/src/main/sqldelight/data/mangas.sq create mode 100644 app/src/main/sqldelight/data/mangas_categories.sq create mode 100644 app/src/main/sqldelight/data/merged.sq create mode 100644 app/src/main/sqldelight/data/saved_search.sq create mode 100644 app/src/main/sqldelight/data/search_metadata.sq create mode 100644 app/src/main/sqldelight/data/search_tags.sq create mode 100644 app/src/main/sqldelight/data/search_titles.sq create mode 100644 app/src/main/sqldelight/migrations/1.sqm create mode 100644 app/src/main/sqldelight/migrations/10.sqm create mode 100644 app/src/main/sqldelight/migrations/11.sqm create mode 100644 app/src/main/sqldelight/migrations/12.sqm create mode 100644 app/src/main/sqldelight/migrations/13.sqm create mode 100644 app/src/main/sqldelight/migrations/2.sqm create mode 100644 app/src/main/sqldelight/migrations/3.sqm create mode 100644 app/src/main/sqldelight/migrations/4.sqm create mode 100644 app/src/main/sqldelight/migrations/5.sqm create mode 100644 app/src/main/sqldelight/migrations/6.sqm create mode 100644 app/src/main/sqldelight/migrations/7.sqm create mode 100644 app/src/main/sqldelight/migrations/8.sqm create mode 100644 app/src/main/sqldelight/migrations/9.sqm create mode 100644 app/src/main/sqldelight/view/historyView.sq diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 284d2cfd0..a79ca4bec 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { kotlin("plugin.parcelize") kotlin("plugin.serialization") id("com.github.zellius.shortcut-helper") + id("com.squareup.sqldelight") } if (gradle.startParameter.taskRequests.toString().contains("Standard")) { @@ -130,6 +131,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..d51e2c514 --- /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.Date + +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..e7ebf5cb4 --- /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.Date + +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..d60316f3b --- /dev/null +++ b/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt @@ -0,0 +1,27 @@ +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, String?) -> Manga = + { id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewer, chapterFlags, coverLastModified, dateAdded, filteredScanlators -> + 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, + filteredScanlators = filteredScanlators + ) + } 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..58c1c985e --- /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.Date + +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..6f5a8e1fc --- /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.Date + +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..7511a40a7 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt @@ -0,0 +1,39 @@ +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, + // SY --> + val filteredScanlators: String?, +// SY <-- +) { + + 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..9689fb73a 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 @@ -59,7 +60,7 @@ import uy.kohesive.injekt.api.get import java.text.DateFormat import java.text.DecimalFormat import java.text.DecimalFormatSymbols -import java.util.* +import java.util.Date val chapterFormatter = DecimalFormat( "#.###", @@ -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 9bbad01a5..5fbf2749b 100755 --- 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.library.CustomMangaManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper @@ -27,11 +36,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) } @@ -65,6 +100,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 87ee4a841..5d02ebc64 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt @@ -47,7 +47,10 @@ 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, @@ -66,7 +69,7 @@ open class DatabaseHelper(context: Context) : 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 93826ab8a..ef541ac42 100755 --- 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,115 +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 exh.favorites.sql.tables.FavoriteEntryTable -import exh.merged.sql.tables.MergedTable -import exh.metadata.sql.tables.SearchMetadataTable -import exh.metadata.sql.tables.SearchTagTable -import exh.metadata.sql.tables.SearchTitleTable -import exh.savedsearches.tables.FeedSavedSearchTable -import exh.savedsearches.tables.SavedSearchTable +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 = /* SY --> */ 13 // SY <-- } - 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) - // SY --> - execSQL(SearchMetadataTable.createTableQuery) - execSQL(SearchTagTable.createTableQuery) - execSQL(SearchTitleTable.createTableQuery) - execSQL(MergedTable.createTableQuery) - execSQL(FavoriteEntryTable.createTableQuery) - execSQL(SavedSearchTable.createTableQuery) - execSQL(FeedSavedSearchTable.createTableQuery) - // SY <-- - - // DB indexes - execSQL(MangaTable.createUrlIndexQuery) - execSQL(MangaTable.createLibraryIndexQuery) - execSQL(ChapterTable.createMangaIdIndexQuery) - execSQL(ChapterTable.createUnreadChaptersIndexQuery) - execSQL(HistoryTable.createChapterIdIndexQuery) - // SY --> - execSQL(SearchMetadataTable.createUploaderIndexQuery) - execSQL(SearchMetadataTable.createIndexedExtraIndexQuery) - execSQL(SearchTagTable.createMangaIdIndexQuery) - execSQL(SearchTagTable.createNamespaceNameIndexQuery) - execSQL(SearchTitleTable.createMangaIdIndexQuery) - execSQL(SearchTitleTable.createTitleIndexQuery) - execSQL(MergedTable.createIndexQuery) - execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery) - // SY <-- + 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(MangaTable.addCoverLastModified) - } - if (oldVersion < 3) { - db.execSQL(MangaTable.addDateAdded) - db.execSQL(MangaTable.backfillDateAdded) - } - if (oldVersion < 4) { - db.execSQL(MergedTable.dropTableQuery) - db.execSQL(MergedTable.createTableQuery) - db.execSQL(MergedTable.createIndexQuery) - } - /*if (oldVersion < 5) { - db.execSQL(SimilarTable.createTableQuery) - db.execSQL(SimilarTable.createMangaIdIndexQuery) - }*/ - if (oldVersion < 6) { - db.execSQL(MangaTable.addFilteredScanlators) - } - if (oldVersion < 7) { - db.execSQL("DROP TABLE IF EXISTS manga_related") - } - if (oldVersion < 8) { - db.execSQL(MangaTable.addNextUpdateCol) - } - if (oldVersion < 9) { - db.execSQL(TrackTable.renameTableToTemp) - db.execSQL(TrackTable.createTableQuery) - db.execSQL(TrackTable.insertFromTempTable) - db.execSQL(TrackTable.dropTempTable) - } - if (oldVersion < 10) { - db.execSQL(ChapterTable.fixDateUploadIfNeeded) - } - if (oldVersion < 11) { - db.execSQL(FavoriteEntryTable.createTableQuery) - } - if (oldVersion < 12) { - db.execSQL(FavoriteEntryTable.fixTableQuery) - } - if (oldVersion < 13) { - db.execSQL(SavedSearchTable.createTableQuery) - db.execSQL(FeedSavedSearchTable.createTableQuery) - db.execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery) - } + 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 e8998e64a..e2ab2f346 100755 --- 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,40 +4,12 @@ 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.HistoryChapterIdPutResolver 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( @@ -80,34 +52,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 8d3e69f3e..8327e0c2e 100755 --- 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 @@ -190,7 +190,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 5ca03b56e..80a88ca55 100755 --- 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 @@ -15,19 +15,4 @@ object CategoryTable { // SY --> const val COL_MANGA_ORDER = "manga_order" // SY <-- - - 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, - $COL_MANGA_ORDER TEXT NOT NULL - )""" - - // SY --> - val addMangaOrder: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT" - // SY <-- } 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 100755 --- 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 100755 --- 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 100755 --- 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 9f2e46f78..3e42dea7a 100755 --- 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 @@ -51,59 +51,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, - $COL_FILTERED_SCANLATORS TEXT - )""" - - 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" - - // SY --> - val addFilteredScanlators: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT" - // SY <-- } 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 100755 --- 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 100755 --- 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/browse/migration/advanced/process/MigrationProcessAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt index 23b9b6c47..c45e49ec9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt @@ -137,7 +137,7 @@ class MigrationProcessAdapter( } } db.insertChapters(dbChapters).executeAsBlocking() - db.updateHistoryLastRead(historyList).executeAsBlocking() + db.upsertHistoryLastRead(historyList).executeAsBlocking() } // Update categories if (MigrationFlags.hasCategories(flags)) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt index 1b709751f..94bd21a1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt @@ -37,8 +37,8 @@ class SearchController( Injekt.get().getManga(mangaId).executeAsBlocking(), sources.map { Injekt.get().getOrStub(it) }.filterIsInstance(), ) { - this.targetController = targetController - } + this.targetController = targetController + } @Suppress("unused") constructor(bundle: Bundle) : this( 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 f43e8073e..f88bdce56 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 @@ -136,6 +137,8 @@ class MangaController : DownloadCustomChaptersDialog.Listener, DeleteChaptersDialog.Listener { + constructor(history: HistoryWithRelations) : this(history.mangaId) + constructor(manga: Manga?, fromSource: Boolean = false, smartSearchConfig: SourceController.SmartSearchConfig? = null, update: 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..7fd31dfa5 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 @@ -28,7 +25,7 @@ import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.update import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.api.get -import java.util.* +import java.util.Date /** * Presenter of HistoryFragment. @@ -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,10 +92,12 @@ class HistoryPresenter( } } - fun getNextChapterForManga(manga: Manga, chapter: Chapter) { + fun getNextChapterForManga(mangaId: Long, chapterId: Long) { presenterScope.launchIO { - val chapter = getNextChapterForManga.await(manga, chapter) - view?.openChapter(chapter) + val chapter = getNextChapterForManga.await(mangaId, chapterId) + launchUI { + view?.openChapter(chapter) + } } } @@ -121,7 +113,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 0f796ee12..e0c694a15 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() @@ -32,7 +34,7 @@ class ClearDatabasePresenter : BasePresenter() { db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking() } // SY <-- - db.deleteHistoryNoLastRead().executeAsBlocking() + database.historyQueries.removeResettedHistory() } private fun getDatabaseSourcesObservable(): Observable> { diff --git a/app/src/main/java/exh/favorites/sql/tables/FavoriteEntryTable.kt b/app/src/main/java/exh/favorites/sql/tables/FavoriteEntryTable.kt index e29e2914c..52b7b0f57 100644 --- a/app/src/main/java/exh/favorites/sql/tables/FavoriteEntryTable.kt +++ b/app/src/main/java/exh/favorites/sql/tables/FavoriteEntryTable.kt @@ -13,20 +13,4 @@ object FavoriteEntryTable { const val COL_TOKEN = "token" const val COL_CATEGORY = "category" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_TITLE TEXT NOT NULL, - $COL_GID TEXT NOT NULL, - $COL_TOKEN TEXT NOT NULL, - $COL_CATEGORY INTEGER NOT NULL - )""" - - val fixTableQuery: String - get() = createTableQuery.replace( - "CREATE TABLE", - "CREATE TABLE IF NOT EXISTS", - ) } diff --git a/app/src/main/java/exh/merged/sql/tables/MergedTable.kt b/app/src/main/java/exh/merged/sql/tables/MergedTable.kt index c54bf5cac..5648c188f 100644 --- a/app/src/main/java/exh/merged/sql/tables/MergedTable.kt +++ b/app/src/main/java/exh/merged/sql/tables/MergedTable.kt @@ -1,7 +1,5 @@ package exh.merged.sql.tables -import eu.kanade.tachiyomi.data.database.tables.MangaTable - object MergedTable { const val TABLE = "merged" @@ -27,30 +25,4 @@ object MergedTable { const val COL_MANGA_URL = "manga_url" const val COL_MANGA_SOURCE = "manga_source" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_IS_INFO_MANGA BOOLEAN NOT NULL, - $COL_GET_CHAPTER_UPDATES BOOLEAN NOT NULL, - $COL_CHAPTER_SORT_MODE INTEGER NOT NULL, - $COL_CHAPTER_PRIORITY INTEGER NOT NULL, - $COL_DOWNLOAD_CHAPTERS BOOLEAN NOT NULL, - $COL_MERGE_ID INTEGER NOT NULL, - $COL_MERGE_URL TEXT NOT NULL, - $COL_MANGA_ID INTEGER, - $COL_MANGA_URL TEXT NOT NULL, - $COL_MANGA_SOURCE INTEGER NOT NULL, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE SET NULL, - FOREIGN KEY($COL_MERGE_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" - - val dropTableQuery: String - get() = "DROP TABLE $TABLE" - - val createIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_MERGE_ID}_index ON $TABLE($COL_MERGE_ID)" } diff --git a/app/src/main/java/exh/metadata/sql/tables/SearchMetadataTable.kt b/app/src/main/java/exh/metadata/sql/tables/SearchMetadataTable.kt index 434f52f65..2196883c0 100755 --- a/app/src/main/java/exh/metadata/sql/tables/SearchMetadataTable.kt +++ b/app/src/main/java/exh/metadata/sql/tables/SearchMetadataTable.kt @@ -1,7 +1,5 @@ package exh.metadata.sql.tables -import eu.kanade.tachiyomi.data.database.tables.MangaTable - object SearchMetadataTable { const val TABLE = "search_metadata" @@ -14,23 +12,4 @@ object SearchMetadataTable { const val COL_INDEXED_EXTRA = "indexed_extra" const val COL_EXTRA_VERSION = "extra_version" - - // Insane foreign, primary key to avoid touch manga table - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_MANGA_ID INTEGER NOT NULL PRIMARY KEY, - $COL_UPLOADER TEXT, - $COL_EXTRA TEXT NOT NULL, - $COL_INDEXED_EXTRA TEXT, - $COL_EXTRA_VERSION INT NOT NULL, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" - - val createUploaderIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_UPLOADER}_index ON $TABLE($COL_UPLOADER)" - - val createIndexedExtraIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_INDEXED_EXTRA}_index ON $TABLE($COL_INDEXED_EXTRA)" } diff --git a/app/src/main/java/exh/metadata/sql/tables/SearchTagTable.kt b/app/src/main/java/exh/metadata/sql/tables/SearchTagTable.kt index a9b3e72c3..a7aabc7ac 100755 --- a/app/src/main/java/exh/metadata/sql/tables/SearchTagTable.kt +++ b/app/src/main/java/exh/metadata/sql/tables/SearchTagTable.kt @@ -1,7 +1,5 @@ package exh.metadata.sql.tables -import eu.kanade.tachiyomi.data.database.tables.MangaTable - object SearchTagTable { const val TABLE = "search_tags" @@ -14,22 +12,4 @@ object SearchTagTable { const val COL_NAME = "name" const val COL_TYPE = "type" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_MANGA_ID INTEGER NOT NULL, - $COL_NAMESPACE TEXT, - $COL_NAME TEXT NOT NULL, - $COL_TYPE INT 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 createNamespaceNameIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_NAMESPACE}_${COL_NAME}_index ON $TABLE($COL_NAMESPACE, $COL_NAME)" } diff --git a/app/src/main/java/exh/metadata/sql/tables/SearchTitleTable.kt b/app/src/main/java/exh/metadata/sql/tables/SearchTitleTable.kt index 48b71e422..98e383296 100755 --- a/app/src/main/java/exh/metadata/sql/tables/SearchTitleTable.kt +++ b/app/src/main/java/exh/metadata/sql/tables/SearchTitleTable.kt @@ -1,7 +1,5 @@ package exh.metadata.sql.tables -import eu.kanade.tachiyomi.data.database.tables.MangaTable - object SearchTitleTable { const val TABLE = "search_titles" @@ -12,21 +10,4 @@ object SearchTitleTable { const val COL_TITLE = "title" const val COL_TYPE = "type" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_MANGA_ID INTEGER NOT NULL, - $COL_TITLE TEXT NOT NULL, - $COL_TYPE INT 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 createTitleIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_TITLE}_index ON $TABLE($COL_TITLE)" } diff --git a/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt b/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt index 6b6f5b631..b23dd1d8d 100644 --- a/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt +++ b/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt @@ -11,18 +11,4 @@ object FeedSavedSearchTable { const val COL_SAVED_SEARCH_ID = "saved_search" const val COL_GLOBAL = "global" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_SOURCE INTEGER NOT NULL, - $COL_SAVED_SEARCH_ID INTEGER, - $COL_GLOBAL BOOLEAN NOT NULL, - FOREIGN KEY($COL_SAVED_SEARCH_ID) REFERENCES ${SavedSearchTable.TABLE} (${SavedSearchTable.COL_ID}) - ON DELETE CASCADE - )""" - - val createSavedSearchIdIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_SAVED_SEARCH_ID}_index ON $TABLE($COL_SAVED_SEARCH_ID)" } diff --git a/app/src/main/java/exh/savedsearches/tables/SavedSearchTable.kt b/app/src/main/java/exh/savedsearches/tables/SavedSearchTable.kt index 3a345fb05..35e1839b7 100644 --- a/app/src/main/java/exh/savedsearches/tables/SavedSearchTable.kt +++ b/app/src/main/java/exh/savedsearches/tables/SavedSearchTable.kt @@ -13,14 +13,4 @@ object SavedSearchTable { const val COL_QUERY = "query" const val COL_FILTERS_JSON = "filters_json" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_SOURCE INTEGER NOT NULL, - $COL_NAME TEXT NOT NULL, - $COL_QUERY TEXT, - $COL_FILTERS_JSON TEXT - )""" } diff --git a/app/src/main/sqldelight/data/categories.sq b/app/src/main/sqldelight/data/categories.sq new file mode 100644 index 000000000..af3ea448f --- /dev/null +++ b/app/src/main/sqldelight/data/categories.sq @@ -0,0 +1,7 @@ +CREATE TABLE categories( + _id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + sort INTEGER NOT NULL, + flags INTEGER NOT NULL, + manga_order TEXT 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/eh_favorites.sq b/app/src/main/sqldelight/data/eh_favorites.sq new file mode 100644 index 000000000..d0cd2b495 --- /dev/null +++ b/app/src/main/sqldelight/data/eh_favorites.sq @@ -0,0 +1,7 @@ +CREATE TABLE eh_favorites ( + _id INTEGER NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + gid TEXT NOT NULL, + token TEXT NOT NULL, + category INTEGER NOT NULL +); \ No newline at end of file diff --git a/app/src/main/sqldelight/data/feed_saved_search.sq b/app/src/main/sqldelight/data/feed_saved_search.sq new file mode 100644 index 000000000..bf860b3ea --- /dev/null +++ b/app/src/main/sqldelight/data/feed_saved_search.sq @@ -0,0 +1,10 @@ +CREATE TABLE feed_saved_search ( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + saved_search INTEGER, + global INTEGER AS Boolean NOT NULL, + FOREIGN KEY(saved_search) REFERENCES saved_search (_id) + ON DELETE CASCADE +); + +CREATE INDEX feed_saved_search_saved_search_index ON feed_saved_search(saved_search); \ 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..89b03aec0 --- /dev/null +++ b/app/src/main/sqldelight/data/mangas.sq @@ -0,0 +1,29 @@ +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, + filtered_scanlators TEXT +); + +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/data/merged.sq b/app/src/main/sqldelight/data/merged.sq new file mode 100644 index 000000000..fc666df5c --- /dev/null +++ b/app/src/main/sqldelight/data/merged.sq @@ -0,0 +1,19 @@ +CREATE TABLE merged( + _id INTEGER NOT NULL PRIMARY KEY, + info_manga INTEGER AS Boolean NOT NULL, + get_chapter_updates INTEGER AS Boolean NOT NULL, + chapter_sort_mode INTEGER NOT NULL, + chapter_priority INTEGER NOT NULL, + download_chapters INTEGER AS Boolean NOT NULL, + merge_id INTEGER NOT NULL, + merge_url TEXT NOT NULL, + manga_id INTEGER, + manga_url TEXT NOT NULL, + manga_source INTEGER NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE SET NULL, + FOREIGN KEY(merge_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +CREATE INDEX merged_merge_id_index ON merged(merge_id); \ No newline at end of file diff --git a/app/src/main/sqldelight/data/saved_search.sq b/app/src/main/sqldelight/data/saved_search.sq new file mode 100644 index 000000000..e6a376d49 --- /dev/null +++ b/app/src/main/sqldelight/data/saved_search.sq @@ -0,0 +1,7 @@ +CREATE TABLE saved_search( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + name TEXT NOT NULL, + query TEXT, + filters_json TEXT +); \ No newline at end of file diff --git a/app/src/main/sqldelight/data/search_metadata.sq b/app/src/main/sqldelight/data/search_metadata.sq new file mode 100644 index 000000000..104266769 --- /dev/null +++ b/app/src/main/sqldelight/data/search_metadata.sq @@ -0,0 +1,12 @@ +CREATE TABLE search_metadata ( + manga_id INTEGER NOT NULL PRIMARY KEY, + uploader TEXT, + extra TEXT NOT NULL, + indexed_extra TEXT, + extra_version INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +CREATE INDEX search_metadata_uploader_index ON search_metadata(uploader); +CREATE INDEX search_metadata_indexed_extra_index ON search_metadata(indexed_extra); \ No newline at end of file diff --git a/app/src/main/sqldelight/data/search_tags.sq b/app/src/main/sqldelight/data/search_tags.sq new file mode 100644 index 000000000..9aff4777d --- /dev/null +++ b/app/src/main/sqldelight/data/search_tags.sq @@ -0,0 +1,12 @@ +CREATE TABLE search_tags ( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + namespace TEXT, + name TEXT NOT NULL, + type INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +CREATE INDEX search_tags_manga_id_index ON search_tags(manga_id); +CREATE INDEX search_tags_namespace_name_index ON search_tags(namespace, name); \ No newline at end of file diff --git a/app/src/main/sqldelight/data/search_titles.sq b/app/src/main/sqldelight/data/search_titles.sq new file mode 100644 index 000000000..1decb5bfe --- /dev/null +++ b/app/src/main/sqldelight/data/search_titles.sq @@ -0,0 +1,11 @@ +CREATE TABLE search_titles ( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + title TEXT NOT NULL, + type INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +CREATE INDEX search_titles_manga_id_index ON search_titles(manga_id); +CREATE INDEX search_titles_title_index ON search_titles(title); \ 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..6eb647300 --- /dev/null +++ b/app/src/main/sqldelight/migrations/1.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/migrations/10.sqm b/app/src/main/sqldelight/migrations/10.sqm new file mode 100644 index 000000000..d0cd2b495 --- /dev/null +++ b/app/src/main/sqldelight/migrations/10.sqm @@ -0,0 +1,7 @@ +CREATE TABLE eh_favorites ( + _id INTEGER NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + gid TEXT NOT NULL, + token TEXT NOT NULL, + category INTEGER NOT NULL +); \ 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..b1fb3d5cb --- /dev/null +++ b/app/src/main/sqldelight/migrations/11.sqm @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS eh_favorites ( + _id INTEGER NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + gid TEXT NOT NULL, + token TEXT NOT NULL, + category INTEGER NOT NULL +); \ 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..28261b1d0 --- /dev/null +++ b/app/src/main/sqldelight/migrations/12.sqm @@ -0,0 +1,16 @@ +CREATE TABLE saved_search( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + name TEXT NOT NULL, + query TEXT, + filters_json TEXT +); +CREATE TABLE feed_saved_search ( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + saved_search INTEGER, + global INTEGER AS Boolean NOT NULL, + FOREIGN KEY(saved_search) REFERENCES saved_search (_id) + ON DELETE CASCADE +); +CREATE INDEX feed_saved_search_saved_search_index ON feed_saved_search(saved_search); \ 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..4aef6884e --- /dev/null +++ b/app/src/main/sqldelight/migrations/13.sqm @@ -0,0 +1,275 @@ +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; + +DROP INDEX IF EXISTS search_metadata_uploader_index; +DROP INDEX IF EXISTS search_metadata_indexed_extra_index; +DROP INDEX IF EXISTS search_tags_manga_id_index; +DROP INDEX IF EXISTS search_tags_namespace_name_index; +DROP INDEX IF EXISTS search_titles_manga_id_index; +DROP INDEX IF EXISTS search_titles_title_index; +DROP INDEX IF EXISTS merged_merge_id_index; +DROP INDEX IF EXISTS feed_saved_search_saved_search_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, + filtered_scanlators TEXT +); +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,filtered_scanlators +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, + manga_order TEXT NOT NULL +); +INSERT INTO categories +SELECT _id,name,sort,flags,manga_order +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; + +ALTER TABLE eh_favorites RENAME TO eh_favorites_temp; +CREATE TABLE eh_favorites ( + _id INTEGER NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + gid TEXT NOT NULL, + token TEXT NOT NULL, + category INTEGER NOT NULL +); +INSERT INTO eh_favorites +SELECT _id,title,gid,token,category +FROM eh_favorites_temp; + +ALTER TABLE saved_search RENAME TO saved_search_temp; +CREATE TABLE saved_search( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + name TEXT NOT NULL, + query TEXT, + filters_json TEXT +); +INSERT INTO saved_search +SELECT _id,source,name,query,filters_json +FROM saved_search_temp; + +ALTER TABLE feed_saved_search RENAME TO feed_saved_search_temp; +CREATE TABLE feed_saved_search ( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + saved_search INTEGER, + global INTEGER AS Boolean NOT NULL, + FOREIGN KEY(saved_search) REFERENCES saved_search (_id) + ON DELETE CASCADE +); +INSERT INTO feed_saved_search +SELECT _id, source, saved_search, global +FROM feed_saved_search_temp; + +ALTER TABLE search_metadata RENAME TO search_metadata_temp; +CREATE TABLE search_metadata ( + manga_id INTEGER NOT NULL PRIMARY KEY, + uploader TEXT, + extra TEXT NOT NULL, + indexed_extra TEXT, + extra_version INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +INSERT INTO search_metadata +SELECT manga_id, uploader, extra, indexed_extra, extra_version +FROM search_metadata_temp; + +ALTER TABLE search_tags RENAME TO search_tags_temp; +CREATE TABLE search_tags ( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + namespace TEXT, + name TEXT NOT NULL, + type INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +INSERT INTO search_tags +SELECT _id, manga_id, namespace, name, type +FROM search_tags_temp; + +ALTER TABLE search_titles RENAME TO search_titles_temp; +CREATE TABLE search_titles ( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + title TEXT NOT NULL, + type INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +INSERT INTO search_titles +SELECT _id, manga_id, title, type +FROM search_titles_temp; + +ALTER TABLE merged RENAME TO merged_temp; +CREATE TABLE merged( + _id INTEGER NOT NULL PRIMARY KEY, + info_manga INTEGER AS Boolean NOT NULL, + get_chapter_updates INTEGER AS Boolean NOT NULL, + chapter_sort_mode INTEGER NOT NULL, + chapter_priority INTEGER NOT NULL, + download_chapters INTEGER AS Boolean NOT NULL, + merge_id INTEGER NOT NULL, + merge_url TEXT NOT NULL, + manga_id INTEGER, + manga_url TEXT NOT NULL, + manga_source INTEGER NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE SET NULL, + FOREIGN KEY(merge_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +INSERT INTO merged +SELECT _id,info_manga,get_chapter_updates,chapter_sort_mode,chapter_priority,download_chapters,merge_id,merge_url,manga_id,manga_url,manga_source +FROM merged_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 INDEX search_metadata_uploader_index ON search_metadata(uploader); +CREATE INDEX search_metadata_indexed_extra_index ON search_metadata(indexed_extra); +CREATE INDEX search_tags_manga_id_index ON search_tags(manga_id); +CREATE INDEX search_tags_namespace_name_index ON search_tags(namespace, name); +CREATE INDEX search_titles_manga_id_index ON search_titles(manga_id); +CREATE INDEX search_titles_title_index ON search_titles(title); +CREATE INDEX merged_merge_id_index ON merged(merge_id); +CREATE INDEX feed_saved_search_saved_search_index ON feed_saved_search(saved_search); + +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 eh_favorites_temp; +DROP TABLE IF EXISTS saved_search_temp; +DROP TABLE IF EXISTS feed_saved_search_temp; +DROP TABLE IF EXISTS search_metadata_temp; +DROP TABLE IF EXISTS search_tags_temp; +DROP TABLE IF EXISTS search_titles_temp; +DROP TABLE IF EXISTS merged_temp; +DROP TABLE IF EXISTS manga_temp; diff --git a/app/src/main/sqldelight/migrations/2.sqm b/app/src/main/sqldelight/migrations/2.sqm new file mode 100644 index 000000000..20a2c8444 --- /dev/null +++ b/app/src/main/sqldelight/migrations/2.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/3.sqm b/app/src/main/sqldelight/migrations/3.sqm new file mode 100644 index 000000000..553c44029 --- /dev/null +++ b/app/src/main/sqldelight/migrations/3.sqm @@ -0,0 +1,19 @@ +DROP TABLE merged; +CREATE TABLE merged( + _id INTEGER NOT NULL PRIMARY KEY, + info_manga INTEGER AS Boolean NOT NULL, + get_chapter_updates INTEGER AS Boolean NOT NULL, + chapter_sort_mode INTEGER NOT NULL, + chapter_priority INTEGER NOT NULL, + download_chapters INTEGER AS Boolean NOT NULL, + merge_id INTEGER NOT NULL, + merge_url TEXT NOT NULL, + manga_id INTEGER, + manga_url TEXT NOT NULL, + COL_MANGA_SOURCE INTEGER NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE SET NULL, + FOREIGN KEY(merge_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +CREATE INDEX merged_merge_id_index ON merged(merge_id); \ 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..e69de29bb diff --git a/app/src/main/sqldelight/migrations/5.sqm b/app/src/main/sqldelight/migrations/5.sqm new file mode 100644 index 000000000..4a70d4170 --- /dev/null +++ b/app/src/main/sqldelight/migrations/5.sqm @@ -0,0 +1,2 @@ +ALTER TABLE mangas +ADD COLUMN filtered_scanlators TEXT; \ 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..7718ccb35 --- /dev/null +++ b/app/src/main/sqldelight/migrations/6.sqm @@ -0,0 +1 @@ +DROP TABLE IF EXISTS manga_related; \ 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..23b429acd --- /dev/null +++ b/app/src/main/sqldelight/migrations/7.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/8.sqm b/app/src/main/sqldelight/migrations/8.sqm new file mode 100644 index 000000000..c80623439 --- /dev/null +++ b/app/src/main/sqldelight/migrations/8.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/9.sqm b/app/src/main/sqldelight/migrations/9.sqm new file mode 100644 index 000000000..78e1ece21 --- /dev/null +++ b/app/src/main/sqldelight/migrations/9.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/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 8d647913d..10a3a5a37 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") classpath(sylibs.firebase.crashlytics.gradle) } } 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"]