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