diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 2b80dfdc7..ed2214086 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -7,6 +7,7 @@ plugins { kotlin("plugin.parcelize") kotlin("plugin.serialization") id("com.github.zellius.shortcut-helper") + id("com.squareup.sqldelight") } if (gradle.startParameter.taskRequests.toString().contains("Standard")) { @@ -92,6 +93,7 @@ android { buildFeatures { viewBinding = true + compose = true // Disable some unused things aidl = false @@ -105,6 +107,10 @@ android { checkReleaseBuilds = false } + composeOptions { + kotlinCompilerExtensionVersion = compose.versions.compose.get() + } + compileOptions { sourceCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8 @@ -116,6 +122,19 @@ android { } dependencies { + implementation(compose.foundation) + implementation(compose.material3.core) + implementation(compose.material3.adapter) + implementation(compose.animation) + implementation(compose.ui.tooling) + + implementation(androidx.paging.runtime) + implementation(androidx.paging.compose) + + implementation(libs.sqldelight.android.driver) + implementation(libs.sqldelight.coroutines) + implementation(libs.sqldelight.android.paging) + implementation(kotlinx.reflect) implementation(kotlinx.bundles.coroutines) @@ -269,6 +288,9 @@ tasks { "-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi", "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi", "-Xopt-in=coil.annotation.ExperimentalCoilApi", + "-Xopt-in=androidx.compose.material3.ExperimentalMaterial3Api", + "-Xopt-in=androidx.compose.ui.ExperimentalComposeUiApi", + "-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi", "-Xopt-in=kotlin.time.ExperimentalTime", ) } diff --git a/app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt b/app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt new file mode 100644 index 000000000..bd4d99fde --- /dev/null +++ b/app/src/main/java/eu/kanade/data/AndroidDatabaseHandler.kt @@ -0,0 +1,94 @@ +package eu.kanade.data + +import androidx.paging.PagingSource +import com.squareup.sqldelight.Query +import com.squareup.sqldelight.Transacter +import com.squareup.sqldelight.android.paging3.QueryPagingSource +import com.squareup.sqldelight.db.SqlDriver +import com.squareup.sqldelight.runtime.coroutines.asFlow +import com.squareup.sqldelight.runtime.coroutines.mapToList +import com.squareup.sqldelight.runtime.coroutines.mapToOne +import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull +import eu.kanade.tachiyomi.Database +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.withContext + +class AndroidDatabaseHandler( + val db: Database, + private val driver: SqlDriver, + val queryDispatcher: CoroutineDispatcher = Dispatchers.IO, + val transactionDispatcher: CoroutineDispatcher = queryDispatcher +) : DatabaseHandler { + + val suspendingTransactionId = ThreadLocal() + + override suspend fun await(inTransaction: Boolean, block: suspend Database.() -> T): T { + return dispatch(inTransaction, block) + } + + override suspend fun awaitList( + inTransaction: Boolean, + block: suspend Database.() -> Query + ): List { + return dispatch(inTransaction) { block(db).executeAsList() } + } + + override suspend fun awaitOne( + inTransaction: Boolean, + block: suspend Database.() -> Query + ): T { + return dispatch(inTransaction) { block(db).executeAsOne() } + } + + override suspend fun awaitOneOrNull( + inTransaction: Boolean, + block: suspend Database.() -> Query + ): T? { + return dispatch(inTransaction) { block(db).executeAsOneOrNull() } + } + + override fun subscribeToList(block: Database.() -> Query): Flow> { + return block(db).asFlow().mapToList(queryDispatcher) + } + + override fun subscribeToOne(block: Database.() -> Query): Flow { + return block(db).asFlow().mapToOne(queryDispatcher) + } + + override fun subscribeToOneOrNull(block: Database.() -> Query): Flow { + return block(db).asFlow().mapToOneOrNull(queryDispatcher) + } + + override fun subscribeToPagingSource( + countQuery: Database.() -> Query, + transacter: Database.() -> Transacter, + queryProvider: Database.(Long, Long) -> Query + ): PagingSource { + return QueryPagingSource( + countQuery = countQuery(db), + transacter = transacter(db), + dispatcher = queryDispatcher, + queryProvider = { limit, offset -> + queryProvider.invoke(db, limit, offset) + } + ) + } + + private suspend fun dispatch(inTransaction: Boolean, block: suspend Database.() -> T): T { + // Create a transaction if needed and run the calling block inside it. + if (inTransaction) { + return withTransaction { block(db) } + } + + // If we're currently in the transaction thread, there's no need to dispatch our query. + if (driver.currentTransaction() != null) { + return block(db) + } + + // Get the current database context and run the calling block. + val context = getCurrentDatabaseContext() + return withContext(context) { block(db) } + } +} diff --git a/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt b/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt new file mode 100644 index 000000000..d51e2c514 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/DatabaseAdapter.kt @@ -0,0 +1,20 @@ +package eu.kanade.data + +import com.squareup.sqldelight.ColumnAdapter +import java.util.Date + +val dateAdapter = object : ColumnAdapter { + override fun decode(databaseValue: Long): Date = Date(databaseValue) + override fun encode(value: Date): Long = value.time +} + +private const val listOfStringsSeparator = ", " +val listOfStringsAdapter = object : ColumnAdapter, String> { + override fun decode(databaseValue: String) = + if (databaseValue.isEmpty()) { + listOf() + } else { + databaseValue.split(listOfStringsSeparator) + } + override fun encode(value: List) = value.joinToString(separator = listOfStringsSeparator) +} diff --git a/app/src/main/java/eu/kanade/data/DatabaseHandler.kt b/app/src/main/java/eu/kanade/data/DatabaseHandler.kt new file mode 100644 index 000000000..a528b7010 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/DatabaseHandler.kt @@ -0,0 +1,39 @@ +package eu.kanade.data + +import androidx.paging.PagingSource +import com.squareup.sqldelight.Query +import com.squareup.sqldelight.Transacter +import eu.kanade.tachiyomi.Database +import kotlinx.coroutines.flow.Flow + +interface DatabaseHandler { + + suspend fun await(inTransaction: Boolean = false, block: suspend Database.() -> T): T + + suspend fun awaitList( + inTransaction: Boolean = false, + block: suspend Database.() -> Query + ): List + + suspend fun awaitOne( + inTransaction: Boolean = false, + block: suspend Database.() -> Query + ): T + + suspend fun awaitOneOrNull( + inTransaction: Boolean = false, + block: suspend Database.() -> Query + ): T? + + fun subscribeToList(block: Database.() -> Query): Flow> + + fun subscribeToOne(block: Database.() -> Query): Flow + + fun subscribeToOneOrNull(block: Database.() -> Query): Flow + + fun subscribeToPagingSource( + countQuery: Database.() -> Query, + transacter: Database.() -> Transacter, + queryProvider: Database.(Long, Long) -> Query + ): PagingSource +} diff --git a/app/src/main/java/eu/kanade/data/TransactionContext.kt b/app/src/main/java/eu/kanade/data/TransactionContext.kt new file mode 100644 index 000000000..156b4cdba --- /dev/null +++ b/app/src/main/java/eu/kanade/data/TransactionContext.kt @@ -0,0 +1,160 @@ +package eu.kanade.data + +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.Job +import kotlinx.coroutines.asContextElement +import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlinx.coroutines.withContext +import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.atomic.AtomicInteger +import kotlin.coroutines.ContinuationInterceptor +import kotlin.coroutines.CoroutineContext +import kotlin.coroutines.EmptyCoroutineContext +import kotlin.coroutines.coroutineContext +import kotlin.coroutines.resume + +/** + * Returns the transaction dispatcher if we are on a transaction, or the database dispatchers. + */ +internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): CoroutineContext { + return coroutineContext[TransactionElement]?.transactionDispatcher ?: queryDispatcher +} + +/** + * Calls the specified suspending [block] in a database transaction. The transaction will be + * marked as successful unless an exception is thrown in the suspending [block] or the coroutine + * is cancelled. + * + * SQLDelight will only perform at most one transaction at a time, additional transactions are queued + * and executed on a first come, first serve order. + * + * Performing blocking database operations is not permitted in a coroutine scope other than the + * one received by the suspending block. It is recommended that all [Dao] function invoked within + * the [block] be suspending functions. + * + * The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor. + */ +internal suspend fun AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T { + // Use inherited transaction context if available, this allows nested suspending transactions. + val transactionContext = + coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext() + return withContext(transactionContext) { + val transactionElement = coroutineContext[TransactionElement]!! + transactionElement.acquire() + try { + db.transactionWithResult { + runBlocking(transactionContext) { + block() + } + } + } finally { + transactionElement.release() + } + } +} + +/** + * Creates a [CoroutineContext] for performing database operations within a coroutine transaction. + * + * The context is a combination of a dispatcher, a [TransactionElement] and a thread local element. + * + * * The dispatcher will dispatch coroutines to a single thread that is taken over from the SQLDelight + * query executor. If the coroutine context is switched, suspending DAO functions will be able to + * dispatch to the transaction thread. + * + * * The [TransactionElement] serves as an indicator for inherited context, meaning, if there is a + * switch of context, suspending DAO methods will be able to use the indicator to dispatch the + * database operation to the transaction thread. + * + * * The thread local element serves as a second indicator and marks threads that are used to + * execute coroutines within the coroutine transaction, more specifically it allows us to identify + * if a blocking DAO method is invoked within the transaction coroutine. Never assign meaning to + * this value, for now all we care is if its present or not. + */ +private suspend fun AndroidDatabaseHandler.createTransactionContext(): CoroutineContext { + val controlJob = Job() + // make sure to tie the control job to this context to avoid blocking the transaction if + // context get cancelled before we can even start using this job. Otherwise, the acquired + // transaction thread will forever wait for the controlJob to be cancelled. + // see b/148181325 + coroutineContext[Job]?.invokeOnCompletion { + controlJob.cancel() + } + + val dispatcher = transactionDispatcher.acquireTransactionThread(controlJob) + val transactionElement = TransactionElement(controlJob, dispatcher) + val threadLocalElement = + suspendingTransactionId.asContextElement(System.identityHashCode(controlJob)) + return dispatcher + transactionElement + threadLocalElement +} + +/** + * Acquires a thread from the executor and returns a [ContinuationInterceptor] to dispatch + * coroutines to the acquired thread. The [controlJob] is used to control the release of the + * thread by cancelling the job. + */ +private suspend fun CoroutineDispatcher.acquireTransactionThread( + controlJob: Job +): ContinuationInterceptor { + return suspendCancellableCoroutine { continuation -> + continuation.invokeOnCancellation { + // We got cancelled while waiting to acquire a thread, we can't stop our attempt to + // acquire a thread, but we can cancel the controlling job so once it gets acquired it + // is quickly released. + controlJob.cancel() + } + try { + dispatch(EmptyCoroutineContext) { + runBlocking { + // Thread acquired, resume coroutine. + continuation.resume(coroutineContext[ContinuationInterceptor]!!) + controlJob.join() + } + } + } catch (ex: RejectedExecutionException) { + // Couldn't acquire a thread, cancel coroutine. + continuation.cancel( + IllegalStateException( + "Unable to acquire a thread to perform the database transaction.", ex + ) + ) + } + } +} + +/** + * A [CoroutineContext.Element] that indicates there is an on-going database transaction. + */ +private class TransactionElement( + private val transactionThreadControlJob: Job, + val transactionDispatcher: ContinuationInterceptor +) : CoroutineContext.Element { + + companion object Key : CoroutineContext.Key + + override val key: CoroutineContext.Key + get() = TransactionElement + + /** + * Number of transactions (including nested ones) started with this element. + * Call [acquire] to increase the count and [release] to decrease it. If the count reaches zero + * when [release] is invoked then the transaction job is cancelled and the transaction thread + * is released. + */ + private val referenceCount = AtomicInteger(0) + + fun acquire() { + referenceCount.incrementAndGet() + } + + fun release() { + val count = referenceCount.decrementAndGet() + if (count < 0) { + throw IllegalStateException("Transaction was never started or was already released.") + } else if (count == 0) { + // Cancel the job that controls the transaction thread, causing it to be released. + transactionThreadControlJob.cancel() + } + } +} diff --git a/app/src/main/java/eu/kanade/data/chapter/ChapterMapper.kt b/app/src/main/java/eu/kanade/data/chapter/ChapterMapper.kt new file mode 100644 index 000000000..05de96ef2 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/chapter/ChapterMapper.kt @@ -0,0 +1,21 @@ +package eu.kanade.data.chapter + +import eu.kanade.domain.chapter.model.Chapter + +val chapterMapper: (Long, Long, String, String, String?, Boolean, Boolean, Long, Float, Long, Long, Long) -> Chapter = + { id, mangaId, url, name, scanlator, read, bookmark, lastPageRead, chapterNumber, sourceOrder, dateFetch, dateUpload -> + Chapter( + id = id, + mangaId = mangaId, + read = read, + bookmark = bookmark, + lastPageRead = lastPageRead, + dateFetch = dateFetch, + sourceOrder = sourceOrder, + url = url, + name = name, + dateUpload = dateUpload, + chapterNumber = chapterNumber, + scanlator = scanlator, + ) + } diff --git a/app/src/main/java/eu/kanade/data/exh/FeedSavedSearch.kt b/app/src/main/java/eu/kanade/data/exh/FeedSavedSearch.kt new file mode 100644 index 000000000..430699d07 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/exh/FeedSavedSearch.kt @@ -0,0 +1,13 @@ +package eu.kanade.data.exh + +import exh.savedsearches.models.FeedSavedSearch + +val feedSavedSearchMapper: (Long, Long, Long?, Boolean) -> FeedSavedSearch = + { id, source, savedSearch, global -> + FeedSavedSearch( + id = id, + source = source, + savedSearch = savedSearch, + global = global + ) + } diff --git a/app/src/main/java/eu/kanade/data/exh/SavedSearch.kt b/app/src/main/java/eu/kanade/data/exh/SavedSearch.kt new file mode 100644 index 000000000..a31d4c787 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/exh/SavedSearch.kt @@ -0,0 +1,14 @@ +package eu.kanade.data.exh + +import exh.savedsearches.models.SavedSearch + +val savedSearchMapper: (Long, Long, String, String?, String?) -> SavedSearch = + { id, source, name, query, filtersJson -> + SavedSearch( + id = id, + source = source, + name = name, + query = query, + filtersJson = filtersJson + ) + } diff --git a/app/src/main/java/eu/kanade/data/history/HistoryMapper.kt b/app/src/main/java/eu/kanade/data/history/HistoryMapper.kt new file mode 100644 index 000000000..e7ebf5cb4 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/history/HistoryMapper.kt @@ -0,0 +1,26 @@ +package eu.kanade.data.history + +import eu.kanade.domain.history.model.History +import eu.kanade.domain.history.model.HistoryWithRelations +import java.util.Date + +val historyMapper: (Long, Long, Date?, Date?) -> History = { id, chapterId, readAt, _ -> + History( + id = id, + chapterId = chapterId, + readAt = readAt, + ) +} + +val historyWithRelationsMapper: (Long, Long, Long, String, String?, Float, Date?) -> HistoryWithRelations = { + historyId, mangaId, chapterId, title, thumbnailUrl, chapterNumber, readAt -> + HistoryWithRelations( + id = historyId, + chapterId = chapterId, + mangaId = mangaId, + title = title, + thumbnailUrl = thumbnailUrl ?: "", + chapterNumber = chapterNumber, + readAt = readAt + ) +} diff --git a/app/src/main/java/eu/kanade/data/history/HistoryRepositoryImpl.kt b/app/src/main/java/eu/kanade/data/history/HistoryRepositoryImpl.kt new file mode 100644 index 000000000..15d2d2633 --- /dev/null +++ b/app/src/main/java/eu/kanade/data/history/HistoryRepositoryImpl.kt @@ -0,0 +1,91 @@ +package eu.kanade.data.history + +import androidx.paging.PagingSource +import eu.kanade.data.DatabaseHandler +import eu.kanade.data.chapter.chapterMapper +import eu.kanade.data.manga.mangaMapper +import eu.kanade.domain.chapter.model.Chapter +import eu.kanade.domain.history.model.HistoryWithRelations +import eu.kanade.domain.history.repository.HistoryRepository +import eu.kanade.domain.manga.model.Manga +import eu.kanade.tachiyomi.util.system.logcat + +class HistoryRepositoryImpl( + private val handler: DatabaseHandler +) : HistoryRepository { + + override fun getHistory(query: String): PagingSource { + return handler.subscribeToPagingSource( + countQuery = { historyViewQueries.countHistory(query) }, + transacter = { historyViewQueries }, + queryProvider = { limit, offset -> + historyViewQueries.history(query, limit, offset, historyWithRelationsMapper) + } + ) + } + + override suspend fun getNextChapterForManga(mangaId: Long, chapterId: Long): Chapter? { + val chapter = handler.awaitOne { chaptersQueries.getChapterById(chapterId, chapterMapper) } + val manga = handler.awaitOne { mangasQueries.getMangaById(mangaId, mangaMapper) } + + if (!chapter.read) { + return chapter + } + + val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) { + Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.sourceOrder.compareTo(c1.sourceOrder) } + Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapterNumber.compareTo(c2.chapterNumber) } + Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.dateUpload.compareTo(c2.dateUpload) } + else -> throw NotImplementedError("Unknown sorting method") + } + + val chapters = handler.awaitList { chaptersQueries.getChapterByMangaId(mangaId, chapterMapper) } + .sortedWith(sortFunction) + + val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id } + return when (manga.sorting) { + Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1) + Manga.CHAPTER_SORTING_NUMBER -> { + val chapterNumber = chapter.chapterNumber + + ((currChapterIndex + 1) until chapters.size) + .map { chapters[it] } + .firstOrNull { + it.chapterNumber > chapterNumber && + it.chapterNumber <= chapterNumber + 1 + } + } + Manga.CHAPTER_SORTING_UPLOAD_DATE -> { + chapters.drop(currChapterIndex + 1) + .firstOrNull { it.dateUpload >= chapter.dateUpload } + } + else -> throw NotImplementedError("Unknown sorting method") + } + } + + override suspend fun resetHistory(historyId: Long) { + try { + handler.await { historyQueries.resetHistoryById(historyId) } + } catch (e: Exception) { + logcat(throwable = e) + } + } + + override suspend fun resetHistoryByMangaId(mangaId: Long) { + try { + handler.await { historyQueries.resetHistoryByMangaId(mangaId) } + } catch (e: Exception) { + logcat(throwable = e) + } + } + + override suspend fun deleteAllHistory(): Boolean { + return try { + handler.await { historyQueries.removeAllHistory() } + true + } catch (e: Exception) { + logcat(throwable = e) + false + } + } +} diff --git a/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt b/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt new file mode 100644 index 000000000..d60316f3b --- /dev/null +++ b/app/src/main/java/eu/kanade/data/manga/MangaMapper.kt @@ -0,0 +1,27 @@ +package eu.kanade.data.manga + +import eu.kanade.domain.manga.model.Manga + +val mangaMapper: (Long, Long, String, String?, String?, String?, List?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, String?) -> Manga = + { id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewer, chapterFlags, coverLastModified, dateAdded, filteredScanlators -> + Manga( + id = id, + source = source, + favorite = favorite, + lastUpdate = lastUpdate ?: 0, + dateAdded = dateAdded, + viewerFlags = viewer, + chapterFlags = chapterFlags, + coverLastModified = coverLastModified, + url = url, + title = title, + artist = artist, + author = author, + description = description, + genre = genre, + status = status, + thumbnailUrl = thumbnailUrl, + initialized = initialized, + filteredScanlators = filteredScanlators + ) + } diff --git a/app/src/main/java/eu/kanade/domain/DomainModule.kt b/app/src/main/java/eu/kanade/domain/DomainModule.kt new file mode 100644 index 000000000..9462ae7f9 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/DomainModule.kt @@ -0,0 +1,26 @@ +package eu.kanade.domain + +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 +import eu.kanade.domain.history.interactor.RemoveHistoryById +import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId +import eu.kanade.domain.history.repository.HistoryRepository +import uy.kohesive.injekt.api.InjektModule +import uy.kohesive.injekt.api.InjektRegistrar +import uy.kohesive.injekt.api.addFactory +import uy.kohesive.injekt.api.addSingletonFactory +import uy.kohesive.injekt.api.get + +class DomainModule : InjektModule { + + override fun InjektRegistrar.registerInjectables() { + addSingletonFactory { HistoryRepositoryImpl(get()) } + addFactory { DeleteHistoryTable(get()) } + addFactory { GetHistory(get()) } + addFactory { GetNextChapterForManga(get()) } + addFactory { RemoveHistoryById(get()) } + addFactory { RemoveHistoryByMangaId(get()) } + } +} diff --git a/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt new file mode 100644 index 000000000..6eff7c580 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/chapter/model/Chapter.kt @@ -0,0 +1,16 @@ +package eu.kanade.domain.chapter.model + +data class Chapter( + val id: Long, + val mangaId: Long, + val read: Boolean, + val bookmark: Boolean, + val lastPageRead: Long, + val dateFetch: Long, + val sourceOrder: Long, + val url: String, + val name: String, + val dateUpload: Long, + val chapterNumber: Float, + val scanlator: String? +) diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/DeleteHistoryTable.kt b/app/src/main/java/eu/kanade/domain/history/interactor/DeleteHistoryTable.kt new file mode 100644 index 000000000..bebf1209d --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/interactor/DeleteHistoryTable.kt @@ -0,0 +1,12 @@ +package eu.kanade.domain.history.interactor + +import eu.kanade.domain.history.repository.HistoryRepository + +class DeleteHistoryTable( + private val repository: HistoryRepository +) { + + suspend fun await(): Boolean { + return repository.deleteAllHistory() + } +} diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/GetHistory.kt b/app/src/main/java/eu/kanade/domain/history/interactor/GetHistory.kt new file mode 100644 index 000000000..d2f8302b7 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/interactor/GetHistory.kt @@ -0,0 +1,21 @@ +package eu.kanade.domain.history.interactor + +import androidx.paging.Pager +import androidx.paging.PagingConfig +import androidx.paging.PagingData +import eu.kanade.domain.history.model.HistoryWithRelations +import eu.kanade.domain.history.repository.HistoryRepository +import kotlinx.coroutines.flow.Flow + +class GetHistory( + private val repository: HistoryRepository +) { + + fun subscribe(query: String): Flow> { + return Pager( + PagingConfig(pageSize = 25) + ) { + repository.getHistory(query) + }.flow + } +} diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/GetNextChapterForManga.kt b/app/src/main/java/eu/kanade/domain/history/interactor/GetNextChapterForManga.kt new file mode 100644 index 000000000..477408ca3 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/interactor/GetNextChapterForManga.kt @@ -0,0 +1,13 @@ +package eu.kanade.domain.history.interactor + +import eu.kanade.domain.chapter.model.Chapter +import eu.kanade.domain.history.repository.HistoryRepository + +class GetNextChapterForManga( + private val repository: HistoryRepository +) { + + suspend fun await(mangaId: Long, chapterId: Long): Chapter? { + return repository.getNextChapterForManga(mangaId, chapterId) + } +} diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt new file mode 100644 index 000000000..93012c266 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryById.kt @@ -0,0 +1,13 @@ +package eu.kanade.domain.history.interactor + +import eu.kanade.domain.history.model.HistoryWithRelations +import eu.kanade.domain.history.repository.HistoryRepository + +class RemoveHistoryById( + private val repository: HistoryRepository +) { + + suspend fun await(history: HistoryWithRelations) { + repository.resetHistory(history.id) + } +} diff --git a/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryByMangaId.kt b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryByMangaId.kt new file mode 100644 index 000000000..f32fa5f7b --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/interactor/RemoveHistoryByMangaId.kt @@ -0,0 +1,12 @@ +package eu.kanade.domain.history.interactor + +import eu.kanade.domain.history.repository.HistoryRepository + +class RemoveHistoryByMangaId( + private val repository: HistoryRepository +) { + + suspend fun await(mangaId: Long) { + repository.resetHistoryByMangaId(mangaId) + } +} diff --git a/app/src/main/java/eu/kanade/domain/history/model/History.kt b/app/src/main/java/eu/kanade/domain/history/model/History.kt new file mode 100644 index 000000000..58c1c985e --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/model/History.kt @@ -0,0 +1,9 @@ +package eu.kanade.domain.history.model + +import java.util.Date + +data class History( + val id: Long?, + val chapterId: Long, + val readAt: Date? +) diff --git a/app/src/main/java/eu/kanade/domain/history/model/HistoryWithRelations.kt b/app/src/main/java/eu/kanade/domain/history/model/HistoryWithRelations.kt new file mode 100644 index 000000000..6f5a8e1fc --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/model/HistoryWithRelations.kt @@ -0,0 +1,13 @@ +package eu.kanade.domain.history.model + +import java.util.Date + +data class HistoryWithRelations( + val id: Long, + val chapterId: Long, + val mangaId: Long, + val title: String, + val thumbnailUrl: String, + val chapterNumber: Float, + val readAt: Date? +) diff --git a/app/src/main/java/eu/kanade/domain/history/repository/HistoryRepository.kt b/app/src/main/java/eu/kanade/domain/history/repository/HistoryRepository.kt new file mode 100644 index 000000000..38e0f4192 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/history/repository/HistoryRepository.kt @@ -0,0 +1,18 @@ +package eu.kanade.domain.history.repository + +import androidx.paging.PagingSource +import eu.kanade.domain.chapter.model.Chapter +import eu.kanade.domain.history.model.HistoryWithRelations + +interface HistoryRepository { + + fun getHistory(query: String): PagingSource + + suspend fun getNextChapterForManga(mangaId: Long, chapterId: Long): Chapter? + + suspend fun resetHistory(historyId: Long) + + suspend fun resetHistoryByMangaId(mangaId: Long) + + suspend fun deleteAllHistory(): Boolean +} diff --git a/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt new file mode 100644 index 000000000..7511a40a7 --- /dev/null +++ b/app/src/main/java/eu/kanade/domain/manga/model/Manga.kt @@ -0,0 +1,39 @@ +package eu.kanade.domain.manga.model + +data class Manga( + val id: Long, + val source: Long, + val favorite: Boolean, + val lastUpdate: Long, + val dateAdded: Long, + val viewerFlags: Long, + val chapterFlags: Long, + val coverLastModified: Long, + val url: String, + val title: String, + val artist: String?, + val author: String?, + val description: String?, + val genre: List?, + val status: Long, + val thumbnailUrl: String?, + val initialized: Boolean, + // SY --> + val filteredScanlators: String?, +// SY <-- +) { + + val sorting: Long + get() = chapterFlags and CHAPTER_SORTING_MASK + + companion object { + + // Generic filter that does not filter anything + const val SHOW_ALL = 0x00000000L + + const val CHAPTER_SORTING_SOURCE = 0x00000000L + const val CHAPTER_SORTING_NUMBER = 0x00000100L + const val CHAPTER_SORTING_UPLOAD_DATE = 0x00000200L + const val CHAPTER_SORTING_MASK = 0x00000300L + } +} diff --git a/app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt b/app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt new file mode 100644 index 000000000..e94bef827 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/EmptyScreen.kt @@ -0,0 +1,49 @@ +package eu.kanade.presentation.components + +import android.view.ViewGroup +import androidx.annotation.StringRes +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.viewinterop.AndroidView +import eu.kanade.tachiyomi.widget.EmptyView + +@Composable +fun EmptyScreen( + @StringRes textResource: Int, + actions: List? = null, +) { + EmptyScreen( + message = stringResource(id = textResource), + actions = actions, + ) +} + +@Composable +fun EmptyScreen( + message: String, + actions: List? = null, +) { + Box( + modifier = Modifier + .fillMaxSize() + ) { + AndroidView( + factory = { context -> + EmptyView(context).apply { + layoutParams = ViewGroup.LayoutParams( + ViewGroup.LayoutParams.WRAP_CONTENT, + ViewGroup.LayoutParams.WRAP_CONTENT, + ) + } + }, + modifier = Modifier + .align(Alignment.Center), + ) { view -> + view.show(message, actions) + } + } +} diff --git a/app/src/main/java/eu/kanade/presentation/components/MangaCover.kt b/app/src/main/java/eu/kanade/presentation/components/MangaCover.kt new file mode 100644 index 000000000..33c8dfaf6 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/components/MangaCover.kt @@ -0,0 +1,39 @@ +package eu.kanade.presentation.components + +import androidx.compose.foundation.layout.aspectRatio +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.draw.clip +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.Shape +import androidx.compose.ui.graphics.painter.ColorPainter +import androidx.compose.ui.layout.ContentScale +import androidx.compose.ui.unit.dp +import coil.compose.AsyncImage + +enum class MangaCoverAspect(val ratio: Float) { + SQUARE(1f / 1f), + COVER(2f / 3f) +} + +@Composable +fun MangaCover( + modifier: Modifier = Modifier, + data: String?, + aspect: MangaCoverAspect, + contentDescription: String = "", + shape: Shape = RoundedCornerShape(4.dp) +) { + AsyncImage( + model = data, + placeholder = ColorPainter(CoverPlaceholderColor), + contentDescription = contentDescription, + modifier = modifier + .aspectRatio(aspect.ratio) + .clip(shape), + contentScale = ContentScale.Crop + ) +} + +private val CoverPlaceholderColor = Color(0x1F888888) diff --git a/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt new file mode 100644 index 000000000..02bafed72 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/history/HistoryScreen.kt @@ -0,0 +1,297 @@ +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 +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.navigationBarsPadding +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.lazy.LazyColumn +import androidx.compose.foundation.lazy.rememberLazyListState +import androidx.compose.foundation.selection.toggleable +import androidx.compose.material.icons.Icons +import androidx.compose.material.icons.filled.PlayArrow +import androidx.compose.material.icons.outlined.Delete +import androidx.compose.material3.AlertDialog +import androidx.compose.material3.Checkbox +import androidx.compose.material3.CircularProgressIndicator +import androidx.compose.material3.Icon +import androidx.compose.material3.IconButton +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.runtime.collectAsState +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.input.nestedscroll.NestedScrollConnection +import androidx.compose.ui.input.nestedscroll.nestedScroll +import androidx.compose.ui.platform.ComposeView +import androidx.compose.ui.platform.LocalContext +import androidx.compose.ui.platform.rememberNestedScrollInteropConnection +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.unit.dp +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.util.horizontalPadding +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.ui.recent.history.HistoryPresenter +import eu.kanade.tachiyomi.ui.recent.history.UiModel +import eu.kanade.tachiyomi.util.lang.toRelativeString +import eu.kanade.tachiyomi.util.lang.toTimestampString +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get +import java.text.DateFormat +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.Date + +@Composable +fun HistoryScreen( + composeView: ComposeView, + presenter: HistoryPresenter, + onClickItem: (HistoryWithRelations) -> Unit, + onClickResume: (HistoryWithRelations) -> Unit, + onClickDelete: (HistoryWithRelations, Boolean) -> Unit, +) { + val nestedScrollInterop = rememberNestedScrollInteropConnection(composeView) + val state by presenter.state.collectAsState() + val history = state.list?.collectAsLazyPagingItems() + when { + history == null -> { + CircularProgressIndicator() + } + history.itemCount == 0 -> { + EmptyScreen( + textResource = R.string.information_no_recent_manga + ) + } + else -> { + HistoryContent( + nestedScroll = nestedScrollInterop, + history = history, + onClickItem = onClickItem, + onClickResume = onClickResume, + onClickDelete = onClickDelete, + ) + } + } +} + +@Composable +fun HistoryContent( + history: LazyPagingItems, + onClickItem: (HistoryWithRelations) -> Unit, + onClickResume: (HistoryWithRelations) -> Unit, + onClickDelete: (HistoryWithRelations, Boolean) -> Unit, + preferences: PreferencesHelper = Injekt.get(), + nestedScroll: NestedScrollConnection +) { + val relativeTime: Int = remember { preferences.relativeTime().get() } + val dateFormat: DateFormat = remember { preferences.dateFormat() } + + val (removeState, setRemoveState) = remember { mutableStateOf(null) } + + val scrollState = rememberLazyListState() + LazyColumn( + modifier = Modifier + .nestedScroll(nestedScroll), + state = scrollState, + ) { + items(history) { item -> + when (item) { + is UiModel.Header -> { + HistoryHeader( + modifier = Modifier + .animateItemPlacement(), + date = item.date, + relativeTime = relativeTime, + dateFormat = dateFormat + ) + } + is UiModel.Item -> { + val value = item.item + HistoryItem( + modifier = Modifier.animateItemPlacement(), + history = value, + onClickItem = { onClickItem(value) }, + onClickResume = { onClickResume(value) }, + onClickDelete = { setRemoveState(value) }, + ) + } + null -> {} + } + } + item { + Spacer(Modifier.navigationBarsPadding()) + } + } + + if (removeState != null) { + RemoveHistoryDialog( + onPositive = { all -> + onClickDelete(removeState, all) + setRemoveState(null) + }, + onNegative = { setRemoveState(null) } + ) + } +} + +@Composable +fun HistoryHeader( + modifier: Modifier = Modifier, + date: Date, + relativeTime: Int, + dateFormat: DateFormat, +) { + Text( + modifier = modifier + .padding(horizontal = horizontalPadding, vertical = 8.dp), + text = date.toRelativeString( + LocalContext.current, + relativeTime, + dateFormat + ), + style = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurfaceVariant, + fontWeight = FontWeight.SemiBold, + ) + ) +} + +@Composable +fun HistoryItem( + modifier: Modifier = Modifier, + history: HistoryWithRelations, + onClickItem: () -> Unit, + onClickResume: () -> Unit, + onClickDelete: () -> Unit, +) { + Row( + modifier = modifier + .clickable(onClick = onClickItem) + .height(96.dp) + .padding(horizontal = horizontalPadding, vertical = 8.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + MangaCover( + modifier = Modifier.fillMaxHeight(), + data = history.thumbnailUrl, + aspect = MangaCoverAspect.COVER + ) + Column( + modifier = Modifier + .weight(1f) + .padding(start = horizontalPadding, end = 8.dp), + ) { + val textStyle = MaterialTheme.typography.bodyMedium.copy( + color = MaterialTheme.colorScheme.onSurface, + ) + Text( + text = history.title, + maxLines = 2, + overflow = TextOverflow.Ellipsis, + style = textStyle.copy(fontWeight = FontWeight.SemiBold) + ) + Row { + Text( + text = if (history.chapterNumber > -1) { + stringResource( + R.string.recent_manga_time, + chapterFormatter.format(history.chapterNumber), + history.readAt?.toTimestampString() ?: "", + ) + } else { + history.readAt?.toTimestampString() ?: "" + }, + modifier = Modifier.padding(top = 4.dp), + style = textStyle + ) + } + } + IconButton(onClick = onClickDelete) { + Icon( + imageVector = Icons.Outlined.Delete, + contentDescription = stringResource(id = R.string.action_delete), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + IconButton(onClick = onClickResume) { + Icon( + imageVector = Icons.Filled.PlayArrow, + contentDescription = stringResource(id = R.string.action_resume), + tint = MaterialTheme.colorScheme.onSurface, + ) + } + } +} + +@Composable +fun RemoveHistoryDialog( + onPositive: (Boolean) -> Unit, + onNegative: () -> Unit +) { + val (removeEverything, removeEverythingState) = remember { mutableStateOf(false) } + + AlertDialog( + title = { + Text(text = stringResource(id = R.string.action_remove)) + }, + text = { + Column { + Text(text = stringResource(id = R.string.dialog_with_checkbox_remove_description)) + Row( + modifier = Modifier + .padding(top = 16.dp) + .toggleable( + interactionSource = remember { MutableInteractionSource() }, + indication = null, + value = removeEverything, + onValueChange = removeEverythingState + ), + verticalAlignment = Alignment.CenterVertically + ) { + Checkbox( + checked = removeEverything, + onCheckedChange = null, + ) + Text( + modifier = Modifier.padding(start = 4.dp), + text = stringResource(id = R.string.dialog_with_checkbox_reset) + ) + } + } + }, + onDismissRequest = onNegative, + confirmButton = { + TextButton(onClick = { onPositive(removeEverything) }) { + Text(text = stringResource(id = R.string.action_remove)) + } + }, + dismissButton = { + TextButton(onClick = onNegative) { + Text(text = stringResource(id = R.string.action_cancel)) + } + }, + ) +} + +private val chapterFormatter = DecimalFormat( + "#.###", + DecimalFormatSymbols().apply { decimalSeparator = '.' }, +) diff --git a/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt new file mode 100644 index 000000000..adb6644d2 --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/theme/TachiyomiTheme.kt @@ -0,0 +1,20 @@ +package eu.kanade.presentation.theme + +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.platform.LocalContext +import com.google.android.material.composethemeadapter3.createMdc3Theme + +@Composable +fun TachiyomiTheme(content: @Composable () -> Unit) { + val context = LocalContext.current + val (colorScheme, typography) = createMdc3Theme( + context = context + ) + + MaterialTheme( + colorScheme = colorScheme!!, + typography = typography!!, + content = content + ) +} diff --git a/app/src/main/java/eu/kanade/presentation/util/Constants.kt b/app/src/main/java/eu/kanade/presentation/util/Constants.kt new file mode 100644 index 000000000..fcf64d77b --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/util/Constants.kt @@ -0,0 +1,5 @@ +package eu.kanade.presentation.util + +import androidx.compose.ui.unit.dp + +val horizontalPadding = 16.dp diff --git a/app/src/main/java/eu/kanade/presentation/util/LazyListState.kt b/app/src/main/java/eu/kanade/presentation/util/LazyListState.kt new file mode 100644 index 000000000..adf7cd80c --- /dev/null +++ b/app/src/main/java/eu/kanade/presentation/util/LazyListState.kt @@ -0,0 +1,5 @@ +package eu.kanade.presentation.util + +import androidx.compose.foundation.lazy.LazyListState + +fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1 diff --git a/app/src/main/java/eu/kanade/tachiyomi/App.kt b/app/src/main/java/eu/kanade/tachiyomi/App.kt index 2516f0bfa..5a388c916 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/App.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/App.kt @@ -38,6 +38,7 @@ import com.google.firebase.analytics.ktx.analytics import com.google.firebase.ktx.Firebase import com.ms_square.debugoverlay.DebugOverlay import com.ms_square.debugoverlay.modules.FpsModule +import eu.kanade.domain.DomainModule import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder @@ -101,6 +102,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory { } Injekt.importModule(AppModule(this)) + Injekt.importModule(DomainModule()) setupNotificationChannels() if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt index 9bbad01a5..5fbf2749b 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/AppModule.kt @@ -2,9 +2,18 @@ package eu.kanade.tachiyomi import android.app.Application import androidx.core.content.ContextCompat +import com.squareup.sqldelight.android.AndroidSqliteDriver +import com.squareup.sqldelight.db.SqlDriver +import data.History +import data.Mangas +import eu.kanade.data.AndroidDatabaseHandler +import eu.kanade.data.DatabaseHandler +import eu.kanade.data.dateAdapter +import eu.kanade.data.listOfStringsAdapter import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.DbOpenCallback import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.preference.PreferencesHelper @@ -27,11 +36,37 @@ class AppModule(val app: Application) : InjektModule { override fun InjektRegistrar.registerInjectables() { addSingleton(app) + addSingletonFactory { DbOpenCallback() } + + addSingletonFactory { + AndroidSqliteDriver( + schema = Database.Schema, + context = app, + name = DbOpenCallback.DATABASE_NAME, + callback = get() + ) + } + + addSingletonFactory { + Database( + driver = get(), + historyAdapter = History.Adapter( + history_last_readAdapter = dateAdapter, + history_time_readAdapter = dateAdapter + ), + mangasAdapter = Mangas.Adapter( + genreAdapter = listOfStringsAdapter + ) + ) + } + + addSingletonFactory { AndroidDatabaseHandler(get(), get()) } + addSingletonFactory { Json { ignoreUnknownKeys = true } } addSingletonFactory { PreferencesHelper(app) } - addSingletonFactory { DatabaseHelper(app) } + addSingletonFactory { DatabaseHelper(app, get()) } addSingletonFactory { ChapterCache(app) } @@ -65,6 +100,8 @@ class AppModule(val app: Application) : InjektModule { get() + get() + get() get() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt index 6d03f2d42..66a12f4ca 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/AbstractBackupManager.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.backup import android.content.Context import android.net.Uri +import eu.kanade.data.DatabaseHandler import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Manga @@ -20,6 +21,7 @@ import uy.kohesive.injekt.injectLazy abstract class AbstractBackupManager(protected val context: Context) { internal val databaseHelper: DatabaseHelper by injectLazy() + internal val databaseHandler: DatabaseHandler by injectLazy() internal val sourceManager: SourceManager by injectLazy() internal val trackManager: TrackManager by injectLazy() protected val preferences: PreferencesHelper by injectLazy() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt index 05342a3b5..4820c17d9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.full import android.content.Context import android.net.Uri import com.hippo.unifile.UniFile +import eu.kanade.data.exh.savedSearchMapper import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.AbstractBackupManager import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY @@ -39,11 +40,11 @@ import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.system.logcat import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.insertFlatMetadataAsync -import exh.savedsearches.models.SavedSearch import exh.source.MERGED_SOURCE_ID import exh.source.getMainSource import exh.util.executeOnIO import exh.util.nullIfBlank +import kotlinx.coroutines.runBlocking import kotlinx.serialization.protobuf.ProtoBuf import logcat.LogPriority import okio.buffer @@ -167,13 +168,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { * @return list of [BackupSavedSearch] to be backed up */ private fun backupSavedSearches(): List { - return databaseHelper.getSavedSearches().executeAsBlocking().map { - BackupSavedSearch( - it.name, - it.query.orEmpty(), - it.filtersJson ?: "[]", - it.source, - ) + return runBlocking { + databaseHandler.awaitList { saved_searchQueries.selectAll(savedSearchMapper) }.map { + BackupSavedSearch( + it.name, + it.query.orEmpty(), + it.filtersJson ?: "[]", + it.source, + ) + } } } // SY <-- @@ -357,7 +360,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { } } } - databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking() + databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking() } /** @@ -431,25 +434,24 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) { } // SY --> - internal fun restoreSavedSearches(backupSavedSearches: List) { - val currentSavedSearches = databaseHelper.getSavedSearches() - .executeAsBlocking() + internal suspend fun restoreSavedSearches(backupSavedSearches: List) { + val currentSavedSearches = databaseHandler.awaitList { + saved_searchQueries.selectAll(savedSearchMapper) + } - val newSavedSearches = backupSavedSearches.filter { backupSavedSearch -> - currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } - }.map { - SavedSearch( - id = null, - it.source, - it.name, - it.query.nullIfBlank(), - filtersJson = it.filterList.nullIfBlank() - ?.takeUnless { it == "[]" }, - ) - }.ifEmpty { null } - - if (newSavedSearches != null) { - databaseHelper.insertSavedSearches(newSavedSearches) + databaseHandler.await(true) { + backupSavedSearches.filter { backupSavedSearch -> + currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } + }.forEach { + saved_searchQueries.insertSavedSearch( + _id = null, + source = it.source, + name = it.name, + query = it.query.nullIfBlank(), + filters_json = it.filterList.nullIfBlank() + ?.takeUnless { it == "[]" }, + ) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt index 8c7267ed5..06791f37a 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupRestore.kt @@ -76,7 +76,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa } // SY --> - private fun restoreSavedSearches(backupSavedSearches: List) { + private suspend fun restoreSavedSearches(backupSavedSearches: List) { backupManager.restoreSavedSearches(backupSavedSearches) restoreProgress += 1 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt index 8a596485e..d86844fab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt @@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.backup.legacy import android.content.Context import android.net.Uri +import eu.kanade.data.exh.savedSearchMapper import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.backup.AbstractBackupManager import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION @@ -206,7 +207,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab } } } - databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking() + databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking() } /** @@ -289,28 +290,37 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab } // SY --> - internal fun restoreSavedSearches(jsonSavedSearches: String) { + internal suspend fun restoreSavedSearches(jsonSavedSearches: String) { val backupSavedSearches = jsonSavedSearches.split("***").toSet() - val currentSavedSearches = databaseHelper.getSavedSearches().executeAsBlocking() + val currentSavedSearches = databaseHandler.awaitList { + saved_searchQueries.selectAll(savedSearchMapper) + } - val newSavedSearches = backupSavedSearches.mapNotNull { - runCatching { - val content = parser.decodeFromString(it.substringAfter(':')) - SavedSearch( - id = null, - source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null, - content["name"]!!.jsonPrimitive.content, - content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(), - Json.encodeToString(content["filters"]!!.jsonArray), + databaseHandler.await(true) { + backupSavedSearches.mapNotNull { + runCatching { + val content = parser.decodeFromString(it.substringAfter(':')) + SavedSearch( + id = null, + source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null, + content["name"]!!.jsonPrimitive.content, + content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(), + Json.encodeToString(content["filters"]!!.jsonArray), + ) + }.getOrNull() + }.filter { backupSavedSearch -> + currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } + }.forEach { + saved_searchQueries.insertSavedSearch( + _id = null, + source = it.source, + name = it.name, + query = it.query.nullIfBlank(), + filters_json = it.filtersJson.nullIfBlank() + ?.takeUnless { it == "[]" }, ) - }.getOrNull() - }.filter { backupSavedSearch -> - currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } - }.ifEmpty { null } - - if (newSavedSearches != null) { - databaseHelper.insertSavedSearches(newSavedSearches) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestore.kt b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestore.kt index 2f8f5d0f3..9e39605df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestore.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupRestore.kt @@ -73,7 +73,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract } // SY --> - private fun restoreSavedSearches(savedSearches: String) { + private suspend fun restoreSavedSearches(savedSearches: String) { backupManager.restoreSavedSearches(savedSearches) restoreProgress += 1 diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt index 87ee4a841..5d02ebc64 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt @@ -47,7 +47,10 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory /** * This class provides operations to manage the database through its interfaces. */ -open class DatabaseHelper(context: Context) : +open class DatabaseHelper( + context: Context, + callback: DbOpenCallback +) : MangaQueries, ChapterQueries, TrackQueries, @@ -66,7 +69,7 @@ open class DatabaseHelper(context: Context) : private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) .name(DbOpenCallback.DATABASE_NAME) - .callback(DbOpenCallback()) + .callback(callback) .build() override val db = DefaultStorIOSQLite.builder() diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt index 93826ab8a..ef541ac42 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt @@ -2,115 +2,28 @@ package eu.kanade.tachiyomi.data.database import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteOpenHelper -import eu.kanade.tachiyomi.data.database.tables.CategoryTable -import eu.kanade.tachiyomi.data.database.tables.ChapterTable -import eu.kanade.tachiyomi.data.database.tables.HistoryTable -import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable -import eu.kanade.tachiyomi.data.database.tables.MangaTable -import eu.kanade.tachiyomi.data.database.tables.TrackTable -import exh.favorites.sql.tables.FavoriteEntryTable -import exh.merged.sql.tables.MergedTable -import exh.metadata.sql.tables.SearchMetadataTable -import exh.metadata.sql.tables.SearchTagTable -import exh.metadata.sql.tables.SearchTitleTable -import exh.savedsearches.tables.FeedSavedSearchTable -import exh.savedsearches.tables.SavedSearchTable +import com.squareup.sqldelight.android.AndroidSqliteDriver +import eu.kanade.tachiyomi.Database -class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) { +class DbOpenCallback : SupportSQLiteOpenHelper.Callback(Database.Schema.version) { companion object { /** * Name of the database file. */ const val DATABASE_NAME = "tachiyomi.db" - - /** - * Version of the database. - */ - const val DATABASE_VERSION = /* SY --> */ 13 // SY <-- } - override fun onCreate(db: SupportSQLiteDatabase) = with(db) { - execSQL(MangaTable.createTableQuery) - execSQL(ChapterTable.createTableQuery) - execSQL(TrackTable.createTableQuery) - execSQL(CategoryTable.createTableQuery) - execSQL(MangaCategoryTable.createTableQuery) - execSQL(HistoryTable.createTableQuery) - // SY --> - execSQL(SearchMetadataTable.createTableQuery) - execSQL(SearchTagTable.createTableQuery) - execSQL(SearchTitleTable.createTableQuery) - execSQL(MergedTable.createTableQuery) - execSQL(FavoriteEntryTable.createTableQuery) - execSQL(SavedSearchTable.createTableQuery) - execSQL(FeedSavedSearchTable.createTableQuery) - // SY <-- - - // DB indexes - execSQL(MangaTable.createUrlIndexQuery) - execSQL(MangaTable.createLibraryIndexQuery) - execSQL(ChapterTable.createMangaIdIndexQuery) - execSQL(ChapterTable.createUnreadChaptersIndexQuery) - execSQL(HistoryTable.createChapterIdIndexQuery) - // SY --> - execSQL(SearchMetadataTable.createUploaderIndexQuery) - execSQL(SearchMetadataTable.createIndexedExtraIndexQuery) - execSQL(SearchTagTable.createMangaIdIndexQuery) - execSQL(SearchTagTable.createNamespaceNameIndexQuery) - execSQL(SearchTitleTable.createMangaIdIndexQuery) - execSQL(SearchTitleTable.createTitleIndexQuery) - execSQL(MergedTable.createIndexQuery) - execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery) - // SY <-- + override fun onCreate(db: SupportSQLiteDatabase) { + Database.Schema.create(AndroidSqliteDriver(database = db, cacheSize = 1)) } override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) { - if (oldVersion < 2) { - db.execSQL(MangaTable.addCoverLastModified) - } - if (oldVersion < 3) { - db.execSQL(MangaTable.addDateAdded) - db.execSQL(MangaTable.backfillDateAdded) - } - if (oldVersion < 4) { - db.execSQL(MergedTable.dropTableQuery) - db.execSQL(MergedTable.createTableQuery) - db.execSQL(MergedTable.createIndexQuery) - } - /*if (oldVersion < 5) { - db.execSQL(SimilarTable.createTableQuery) - db.execSQL(SimilarTable.createMangaIdIndexQuery) - }*/ - if (oldVersion < 6) { - db.execSQL(MangaTable.addFilteredScanlators) - } - if (oldVersion < 7) { - db.execSQL("DROP TABLE IF EXISTS manga_related") - } - if (oldVersion < 8) { - db.execSQL(MangaTable.addNextUpdateCol) - } - if (oldVersion < 9) { - db.execSQL(TrackTable.renameTableToTemp) - db.execSQL(TrackTable.createTableQuery) - db.execSQL(TrackTable.insertFromTempTable) - db.execSQL(TrackTable.dropTempTable) - } - if (oldVersion < 10) { - db.execSQL(ChapterTable.fixDateUploadIfNeeded) - } - if (oldVersion < 11) { - db.execSQL(FavoriteEntryTable.createTableQuery) - } - if (oldVersion < 12) { - db.execSQL(FavoriteEntryTable.fixTableQuery) - } - if (oldVersion < 13) { - db.execSQL(SavedSearchTable.createTableQuery) - db.execSQL(FeedSavedSearchTable.createTableQuery) - db.execSQL(FeedSavedSearchTable.createSavedSearchIdIndexQuery) - } + Database.Schema.migrate( + driver = AndroidSqliteDriver(database = db, cacheSize = 1), + oldVersion = oldVersion, + newVersion = newVersion + ) } override fun onConfigure(db: SupportSQLiteDatabase) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt index 7b2ec55e2..e2ab2f346 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt @@ -4,40 +4,12 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery import com.pushtorefresh.storio.sqlite.queries.RawQuery import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.models.History -import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory import eu.kanade.tachiyomi.data.database.resolvers.HistoryChapterIdPutResolver -import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver -import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver +import eu.kanade.tachiyomi.data.database.resolvers.HistoryUpsertResolver 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( @@ -65,9 +37,9 @@ interface HistoryQueries : DbProvider { * Inserts history object if not yet in database * @param history history object */ - fun updateHistoryLastRead(history: History) = db.put() + fun upsertHistoryLastRead(history: History) = db.put() .`object`(history) - .withPutResolver(HistoryLastReadPutResolver()) + .withPutResolver(HistoryUpsertResolver()) .prepare() /** @@ -75,12 +47,12 @@ interface HistoryQueries : DbProvider { * Inserts history object if not yet in database * @param historyList history object list */ - fun updateHistoryLastRead(historyList: List) = db.put() + fun upsertHistoryLastRead(historyList: List) = db.put() .objects(historyList) - .withPutResolver(HistoryLastReadPutResolver()) + .withPutResolver(HistoryUpsertResolver()) .prepare() - fun deleteHistory() = db.delete() + fun dropHistoryTable() = db.delete() .byQuery( DeleteQuery.builder() .table(HistoryTable.TABLE) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt index 8d3e69f3e..15e3c0e5b 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/queries/RawQueries.kt @@ -1,8 +1,6 @@ package eu.kanade.tachiyomi.data.database.queries import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver -import exh.savedsearches.tables.FeedSavedSearchTable -import exh.savedsearches.tables.SavedSearchTable import exh.source.MERGED_SOURCE_ID import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter @@ -76,32 +74,6 @@ fun getReadMangaNotInLibraryQuery() = ) """ -/** - * Query to get the global feed saved searches - */ -fun getGlobalFeedSavedSearchQuery() = - """ - SELECT ${SavedSearchTable.TABLE}.* - FROM ( - SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 1 - ) AS M - JOIN ${SavedSearchTable.TABLE} - ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} -""" - -/** - * Query to get the source feed saved searches - */ -fun getSourceFeedSavedSearchQuery() = - """ - SELECT ${SavedSearchTable.TABLE}.* - FROM ( - SELECT ${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} FROM ${FeedSavedSearchTable.TABLE} WHERE ${FeedSavedSearchTable.COL_GLOBAL} = 0 AND ${FeedSavedSearchTable.COL_SOURCE} = ? - ) AS M - JOIN ${SavedSearchTable.TABLE} - ON ${SavedSearchTable.TABLE}.${SavedSearchTable.COL_ID} = M.${FeedSavedSearchTable.COL_SAVED_SEARCH_ID} -""" - /** * Query to get the manga from the library, with their categories, read and unread count. */ @@ -190,7 +162,8 @@ fun getRecentMangasQuery(search: String = "") = SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ} FROM ${Chapter.TABLE} JOIN ${History.TABLE} ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} - GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read + GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} + ) AS max_last_read ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID} WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryUpsertResolver.kt similarity index 97% rename from app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt rename to app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryUpsertResolver.kt index 7bcba97f7..908aca16d 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryLastReadPutResolver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/resolvers/HistoryUpsertResolver.kt @@ -11,7 +11,7 @@ import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.tables.HistoryTable -class HistoryLastReadPutResolver : HistoryPutResolver() { +class HistoryUpsertResolver : HistoryPutResolver() { /** * Updates last_read time of chapter diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt index 5ca03b56e..80a88ca55 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt @@ -15,19 +15,4 @@ object CategoryTable { // SY --> const val COL_MANGA_ORDER = "manga_order" // SY <-- - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_NAME TEXT NOT NULL, - $COL_ORDER INTEGER NOT NULL, - $COL_FLAGS INTEGER NOT NULL, - $COL_MANGA_ORDER TEXT NOT NULL - )""" - - // SY --> - val addMangaOrder: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_MANGA_ORDER TEXT" - // SY <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt index 793349119..8914e6f3c 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/ChapterTable.kt @@ -27,42 +27,4 @@ object ChapterTable { const val COL_CHAPTER_NUMBER = "chapter_number" const val COL_SOURCE_ORDER = "source_order" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_MANGA_ID INTEGER NOT NULL, - $COL_URL TEXT NOT NULL, - $COL_NAME TEXT NOT NULL, - $COL_SCANLATOR TEXT, - $COL_READ BOOLEAN NOT NULL, - $COL_BOOKMARK BOOLEAN NOT NULL, - $COL_LAST_PAGE_READ INT NOT NULL, - $COL_CHAPTER_NUMBER FLOAT NOT NULL, - $COL_SOURCE_ORDER INTEGER NOT NULL, - $COL_DATE_FETCH LONG NOT NULL, - $COL_DATE_UPLOAD LONG NOT NULL, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" - - val createMangaIdIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)" - - val createUnreadChaptersIndexQuery: String - get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " + - "WHERE $COL_READ = 0" - - val sourceOrderUpdateQuery: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0" - - val bookmarkUpdateQuery: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE" - - val addScanlator: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL" - - val fixDateUploadIfNeeded: String - get() = "UPDATE $TABLE SET $COL_DATE_UPLOAD = $COL_DATE_FETCH WHERE $COL_DATE_UPLOAD = 0" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/HistoryTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/HistoryTable.kt index 9d19544a4..4dfe9f0dd 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/HistoryTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/HistoryTable.kt @@ -26,24 +26,4 @@ object HistoryTable { * Time read column name */ const val COL_TIME_READ = "${TABLE}_time_read" - - /** - * query to create history table - */ - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_CHAPTER_ID INTEGER NOT NULL UNIQUE, - $COL_LAST_READ LONG, - $COL_TIME_READ LONG, - FOREIGN KEY($COL_CHAPTER_ID) REFERENCES ${ChapterTable.TABLE} (${ChapterTable.COL_ID}) - ON DELETE CASCADE - )""" - - /** - * query to index history chapter id - */ - val createChapterIdIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_CHAPTER_ID}_index ON $TABLE($COL_CHAPTER_ID)" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt index 578a85bbc..d39b32adf 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaCategoryTable.kt @@ -9,16 +9,4 @@ object MangaCategoryTable { const val COL_MANGA_ID = "manga_id" const val COL_CATEGORY_ID = "category_id" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_MANGA_ID INTEGER NOT NULL, - $COL_CATEGORY_ID INTEGER NOT NULL, - FOREIGN KEY($COL_CATEGORY_ID) REFERENCES ${CategoryTable.TABLE} (${CategoryTable.COL_ID}) - ON DELETE CASCADE, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt index 9f2e46f78..3e42dea7a 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt @@ -51,59 +51,4 @@ object MangaTable { const val COMPUTED_COL_UNREAD_COUNT = "unread_count" const val COMPUTED_COL_READ_COUNT = "read_count" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_SOURCE INTEGER NOT NULL, - $COL_URL TEXT NOT NULL, - $COL_ARTIST TEXT, - $COL_AUTHOR TEXT, - $COL_DESCRIPTION TEXT, - $COL_GENRE TEXT, - $COL_TITLE TEXT NOT NULL, - $COL_STATUS INTEGER NOT NULL, - $COL_THUMBNAIL_URL TEXT, - $COL_FAVORITE INTEGER NOT NULL, - $COL_LAST_UPDATE LONG, - $COL_NEXT_UPDATE LONG, - $COL_INITIALIZED BOOLEAN NOT NULL, - $COL_VIEWER INTEGER NOT NULL, - $COL_CHAPTER_FLAGS INTEGER NOT NULL, - $COL_COVER_LAST_MODIFIED LONG NOT NULL, - $COL_DATE_ADDED LONG NOT NULL, - $COL_FILTERED_SCANLATORS TEXT - )""" - - val createUrlIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_URL}_index ON $TABLE($COL_URL)" - - val createLibraryIndexQuery: String - get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " + - "WHERE $COL_FAVORITE = 1" - - val addCoverLastModified: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0" - - val addDateAdded: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG NOT NULL DEFAULT 0" - - /** - * Used with addDateAdded to populate it with the oldest chapter fetch date. - */ - val backfillDateAdded: String - get() = "UPDATE $TABLE SET $COL_DATE_ADDED = " + - "(SELECT MIN(${ChapterTable.COL_DATE_FETCH}) " + - "FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " + - "ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " + - "GROUP BY $TABLE.$COL_ID)" - - val addNextUpdateCol: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_NEXT_UPDATE LONG DEFAULT 0" - - // SY --> - val addFilteredScanlators: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FILTERED_SCANLATORS TEXT" - // SY <-- } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt index 5a9a8f239..90c38d537 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/database/tables/TrackTable.kt @@ -30,43 +30,6 @@ object TrackTable { const val COL_FINISH_DATE = "finish_date" - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_MANGA_ID INTEGER NOT NULL, - $COL_SYNC_ID INTEGER NOT NULL, - $COL_MEDIA_ID INTEGER NOT NULL, - $COL_LIBRARY_ID INTEGER, - $COL_TITLE TEXT NOT NULL, - $COL_LAST_CHAPTER_READ REAL NOT NULL, - $COL_TOTAL_CHAPTERS INTEGER NOT NULL, - $COL_STATUS INTEGER NOT NULL, - $COL_SCORE FLOAT NOT NULL, - $COL_TRACKING_URL TEXT NOT NULL, - $COL_START_DATE LONG NOT NULL, - $COL_FINISH_DATE LONG NOT NULL, - UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" - - val addTrackingUrl: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_TRACKING_URL TEXT DEFAULT ''" - - val addLibraryId: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL" - - val addStartDate: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_START_DATE LONG NOT NULL DEFAULT 0" - - val addFinishDate: String - get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0" - - val renameTableToTemp: String - get() = - "ALTER TABLE $TABLE RENAME TO ${TABLE}_tmp" - val insertFromTempTable: String get() = """ @@ -74,7 +37,4 @@ object TrackTable { |SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE |FROM ${TABLE}_tmp """.trimMargin() - - val dropTempTable: String - get() = "DROP TABLE ${TABLE}_tmp" } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt index 26fc7dea7..594325527 100755 --- a/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/notification/NotificationReceiver.kt @@ -166,7 +166,7 @@ class NotificationReceiver : BroadcastReceiver() { * @param chapterId id of chapter */ private fun openChapter(context: Context, mangaId: Long, chapterId: Long) { - val db = DatabaseHelper(context) + val db = Injekt.get() val manga = db.getManga(mangaId).executeAsBlocking() val chapter = db.getChapter(chapterId).executeAsBlocking() if (manga != null && chapter != null) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt new file mode 100644 index 000000000..24c6719a6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/controller/ComposeController.kt @@ -0,0 +1,26 @@ +package eu.kanade.tachiyomi.ui.base.controller + +import android.view.LayoutInflater +import android.view.View +import androidx.compose.runtime.Composable +import eu.kanade.presentation.theme.TachiyomiTheme +import eu.kanade.tachiyomi.databinding.ComposeControllerBinding +import nucleus.presenter.Presenter + +abstract class ComposeController

> : NucleusController() { + + override fun createBinding(inflater: LayoutInflater): ComposeControllerBinding = + ComposeControllerBinding.inflate(inflater) + + override fun onViewCreated(view: View) { + super.onViewCreated(view) + + binding.root.setContent { + TachiyomiTheme { + ComposeContent() + } + } + } + + @Composable abstract fun ComposeContent() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedController.kt index 5ebc38bfb..6a750ede4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedController.kt @@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.system.toast import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.SavedSearch @@ -66,41 +67,45 @@ open class FeedController : } private fun addFeed() { - if (presenter.hasTooManyFeeds()) { - activity?.toast(R.string.too_many_in_feed) - return - } - val items = presenter.getEnabledSources() - val itemsStrings = items.map { it.toString() } - var selectedIndex = 0 + viewScope.launchUI { + if (presenter.hasTooManyFeeds()) { + activity?.toast(R.string.too_many_in_feed) + return@launchUI + } + val items = presenter.getEnabledSources() + val itemsStrings = items.map { it.toString() } + var selectedIndex = 0 - MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.feed) - .setSingleChoiceItems(itemsStrings.toTypedArray(), selectedIndex) { _, which -> - selectedIndex = which - } - .setPositiveButton(android.R.string.ok) { _, _ -> - addFeedSearch(items[selectedIndex]) - } - .setNegativeButton(android.R.string.cancel, null) - .show() + MaterialAlertDialogBuilder(activity!!) + .setTitle(R.string.feed) + .setSingleChoiceItems(itemsStrings.toTypedArray(), selectedIndex) { _, which -> + selectedIndex = which + } + .setPositiveButton(android.R.string.ok) { _, _ -> + addFeedSearch(items[selectedIndex]) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } } private fun addFeedSearch(source: CatalogueSource) { - val items = presenter.getSourceSavedSearches(source) - val itemsStrings = listOf(activity!!.getString(R.string.latest)) + items.map { it.name } - var selectedIndex = 0 + viewScope.launchUI { + val items = presenter.getSourceSavedSearches(source) + val itemsStrings = listOf(activity!!.getString(R.string.latest)) + items.map { it.name } + var selectedIndex = 0 - MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.feed) - .setSingleChoiceItems(itemsStrings.toTypedArray(), selectedIndex) { _, which -> - selectedIndex = which - } - .setPositiveButton(android.R.string.ok) { _, _ -> - presenter.createFeed(source, items.getOrNull(selectedIndex - 1)) - } - .setNegativeButton(android.R.string.cancel, null) - .show() + MaterialAlertDialogBuilder(activity!!) + .setTitle(R.string.feed) + .setSingleChoiceItems(itemsStrings.toTypedArray(), selectedIndex) { _, which -> + selectedIndex = which + } + .setPositiveButton(android.R.string.ok) { _, _ -> + presenter.createFeed(source, items.getOrNull(selectedIndex - 1)) + } + .setNegativeButton(android.R.string.cancel, null) + .show() + } } /** diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt index 21c6b932f..2fb86e947 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/feed/FeedPresenter.kt @@ -1,6 +1,9 @@ package eu.kanade.tachiyomi.ui.browse.feed import android.os.Bundle +import eu.kanade.data.DatabaseHandler +import eu.kanade.data.exh.feedSavedSearchMapper +import eu.kanade.data.exh.savedSearchMapper import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.toMangaInfo @@ -15,9 +18,12 @@ import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.runAsObservable +import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.system.logcat import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.SavedSearch +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import logcat.LogPriority @@ -35,11 +41,12 @@ import xyz.nulldev.ts.api.http.serializer.FilterSerializer * Function calls should be done from here. UI calls should be done from the controller. * * @param sourceManager manages the different sources. - * @param db manages the database calls. + * @param database manages the database calls. * @param preferences manages the preference calls. */ open class FeedPresenter( val sourceManager: SourceManager = Injekt.get(), + val database: DatabaseHandler = Injekt.get(), val db: DatabaseHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(), ) : BasePresenter() { @@ -62,14 +69,11 @@ open class FeedPresenter( override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - db.getGlobalFeedSavedSearches() - .asRxObservable() - .observeOn(AndroidSchedulers.mainThread()) - .doOnEach { + database.subscribeToList { feed_saved_searchQueries.selectAllGlobal() } + .onEach { getFeed() } - .subscribe() - .let(::add) + .launchIn(presenterScope) } override fun onDestroy() { @@ -78,8 +82,10 @@ open class FeedPresenter( super.onDestroy() } - fun hasTooManyFeeds(): Boolean { - return db.getGlobalFeedSavedSearches().executeAsBlocking().size > 10 + suspend fun hasTooManyFeeds(): Boolean { + return withIOContext { + database.awaitList { feed_saved_searchQueries.selectAllGlobal() }.size > 10 + } } fun getEnabledSources(): List { @@ -93,33 +99,38 @@ open class FeedPresenter( return list.sortedBy { it.id.toString() !in pinnedSources } } - fun getSourceSavedSearches(source: CatalogueSource): List { - return db.getSavedSearches(source.id).executeAsBlocking() + suspend fun getSourceSavedSearches(source: CatalogueSource): List { + return withIOContext { + database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) } + } } fun createFeed(source: CatalogueSource, savedSearch: SavedSearch?) { launchIO { - db.insertFeedSavedSearch( - FeedSavedSearch( - id = null, + database.await { + feed_saved_searchQueries.insertFeedSavedSearch( + _id = null, source = source.id, - savedSearch = savedSearch?.id, + saved_search = savedSearch?.id, global = true, - ), - ).executeAsBlocking() + ) + } } } fun deleteFeed(feed: FeedSavedSearch) { launchIO { - db.deleteFeedSavedSearch(feed).executeAsBlocking() + database.await { + feed_saved_searchQueries.deleteById(feed.id ?: return@await) + } } } - private fun getSourcesToGetFeed(): List> { - val savedSearches = db.getGlobalSavedSearchesFeed().executeAsBlocking() - .associateBy { it.id!! } - return db.getGlobalFeedSavedSearches().executeAsBlocking() + private suspend fun getSourcesToGetFeed(): List> { + val savedSearches = database.awaitList { + feed_saved_searchQueries.selectGlobalFeedSavedSearch(savedSearchMapper) + }.associateBy { it.id } + return database.awaitList { feed_saved_searchQueries.selectAllGlobal(feedSavedSearchMapper) } .map { it to savedSearches[it.savedSearch] } } @@ -138,7 +149,7 @@ open class FeedPresenter( /** * Initiates get manga per feed. */ - fun getFeed() { + suspend fun getFeed() { // Create image fetch subscription initializeFetchImageSubscription() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt index 23b9b6c47..c45e49ec9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/advanced/process/MigrationProcessAdapter.kt @@ -137,7 +137,7 @@ class MigrationProcessAdapter( } } db.insertChapters(dbChapters).executeAsBlocking() - db.updateHistoryLastRead(historyList).executeAsBlocking() + db.upsertHistoryLastRead(historyList).executeAsBlocking() } // Update categories if (MigrationFlags.hasCategories(flags)) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt index 1b709751f..94bd21a1b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt @@ -37,8 +37,8 @@ class SearchController( Injekt.get().getManga(mangaId).executeAsBlocking(), sources.map { Injekt.get().getOrStub(it) }.filterIsInstance(), ) { - this.targetController = targetController - } + this.targetController = targetController + } @Suppress("unused") constructor(bundle: Bundle) : this( diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt index 9ebf73302..aff2553df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourceController.kt @@ -44,6 +44,7 @@ import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.more.MoreController import eu.kanade.tachiyomi.ui.webview.WebViewActivity +import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.preference.asImmediateFlow import eu.kanade.tachiyomi.util.system.connectivityManager import eu.kanade.tachiyomi.util.system.openInBrowser @@ -201,7 +202,7 @@ open class BrowseSourceController(bundle: Bundle) : // SY --> this, presenter.source, - presenter.loadSearches(), + emptyList(), // SY <-- onFilterClicked = { showProgressBar() @@ -216,54 +217,58 @@ open class BrowseSourceController(bundle: Bundle) : }, // EXH --> onSaveClicked = { - filterSheet?.context?.let { - val names = presenter.loadSearches().map { it.name } - var searchName = "" - MaterialAlertDialogBuilder(it) - .setTitle(R.string.save_search) - .setTextInput(hint = it.getString(R.string.save_search_hint)) { input -> - searchName = input - } - .setPositiveButton(R.string.action_save) { _, _ -> - if (searchName.isNotBlank() && searchName !in names) { - presenter.saveSearch(searchName.trim(), presenter.query, presenter.sourceFilters) - } else { - it.toast(R.string.save_search_invalid_name) - } - } - .setNegativeButton(R.string.action_cancel, null) - .show() - } - }, - onSavedSearchClicked = cb@{ idOfSearch -> - val search = presenter.loadSearch(idOfSearch) - - if (search == null) { + viewScope.launchUI { filterSheet?.context?.let { + val names = presenter.loadSearches().map { it.name } + var searchName = "" MaterialAlertDialogBuilder(it) - .setTitle(R.string.save_search_failed_to_load) - .setMessage(R.string.save_search_failed_to_load_message) + .setTitle(R.string.save_search) + .setTextInput(hint = it.getString(R.string.save_search_hint)) { input -> + searchName = input + } + .setPositiveButton(R.string.action_save) { _, _ -> + if (searchName.isNotBlank() && searchName !in names) { + presenter.saveSearch(searchName.trim(), presenter.query, presenter.sourceFilters) + } else { + it.toast(R.string.save_search_invalid_name) + } + } + .setNegativeButton(R.string.action_cancel, null) .show() } - return@cb } - - if (search.filterList == null) { - activity?.toast(R.string.save_search_invalid) - return@cb - } - - presenter.sourceFilters = FilterList(search.filterList) - filterSheet?.setFilters(presenter.filterItems) - val allDefault = presenter.sourceFilters == presenter.source.getFilterList() - - showProgressBar() - adapter?.clear() - filterSheet?.dismiss() - presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters) - activity?.invalidateOptionsMenu() }, - onSavedSearchDeleteClicked = cb@{ idToDelete, name -> + onSavedSearchClicked = { idOfSearch -> + viewScope.launchUI { + val search = presenter.loadSearch(idOfSearch) + + if (search == null) { + filterSheet?.context?.let { + MaterialAlertDialogBuilder(it) + .setTitle(R.string.save_search_failed_to_load) + .setMessage(R.string.save_search_failed_to_load_message) + .show() + } + return@launchUI + } + + if (search.filterList == null) { + activity?.toast(R.string.save_search_invalid) + return@launchUI + } + + presenter.sourceFilters = FilterList(search.filterList) + filterSheet?.setFilters(presenter.filterItems) + val allDefault = presenter.sourceFilters == presenter.source.getFilterList() + + showProgressBar() + adapter?.clear() + filterSheet?.dismiss() + presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters) + activity?.invalidateOptionsMenu() + } + }, + onSavedSearchDeleteClicked = { idToDelete, name -> filterSheet?.context?.let { MaterialAlertDialogBuilder(it) .setTitle(R.string.save_search_delete) @@ -277,6 +282,9 @@ open class BrowseSourceController(bundle: Bundle) : }, // EXH <-- ) + launchUI { + filterSheet?.setSavedSearches(presenter.loadSearches()) + } filterSheet?.setFilters(presenter.filterItems) filterSheet?.setOnShowListener { actionFab?.hide() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt index b0759389e..1cd6ad975 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt @@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.ui.browse.source.browse import android.os.Bundle import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.data.DatabaseHandler +import eu.kanade.data.exh.savedSearchMapper import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Category @@ -37,6 +39,7 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.system.logcat @@ -50,8 +53,10 @@ import kotlinx.coroutines.flow.asFlow import kotlinx.coroutines.flow.catch import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.filter +import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.onEach +import kotlinx.coroutines.runBlocking import kotlinx.serialization.decodeFromString import kotlinx.serialization.encodeToString import kotlinx.serialization.json.Json @@ -78,6 +83,7 @@ open class BrowseSourcePresenter( // SY <-- private val sourceManager: SourceManager = Injekt.get(), private val db: DatabaseHelper = Injekt.get(), + private val database: DatabaseHandler = Injekt.get(), private val prefs: PreferencesHelper = Injekt.get(), private val coverCache: CoverCache = Injekt.get(), ) : BasePresenter() { @@ -140,7 +146,11 @@ open class BrowseSourcePresenter( val jsonFilters = filters if (savedSearchFilters != null) { runCatching { - val savedSearch = db.getSavedSearch(savedSearchFilters).executeAsBlocking() ?: return@runCatching + val savedSearch = runBlocking { + database.awaitOneOrNull { + saved_searchQueries.selectById(savedSearchFilters, savedSearchMapper) + } + } ?: return@runCatching query = savedSearch.query.orEmpty() val filtersJson = savedSearch.filtersJson ?: return@runCatching @@ -156,18 +166,14 @@ open class BrowseSourcePresenter( } } - db.getSavedSearches(source.id) - .asRxObservable() - .map { - loadSearches(it) + database.subscribeToList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) } + .map { loadSearches(it) } + .onEach { + withUIContext { + view?.setSavedSearches(it) + } } - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .subscribeLatestCache( - { controller, savedSearches -> - controller.setSavedSearches(savedSearches) - }, - ) + .launchIn(presenterScope) // SY <-- if (savedState != null) { @@ -485,83 +491,90 @@ open class BrowseSourcePresenter( fun saveSearch(name: String, query: String, filterList: FilterList) { launchIO { kotlin.runCatching { - val savedSearch = SavedSearch( - id = null, - source = source.id, - name = name.trim(), - query = query.nullIfBlank(), - filtersJson = filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) }, - ) - - db.insertSavedSearch(savedSearch).executeAsBlocking() + database.await { + saved_searchQueries.insertSavedSearch( + _id = null, + source = source.id, + name = name.trim(), + query = query.nullIfBlank(), + filters_json = filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) }, + ) + } } } } fun deleteSearch(searchId: Long) { launchIO { - db.deleteSavedSearch(searchId).executeAsBlocking() + database.await { saved_searchQueries.deleteById(searchId) } } } - fun loadSearch(searchId: Long): EXHSavedSearch? { - val search = db.getSavedSearch(searchId).executeAsBlocking() ?: return null - return EXHSavedSearch( - id = search.id!!, - name = search.name, - query = search.query.orEmpty(), - filterList = runCatching { - val originalFilters = source.getFilterList() - filterSerializer.deserialize( - filters = originalFilters, - json = search.filtersJson - ?.let { Json.decodeFromString(it) } - ?: return@runCatching null, - ) - originalFilters - }.getOrNull(), - ) + suspend fun loadSearch(searchId: Long): EXHSavedSearch? { + return withIOContext { + val search = database.awaitOneOrNull { + saved_searchQueries.selectById(searchId, savedSearchMapper) + } ?: return@withIOContext null + EXHSavedSearch( + id = search.id!!, + name = search.name, + query = search.query.orEmpty(), + filterList = runCatching { + val originalFilters = source.getFilterList() + filterSerializer.deserialize( + filters = originalFilters, + json = search.filtersJson + ?.let { Json.decodeFromString(it) } + ?: return@runCatching null, + ) + originalFilters + }.getOrNull(), + ) + } } - fun loadSearches(searches: List = db.getSavedSearches(source.id).executeAsBlocking()): List { - return searches.map { - val filtersJson = it.filtersJson ?: return@map EXHSavedSearch( - id = it.id!!, - name = it.name, - query = it.query.orEmpty(), - filterList = null, - ) - val filters = try { - Json.decodeFromString(filtersJson) - } catch (e: Exception) { - xLogE("Failed to load saved search!", e) - null - } ?: return@map EXHSavedSearch( - id = it.id!!, - name = it.name, - query = it.query.orEmpty(), - filterList = null, - ) + suspend fun loadSearches(searches: List? = null): List { + return withIOContext { + (searches ?: (database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) })) + .map { + val filtersJson = it.filtersJson ?: return@map EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null, + ) + val filters = try { + Json.decodeFromString(filtersJson) + } catch (e: Exception) { + xLogE("Failed to load saved search!", e) + null + } ?: return@map EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null, + ) - try { - val originalFilters = source.getFilterList() - filterSerializer.deserialize(originalFilters, filters) - EXHSavedSearch( - id = it.id!!, - name = it.name, - query = it.query.orEmpty(), - filterList = originalFilters, - ) - } catch (t: RuntimeException) { - // Load failed - xLogE("Failed to load saved search!", t) - EXHSavedSearch( - id = it.id!!, - name = it.name, - query = it.query.orEmpty(), - filterList = null, - ) - } + try { + val originalFilters = source.getFilterList() + filterSerializer.deserialize(originalFilters, filters) + EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = originalFilters, + ) + } catch (t: RuntimeException) { + // Load failed + xLogE("Failed to load saved search!", t) + EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null, + ) + } + } } } // EXH <-- diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedController.kt index 491a9e59b..973c2c4c4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedController.kt @@ -24,6 +24,8 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.browse.source.browse.SourceFilterSheet import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.lang.launchUI +import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.toast import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.SavedSearch @@ -184,7 +186,7 @@ open class SourceFeedController : // SY --> this, presenter.source, - presenter.loadSearches(), + emptyList(), // SY <-- onFilterClicked = { val allDefault = presenter.sourceFilters == presenter.source.getFilterList() @@ -202,51 +204,60 @@ open class SourceFeedController : }, onResetClicked = {}, onSaveClicked = {}, - onSavedSearchClicked = cb@{ idOfSearch -> - val search = presenter.loadSearch(idOfSearch) + onSavedSearchClicked = { idOfSearch -> + viewScope.launchUI { + val search = presenter.loadSearch(idOfSearch) - if (search == null) { - filterSheet?.context?.let { - MaterialAlertDialogBuilder(it) - .setTitle(R.string.save_search_failed_to_load) - .setMessage(R.string.save_search_failed_to_load_message) + if (search == null) { + filterSheet?.context?.let { + MaterialAlertDialogBuilder(it) + .setTitle(R.string.save_search_failed_to_load) + .setMessage(R.string.save_search_failed_to_load_message) + .show() + } + return@launchUI + } + + if (search.filterList == null) { + activity?.toast(R.string.save_search_invalid) + return@launchUI + } + + presenter.sourceFilters = FilterList(search.filterList) + filterSheet?.setFilters(presenter.filterItems) + val allDefault = presenter.sourceFilters == presenter.source.getFilterList() + filterSheet?.dismiss() + + if (!allDefault) { + onBrowseClick( + search = presenter.query.nullIfBlank(), + savedSearch = search.id, + ) + } + } + }, + onSavedSearchDeleteClicked = { idOfSearch, name -> + viewScope.launchUI { + if (presenter.hasTooManyFeeds()) { + activity?.toast(R.string.too_many_in_feed) + return@launchUI + } + withUIContext { + MaterialAlertDialogBuilder(activity!!) + .setTitle(R.string.feed) + .setMessage(activity!!.getString(R.string.feed_add, name)) + .setPositiveButton(R.string.action_add) { _, _ -> + presenter.createFeed(idOfSearch) + } + .setNegativeButton(android.R.string.cancel, null) .show() } - return@cb } - - if (search.filterList == null) { - activity?.toast(R.string.save_search_invalid) - return@cb - } - - presenter.sourceFilters = FilterList(search.filterList) - filterSheet?.setFilters(presenter.filterItems) - val allDefault = presenter.sourceFilters == presenter.source.getFilterList() - filterSheet?.dismiss() - - if (!allDefault) { - onBrowseClick( - search = presenter.query.nullIfBlank(), - savedSearch = search.id, - ) - } - }, - onSavedSearchDeleteClicked = cb@{ idOfSearch, name -> - if (presenter.hasTooManyFeeds()) { - activity?.toast(R.string.too_many_in_feed) - return@cb - } - MaterialAlertDialogBuilder(activity!!) - .setTitle(R.string.feed) - .setMessage(activity!!.getString(R.string.feed_add, name)) - .setPositiveButton(R.string.action_add) { _, _ -> - presenter.createFeed(idOfSearch) - } - .setNegativeButton(android.R.string.cancel, null) - .show() }, ) + launchUI { + filterSheet?.setSavedSearches(presenter.loadSearches()) + } filterSheet?.setFilters(presenter.filterItems) // TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt index 7729e6945..f4492b8f9 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/feed/SourceFeedPresenter.kt @@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.browse.source.feed import android.os.Bundle import eu.davidea.flexibleadapter.items.IFlexible +import eu.kanade.data.DatabaseHandler +import eu.kanade.data.exh.feedSavedSearchMapper +import eu.kanade.data.exh.savedSearchMapper import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.toMangaInfo @@ -16,11 +19,15 @@ import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourcePresenter.Companion.toItems import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.runAsObservable +import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.system.logcat import exh.log.xLogE import exh.savedsearches.EXHSavedSearch import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.SavedSearch +import kotlinx.coroutines.CancellationException +import kotlinx.coroutines.flow.launchIn +import kotlinx.coroutines.flow.onEach import kotlinx.serialization.decodeFromString import kotlinx.serialization.json.Json import kotlinx.serialization.json.JsonArray @@ -46,11 +53,12 @@ sealed class SourceFeed { * Function calls should be done from here. UI calls should be done from the controller. * * @param source the source. - * @param db manages the database calls. + * @param database manages the database calls. * @param preferences manages the preference calls. */ open class SourceFeedPresenter( val source: CatalogueSource, + val database: DatabaseHandler = Injekt.get(), val db: DatabaseHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(), ) : BasePresenter() { @@ -90,14 +98,11 @@ open class SourceFeedPresenter( sourceFilters = source.getFilterList() - db.getSourceFeedSavedSearches(source.id) - .asRxObservable() - .observeOn(AndroidSchedulers.mainThread()) - .doOnEach { + database.subscribeToList { feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id, savedSearchMapper) } + .onEach { getFeed() } - .subscribe() - .let(::add) + .launchIn(presenterScope) } override fun onDestroy() { @@ -106,35 +111,39 @@ open class SourceFeedPresenter( super.onDestroy() } - fun hasTooManyFeeds(): Boolean { - return db.getSourceFeedSavedSearches(source.id).executeAsBlocking().size > 10 + suspend fun hasTooManyFeeds(): Boolean { + return withIOContext { + database.awaitList { + feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id) + }.size > 10 + } } - fun getSourceSavedSearches(): List { - return db.getSavedSearches(source.id).executeAsBlocking() + suspend fun getSourceSavedSearches(): List { + return database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) } } fun createFeed(savedSearchId: Long) { launchIO { - db.insertFeedSavedSearch( - FeedSavedSearch( - id = null, + database.await { + feed_saved_searchQueries.insertFeedSavedSearch( + _id = null, source = source.id, - savedSearch = savedSearchId, - global = false, - ), - ).executeAsBlocking() + saved_search = savedSearchId, + global = false + ) + } } } fun deleteFeed(feed: FeedSavedSearch) { launchIO { - db.deleteFeedSavedSearch(feed).executeAsBlocking() + database.await { feed_saved_searchQueries.deleteById(feed.id ?: return@await) } } } - private fun getSourcesToGetFeed(): List { - val savedSearches = db.getSourceSavedSearchesFeed(source.id).executeAsBlocking() + private suspend fun getSourcesToGetFeed(): List { + val savedSearches = database.awaitList { feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id, savedSearchMapper) } .associateBy { it.id!! } return listOfNotNull( @@ -142,7 +151,7 @@ open class SourceFeedPresenter( SourceFeed.Latest } else null, SourceFeed.Browse, - ) + db.getSourceFeedSavedSearches(source.id).executeAsBlocking() + ) + database.awaitList { feed_saved_searchQueries.selectBySource(source.id, feedSavedSearchMapper) } .map { SourceFeed.SourceSavedSearch(it, savedSearches[it.savedSearch]!!) } } @@ -159,7 +168,7 @@ open class SourceFeedPresenter( /** * Initiates get manga per feed. */ - fun getFeed() { + suspend fun getFeed() { // Create image fetch subscription initializeFetchImageSubscription() @@ -300,62 +309,69 @@ open class SourceFeedPresenter( return localManga } - fun loadSearch(searchId: Long): EXHSavedSearch? { - val search = db.getSavedSearch(searchId).executeAsBlocking() ?: return null - return EXHSavedSearch( - id = search.id!!, - name = search.name, - query = search.query.orEmpty(), - filterList = runCatching { - val originalFilters = source.getFilterList() - filterSerializer.deserialize( - filters = originalFilters, - json = search.filtersJson - ?.let { Json.decodeFromString(it) } - ?: return@runCatching null, - ) - originalFilters - }.getOrNull(), - ) + suspend fun loadSearch(searchId: Long): EXHSavedSearch? { + return withIOContext { + val search = database.awaitOneOrNull { + saved_searchQueries.selectById(searchId, savedSearchMapper) + } ?: return@withIOContext null + EXHSavedSearch( + id = search.id!!, + name = search.name, + query = search.query.orEmpty(), + filterList = runCatching { + val originalFilters = source.getFilterList() + filterSerializer.deserialize( + filters = originalFilters, + json = search.filtersJson + ?.let { Json.decodeFromString(it) } + ?: return@runCatching null, + ) + originalFilters + }.getOrNull(), + ) + } } - fun loadSearches(): List { - return db.getSavedSearches(source.id).executeAsBlocking().map { - val filtersJson = it.filtersJson ?: return@map EXHSavedSearch( - id = it.id!!, - name = it.name, - query = it.query.orEmpty(), - filterList = null, - ) - val filters = try { - Json.decodeFromString(filtersJson) - } catch (e: Exception) { - null - } ?: return@map EXHSavedSearch( - id = it.id!!, - name = it.name, - query = it.query.orEmpty(), - filterList = null, - ) - - try { - val originalFilters = source.getFilterList() - filterSerializer.deserialize(originalFilters, filters) - EXHSavedSearch( - id = it.id!!, - name = it.name, - query = it.query.orEmpty(), - filterList = originalFilters, - ) - } catch (t: RuntimeException) { - // Load failed - xLogE("Failed to load saved search!", t) - EXHSavedSearch( + suspend fun loadSearches(): List { + return withIOContext { + database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }.map { + val filtersJson = it.filtersJson ?: return@map EXHSavedSearch( id = it.id!!, name = it.name, query = it.query.orEmpty(), filterList = null, ) + val filters = try { + Json.decodeFromString(filtersJson) + } catch (e: Exception) { + if (e is CancellationException) throw e + null + } ?: return@map EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null, + ) + + try { + val originalFilters = source.getFilterList() + filterSerializer.deserialize(originalFilters, filters) + EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = originalFilters, + ) + } catch (t: RuntimeException) { + // Load failed + xLogE("Failed to load saved search!", t) + EXHSavedSearch( + id = it.id!!, + name = it.name, + query = it.query.orEmpty(), + filterList = null, + ) + } } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index f43e8073e..f88bdce56 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt @@ -35,6 +35,7 @@ import com.google.android.material.snackbar.Snackbar import dev.chrisbanes.insetter.applyInsetter import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.SelectableAdapter +import eu.kanade.domain.history.model.HistoryWithRelations import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -136,6 +137,8 @@ class MangaController : DownloadCustomChaptersDialog.Listener, DeleteChaptersDialog.Listener { + constructor(history: HistoryWithRelations) : this(history.mangaId) + constructor(manga: Manga?, fromSource: Boolean = false, smartSearchConfig: SourceController.SmartSearchConfig? = null, update: Boolean = false) : super( bundleOf( MANGA_EXTRA to (manga?.id ?: 0), diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaCoverImageView.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaCoverImageView.kt deleted file mode 100644 index f7e5daf1c..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaCoverImageView.kt +++ /dev/null @@ -1,24 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info - -import android.content.Context -import android.util.AttributeSet -import androidx.appcompat.widget.AppCompatImageView -import kotlin.math.min - -/** - * A custom ImageView for holding a manga cover with: - * - width: min(maxWidth attr, 33% of parent width) - * - height: 2:3 width:height ratio - * - * Should be defined with a width of match_parent. - */ -class MangaCoverImageView(context: Context, attrs: AttributeSet?) : AppCompatImageView(context, attrs) { - - override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) { - super.onMeasure(widthMeasureSpec, heightMeasureSpec) - - val width = min(maxWidth, measuredWidth / 3) - val height = width / 2 * 3 - setMeasuredDimension(width, height) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt index 7ada13307..c222517ce 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt @@ -127,14 +127,19 @@ import kotlin.time.Duration.Companion.seconds class ReaderActivity : BaseRxActivity() { companion object { - fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent { + + fun newIntent(context: Context, mangaId: Long?, chapterId: Long?): Intent { return Intent(context, ReaderActivity::class.java).apply { - putExtra("manga", manga.id) - putExtra("chapter", chapter.id) + putExtra("manga", mangaId) + putExtra("chapter", chapterId) addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) } } + fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent { + return newIntent(context, manga.id, chapter.id) + } + const val SHIFT_DOUBLE_PAGES = "shiftingDoublePages" const val SHIFTED_PAGE_INDEX = "shiftedPageIndex" const val SHIFTED_CHAP_INDEX = "shiftedChapterIndex" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt index d02fdc5ff..9f9757222 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderPresenter.kt @@ -542,7 +542,7 @@ class ReaderPresenter( private fun saveChapterHistory(chapter: ReaderChapter) { if (!incognitoMode) { val history = History.create(chapter.chapter).apply { last_read = Date().time } - db.updateHistoryLastRead(history).asRxCompletable() + db.upsertHistoryLastRead(history).asRxCompletable() .onErrorComplete() .subscribeOn(Schedulers.io()) .subscribe() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt new file mode 100644 index 000000000..a4080a01d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/ClearHistoryDialogController.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.ui.recent.history + +import android.app.Dialog +import android.os.Bundle +import com.google.android.material.dialog.MaterialAlertDialogBuilder +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.ui.base.controller.DialogController + +class ClearHistoryDialogController : DialogController() { + override fun onCreateDialog(savedViewState: Bundle?): Dialog { + return MaterialAlertDialogBuilder(activity!!) + .setMessage(R.string.clear_history_confirmation) + .setPositiveButton(android.R.string.ok) { _, _ -> + (targetController as? HistoryController) + ?.presenter + ?.deleteAllHistory() + } + .setNegativeButton(android.R.string.cancel, null) + .create() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryAdapter.kt deleted file mode 100644 index 8c1a88ec8..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryAdapter.kt +++ /dev/null @@ -1,51 +0,0 @@ -package eu.kanade.tachiyomi.ui.recent.history - -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.source.SourceManager -import uy.kohesive.injekt.injectLazy -import java.text.DecimalFormat -import java.text.DecimalFormatSymbols - -/** - * Adapter of HistoryHolder. - * Connection between Fragment and Holder - * Holder updates should be called from here. - * - * @param controller a HistoryController object - * @constructor creates an instance of the adapter. - */ -class HistoryAdapter(controller: HistoryController) : - FlexibleAdapter>(null, controller, true) { - - val sourceManager: SourceManager by injectLazy() - - val resumeClickListener: OnResumeClickListener = controller - val removeClickListener: OnRemoveClickListener = controller - val itemClickListener: OnItemClickListener = controller - - /** - * DecimalFormat used to display correct chapter number - */ - val decimalFormat = DecimalFormat( - "#.###", - DecimalFormatSymbols() - .apply { decimalSeparator = '.' }, - ) - - init { - setDisplayHeadersAtStartUp(true) - } - - interface OnResumeClickListener { - fun onResumeClick(position: Int) - } - - interface OnRemoveClickListener { - fun onRemoveClick(position: Int) - } - - interface OnItemClickListener { - fun onItemClick(position: Int) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt index 333dea3d4..f6877b1fc 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryController.kt @@ -1,193 +1,53 @@ package eu.kanade.tachiyomi.ui.recent.history -import android.app.Dialog -import android.os.Bundle -import android.view.LayoutInflater import android.view.Menu import android.view.MenuInflater import android.view.MenuItem -import android.view.View import androidx.appcompat.widget.SearchView -import androidx.recyclerview.widget.LinearLayoutManager -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import dev.chrisbanes.insetter.applyInsetter -import eu.davidea.flexibleadapter.FlexibleAdapter +import androidx.compose.runtime.Composable +import eu.kanade.domain.chapter.model.Chapter +import eu.kanade.presentation.history.HistoryScreen import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.backup.BackupRestoreService -import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.data.database.models.History -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.databinding.HistoryControllerBinding -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.ui.base.controller.NucleusController +import eu.kanade.tachiyomi.ui.base.controller.ComposeController import eu.kanade.tachiyomi.ui.base.controller.RootController import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction -import eu.kanade.tachiyomi.ui.browse.source.browse.ProgressItem -import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.reader.ReaderActivity -import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.toast -import eu.kanade.tachiyomi.util.view.onAnimationsFinished -import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.onEach -import logcat.LogPriority import reactivecircus.flowbinding.appcompat.queryTextChanges -import uy.kohesive.injekt.injectLazy -/** - * Fragment that shows recently read manga. - */ -class HistoryController : - NucleusController(), - RootController, - FlexibleAdapter.OnUpdateListener, - FlexibleAdapter.EndlessScrollListener, - HistoryAdapter.OnRemoveClickListener, - HistoryAdapter.OnResumeClickListener, - HistoryAdapter.OnItemClickListener, - RemoveHistoryDialog.Listener { +class HistoryController : ComposeController(), RootController { - private val db: DatabaseHelper by injectLazy() - - /** - * Adapter containing the recent manga. - */ - var adapter: HistoryAdapter? = null - private set - - /** - * Endless loading item. - */ - private var progressItem: ProgressItem? = null - - /** - * Search query. - */ private var query = "" - override fun getTitle(): String? { - return resources?.getString(R.string.label_recent_manga) - } + override fun getTitle() = resources?.getString(R.string.label_recent_manga) - override fun createPresenter(): HistoryPresenter { - return HistoryPresenter() - } + override fun createPresenter() = HistoryPresenter() - override fun createBinding(inflater: LayoutInflater) = HistoryControllerBinding.inflate(inflater) - - override fun onViewCreated(view: View) { - super.onViewCreated(view) - - binding.recycler.applyInsetter { - type(navigationBars = true) { - padding() - } - } - - // Initialize adapter - binding.recycler.layoutManager = LinearLayoutManager(view.context) - adapter = HistoryAdapter(this@HistoryController) - binding.recycler.setHasFixedSize(true) - binding.recycler.adapter = adapter - adapter?.fastScroller = binding.fastScroller - } - - override fun onDestroyView(view: View) { - adapter = null - super.onDestroyView(view) - } - - /** - * Populate adapter with chapters - * - * @param mangaHistory list of manga history - */ - fun onNextManga(mangaHistory: List, cleanBatch: Boolean = false) { - if (adapter?.itemCount ?: 0 == 0) { - resetProgressItem() - } - if (cleanBatch) { - adapter?.updateDataSet(mangaHistory) - } else { - adapter?.onLoadMoreComplete(mangaHistory) - } - binding.recycler.onAnimationsFinished { - (activity as? MainActivity)?.ready = true - } - } - - /** - * Safely error if next page load fails - */ - fun onAddPageError(error: Throwable) { - adapter?.onLoadMoreComplete(null) - adapter?.endlessTargetCount = 1 - logcat(LogPriority.ERROR, error) - } - - override fun onUpdateEmptyView(size: Int) { - if (size > 0) { - binding.emptyView.hide() - } else { - binding.emptyView.show(R.string.information_no_recent_manga) - } - } - - /** - * Sets a new progress item and reenables the scroll listener. - */ - private fun resetProgressItem() { - progressItem = ProgressItem() - adapter?.endlessTargetCount = 0 - adapter?.setEndlessScrollListener(this, progressItem!!) - } - - override fun onLoadMore(lastPosition: Int, currentPage: Int) { - val view = view ?: return - if (BackupRestoreService.isRunning(view.context.applicationContext)) { - onAddPageError(Throwable()) - return - } - val adapter = adapter ?: return - presenter.requestNext(adapter.itemCount - adapter.headerItems.size, query) - } - - override fun noMoreLoad(newItemsSize: Int) {} - - override fun onResumeClick(position: Int) { - val activity = activity ?: return - val (manga, chapter, _) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return - - val nextChapter = presenter.getNextChapter(chapter, manga) - if (nextChapter != null) { - val intent = ReaderActivity.newIntent(activity, manga, nextChapter) - startActivity(intent) - } else { - activity.toast(R.string.no_next_chapter) - } - } - - override fun onRemoveClick(position: Int) { - val (manga, _, history) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return - RemoveHistoryDialog(this, manga, history).showDialog(router) - } - - override fun onItemClick(position: Int) { - val manga = (adapter?.getItem(position) as? HistoryItem)?.mch?.manga ?: return - router.pushController(MangaController(manga).withFadeTransaction()) - } - - override fun removeHistory(manga: Manga, history: History, all: Boolean) { - if (all) { - // Reset last read of chapter to 0L - presenter.removeAllFromHistory(manga.id!!) - } else { - // Remove all chapters belonging to manga from library - presenter.removeFromHistory(history) - } + @Composable + override fun ComposeContent() { + HistoryScreen( + composeView = binding.root, + presenter = presenter, + onClickItem = { history -> + router.pushController(MangaController(history).withFadeTransaction()) + }, + onClickResume = { history -> + presenter.getNextChapterForManga(history.mangaId, history.chapterId) + }, + onClickDelete = { history, all -> + if (all) { + // Reset last read of chapter to 0L + presenter.removeAllFromHistory(history.mangaId) + } else { + // Remove all chapters belonging to manga from library + presenter.removeFromHistory(history) + } + }, + ) } override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { @@ -201,46 +61,33 @@ class HistoryController : searchView.clearFocus() } searchView.queryTextChanges() - .drop(1) // Drop first event after subscribed .filter { router.backstack.lastOrNull()?.controller == this } .onEach { query = it.toString() - presenter.updateList(query) + presenter.search(query) } .launchIn(viewScope) - - // Fixes problem with the overflow icon showing up in lieu of search - searchItem.fixExpand( - onExpand = { invalidateMenuOnExpand() }, - ) } override fun onOptionsItemSelected(item: MenuItem): Boolean { - when (item.itemId) { + return when (item.itemId) { R.id.action_clear_history -> { - val ctrl = ClearHistoryDialogController() - ctrl.targetController = this@HistoryController - ctrl.showDialog(router) + val dialog = ClearHistoryDialogController() + dialog.targetController = this@HistoryController + dialog.showDialog(router) + true } - } - - return super.onOptionsItemSelected(item) - } - - class ClearHistoryDialogController : DialogController() { - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - return MaterialAlertDialogBuilder(activity!!) - .setMessage(R.string.clear_history_confirmation) - .setPositiveButton(android.R.string.ok) { _, _ -> - (targetController as? HistoryController)?.clearHistory() - } - .setNegativeButton(android.R.string.cancel, null) - .create() + else -> super.onOptionsItemSelected(item) } } - private fun clearHistory() { - db.deleteHistory().executeAsBlocking() - activity?.toast(R.string.clear_history_completed) + fun openChapter(chapter: Chapter?) { + val activity = activity ?: return + if (chapter != null) { + val intent = ReaderActivity.newIntent(activity, chapter.mangaId, chapter.id) + startActivity(intent) + } else { + activity.toast(R.string.no_next_chapter) + } } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt deleted file mode 100644 index 8164e5cc8..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryHolder.kt +++ /dev/null @@ -1,71 +0,0 @@ -package eu.kanade.tachiyomi.ui.recent.history - -import android.view.View -import coil.dispose -import coil.load -import eu.davidea.viewholders.FlexibleViewHolder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.databinding.HistoryItemBinding -import eu.kanade.tachiyomi.util.lang.toTimestampString -import java.util.Date - -/** - * Holder that contains recent manga item - * Uses R.layout.item_recently_read. - * UI related actions should be called from here. - * - * @param view the inflated view for this holder. - * @param adapter the adapter handling this holder. - * @constructor creates a new recent chapter holder. - */ -class HistoryHolder( - view: View, - val adapter: HistoryAdapter, -) : FlexibleViewHolder(view, adapter) { - - private val binding = HistoryItemBinding.bind(view) - - init { - binding.holder.setOnClickListener { - adapter.itemClickListener.onItemClick(bindingAdapterPosition) - } - - binding.remove.setOnClickListener { - adapter.removeClickListener.onRemoveClick(bindingAdapterPosition) - } - - binding.resume.setOnClickListener { - adapter.resumeClickListener.onResumeClick(bindingAdapterPosition) - } - } - - /** - * Set values of view - * - * @param item item containing history information - */ - fun bind(item: MangaChapterHistory) { - // Retrieve objects - val (manga, chapter, history) = item - - // Set manga title - binding.mangaTitle.text = manga.title - - // Set chapter number + timestamp - if (chapter.chapter_number > -1f) { - val formattedNumber = adapter.decimalFormat.format(chapter.chapter_number.toDouble()) - binding.mangaSubtitle.text = itemView.context.getString( - R.string.recent_manga_time, - formattedNumber, - Date(history.last_read).toTimestampString(), - ) - } else { - binding.mangaSubtitle.text = Date(history.last_read).toTimestampString() - } - - // Set cover - binding.cover.dispose() - binding.cover.load(item.manga) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryItem.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryItem.kt deleted file mode 100644 index 58f9e0cc2..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryItem.kt +++ /dev/null @@ -1,42 +0,0 @@ -package eu.kanade.tachiyomi.ui.recent.history - -import android.view.View -import androidx.recyclerview.widget.RecyclerView -import eu.davidea.flexibleadapter.FlexibleAdapter -import eu.davidea.flexibleadapter.items.AbstractSectionableItem -import eu.davidea.flexibleadapter.items.IFlexible -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory -import eu.kanade.tachiyomi.ui.recent.DateSectionItem - -class HistoryItem(val mch: MangaChapterHistory, header: DateSectionItem) : - AbstractSectionableItem(header) { - - override fun getLayoutRes(): Int { - return R.layout.history_item - } - - override fun createViewHolder(view: View, adapter: FlexibleAdapter>): HistoryHolder { - return HistoryHolder(view, adapter as HistoryAdapter) - } - - override fun bindViewHolder( - adapter: FlexibleAdapter>, - holder: HistoryHolder, - position: Int, - payloads: List?, - ) { - holder.bind(mch) - } - - override fun equals(other: Any?): Boolean { - if (other is HistoryItem) { - return mch.manga.id == other.mch.manga.id - } - return false - } - - override fun hashCode(): Int { - return mch.manga.id!!.hashCode() - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt index e8feb084d..7fd31dfa5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/HistoryPresenter.kt @@ -1,157 +1,127 @@ package eu.kanade.tachiyomi.ui.recent.history import android.os.Bundle -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.models.MangaChapterHistory -import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import androidx.paging.PagingData +import androidx.paging.cachedIn +import androidx.paging.insertSeparators +import androidx.paging.map +import eu.kanade.domain.history.interactor.DeleteHistoryTable +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.ui.base.presenter.BasePresenter -import eu.kanade.tachiyomi.ui.recent.DateSectionItem +import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.toDateKey -import rx.Observable -import rx.Subscription -import rx.android.schedulers.AndroidSchedulers -import uy.kohesive.injekt.injectLazy -import java.text.DateFormat -import java.util.Calendar +import eu.kanade.tachiyomi.util.system.toast +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.update +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.util.Date -import java.util.TreeMap /** * Presenter of HistoryFragment. * Contains information and data for fragment. * Observable updates should be called from here. */ -class HistoryPresenter : BasePresenter() { +class HistoryPresenter( + private val getHistory: GetHistory = Injekt.get(), + private val getNextChapterForManga: GetNextChapterForManga = Injekt.get(), + private val deleteHistoryTable: DeleteHistoryTable = Injekt.get(), + private val removeHistoryById: RemoveHistoryById = Injekt.get(), + private val removeHistoryByMangaId: RemoveHistoryByMangaId = Injekt.get(), +) : BasePresenter() { - private val db: DatabaseHelper by injectLazy() - private val preferences: PreferencesHelper by injectLazy() - - private val relativeTime: Int = preferences.relativeTime().get() - private val dateFormat: DateFormat = preferences.dateFormat() - - private var recentMangaSubscription: Subscription? = null + private var _query: MutableStateFlow = MutableStateFlow("") + private var _state: MutableStateFlow = MutableStateFlow(HistoryState.EMPTY) + val state: StateFlow = _state override fun onCreate(savedState: Bundle?) { super.onCreate(savedState) - // Used to get a list of recently read manga - updateList() - } - - fun requestNext(offset: Int, search: String = "") { - getRecentMangaObservable(offset = offset, search = search) - .subscribeLatestCache( - { view, mangas -> - view.onNextManga(mangas) - }, - HistoryController::onAddPageError, - ) - } - - /** - * Get recent manga observable - * @return list of history - */ - private fun getRecentMangaObservable(limit: Int = 25, offset: Int = 0, search: String = ""): Observable> { - // Set date limit for recent manga - val cal = Calendar.getInstance().apply { - time = Date() - add(Calendar.YEAR, -50) - } - - return db.getRecentManga(cal.time, limit, offset, search).asRxObservable() - .map { recents -> - val map = TreeMap> { d1, d2 -> d2.compareTo(d1) } - val byDay = recents - .groupByTo(map) { it.history.last_read.toDateKey() } - byDay.flatMap { entry -> - val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat) - entry.value.map { HistoryItem(it, dateItem) } - } - } - .observeOn(AndroidSchedulers.mainThread()) - } - - /** - * Reset last read of chapter to 0L - * @param history history belonging to chapter - */ - fun removeFromHistory(history: History) { - history.last_read = 0L - db.updateHistoryLastRead(history).asRxObservable() - .subscribe() - } - - /** - * Pull a list of history from the db - * @param search a search query to use for filtering - */ - fun updateList(search: String = "") { - recentMangaSubscription?.unsubscribe() - recentMangaSubscription = getRecentMangaObservable(search = search) - .subscribeLatestCache( - { view, mangas -> - view.onNextManga(mangas, true) - }, - HistoryController::onAddPageError, - ) - } - - /** - * Removes all chapters belonging to manga from history. - * @param mangaId id of manga - */ - fun removeAllFromHistory(mangaId: Long) { - db.getHistoryByMangaId(mangaId).asRxSingle() - .map { list -> - list.forEach { it.last_read = 0L } - db.updateHistoryLastRead(list).executeAsBlocking() - } - .subscribe() - } - - /** - * Retrieves the next chapter of the given one. - * - * @param chapter the chapter of the history object. - * @param manga the manga of the chapter. - */ - fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? { - if (!chapter.read) { - return 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 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 + presenterScope.launchIO { + _state.update { state -> + state.copy( + list = _query.flatMapLatest { query -> + getHistory.subscribe(query) + .map { pagingData -> + pagingData + .map { + UiModel.Item(it) + } + .insertSeparators { before, after -> + val beforeDate = before?.item?.readAt?.time?.toDateKey() ?: Date(0) + val afterDate = after?.item?.readAt?.time?.toDateKey() ?: Date(0) + when { + beforeDate.time != afterDate.time && afterDate.time != 0L -> UiModel.Header(afterDate) + // Return null to avoid adding a separator between two items. + else -> null + } + } + } } + .cachedIn(presenterScope), + ) } - Manga.CHAPTER_SORTING_UPLOAD_DATE -> { - chapters.drop(currChapterIndex + 1) - .firstOrNull { it.date_upload >= chapter.date_upload } + } + } + + fun search(query: String) { + presenterScope.launchIO { + _query.emit(query) + } + } + + fun removeFromHistory(history: HistoryWithRelations) { + presenterScope.launchIO { + removeHistoryById.await(history) + } + } + + fun removeAllFromHistory(mangaId: Long) { + presenterScope.launchIO { + removeHistoryByMangaId.await(mangaId) + } + } + + fun getNextChapterForManga(mangaId: Long, chapterId: Long) { + presenterScope.launchIO { + val chapter = getNextChapterForManga.await(mangaId, chapterId) + launchUI { + view?.openChapter(chapter) + } + } + } + + fun deleteAllHistory() { + presenterScope.launchIO { + val result = deleteHistoryTable.await() + if (!result) return@launchIO + launchUI { + view?.activity?.toast(R.string.clear_history_completed) } - else -> throw NotImplementedError("Unknown sorting method") } } } + +sealed class UiModel { + data class Item(val item: HistoryWithRelations) : UiModel() + data class Header(val date: Date) : UiModel() +} + +data class HistoryState( + val list: Flow>? = null, +) { + + companion object { + val EMPTY = HistoryState(null) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/RemoveHistoryDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/RemoveHistoryDialog.kt deleted file mode 100644 index 6243ed1d8..000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/history/RemoveHistoryDialog.kt +++ /dev/null @@ -1,54 +0,0 @@ -package eu.kanade.tachiyomi.ui.recent.history - -import android.app.Dialog -import android.os.Bundle -import com.bluelinelabs.conductor.Controller -import com.google.android.material.dialog.MaterialAlertDialogBuilder -import eu.kanade.tachiyomi.R -import eu.kanade.tachiyomi.data.database.models.History -import eu.kanade.tachiyomi.data.database.models.Manga -import eu.kanade.tachiyomi.ui.base.controller.DialogController -import eu.kanade.tachiyomi.widget.DialogCheckboxView - -class RemoveHistoryDialog(bundle: Bundle? = null) : DialogController(bundle) - where T : Controller, T : RemoveHistoryDialog.Listener { - - private var manga: Manga? = null - - private var history: History? = null - - constructor(target: T, manga: Manga, history: History) : this() { - this.manga = manga - this.history = history - targetController = target - } - - override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val activity = activity!! - - // Create custom view - val dialogCheckboxView = DialogCheckboxView(activity).apply { - setDescription(R.string.dialog_with_checkbox_remove_description) - setOptionDescription(R.string.dialog_with_checkbox_reset) - } - - return MaterialAlertDialogBuilder(activity) - .setTitle(R.string.action_remove) - .setView(dialogCheckboxView) - .setPositiveButton(R.string.action_remove) { _, _ -> onPositive(dialogCheckboxView.isChecked()) } - .setNegativeButton(android.R.string.cancel, null) - .create() - } - - private fun onPositive(checked: Boolean) { - val target = targetController as? Listener ?: return - val manga = manga ?: return - val history = history ?: return - - target.removeHistory(manga, history, checked) - } - - interface Listener { - fun removeHistory(manga: Manga, history: History, all: Boolean) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt index e9d62c6b3..626a410ab 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabaseController.kt @@ -37,7 +37,6 @@ class ClearDatabaseController : private var menu: Menu? = null private var actionFab: ExtendedFloatingActionButton? = null - private var actionFabScrollListener: RecyclerView.OnScrollListener? = null init { setHasOptionsMenu(true) @@ -143,7 +142,6 @@ class ClearDatabaseController : override fun cleanupFab(fab: ExtendedFloatingActionButton) { actionFab?.setOnClickListener(null) - actionFabScrollListener?.let { recycler?.removeOnScrollListener(it) } actionFab = null } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt index 0f796ee12..e0c694a15 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt @@ -1,6 +1,7 @@ package eu.kanade.tachiyomi.ui.setting.database import android.os.Bundle +import eu.kanade.tachiyomi.Database import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter @@ -13,6 +14,7 @@ import uy.kohesive.injekt.api.get class ClearDatabasePresenter : BasePresenter() { private val db = Injekt.get() + private val database = Injekt.get() private val sourceManager = Injekt.get() @@ -32,7 +34,7 @@ class ClearDatabasePresenter : BasePresenter() { db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking() } // SY <-- - db.deleteHistoryNoLastRead().executeAsBlocking() + database.historyQueries.removeResettedHistory() } private fun getDatabaseSourcesObservable(): Observable> { diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiCoordinatorLayout.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiCoordinatorLayout.kt index e7d7db22c..16b1c96a0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiCoordinatorLayout.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/TachiyomiCoordinatorLayout.kt @@ -5,8 +5,10 @@ import android.os.Parcel import android.os.Parcelable import android.util.AttributeSet import android.view.View +import androidx.compose.ui.platform.ComposeView import androidx.coordinatorlayout.R import androidx.coordinatorlayout.widget.CoordinatorLayout +import androidx.core.view.ViewCompat import androidx.core.view.doOnLayout import androidx.core.view.isVisible import androidx.customview.view.AbsSavedState @@ -63,7 +65,16 @@ class TachiyomiCoordinatorLayout @JvmOverloads constructor( super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed) // Disable elevation overlay when tabs are visible if (canLiftAppBarOnScroll) { - appBarLayout?.isLifted = (dyConsumed != 0 || dyUnconsumed >= 0) && tabLayout?.isVisible == false + if (target is ComposeView) { + val scrollCondition = if (type == ViewCompat.TYPE_NON_TOUCH) { + dyUnconsumed >= 0 + } else { + dyConsumed != 0 || dyUnconsumed >= 0 + } + appBarLayout?.isLifted = scrollCondition && tabLayout?.isVisible == false + } else { + appBarLayout?.isLifted = (dyConsumed != 0 || dyUnconsumed >= 0) && tabLayout?.isVisible == false + } } } diff --git a/app/src/main/java/exh/EXHMigrations.kt b/app/src/main/java/exh/EXHMigrations.kt index 8af8f23b0..5cab95914 100644 --- a/app/src/main/java/exh/EXHMigrations.kt +++ b/app/src/main/java/exh/EXHMigrations.kt @@ -6,6 +6,7 @@ import androidx.preference.PreferenceManager import com.pushtorefresh.storio.sqlite.queries.DeleteQuery import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.RawQuery +import eu.kanade.data.DatabaseHandler import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.database.DatabaseHelper @@ -40,8 +41,6 @@ import exh.eh.EHentaiUpdateWorker import exh.log.xLogE import exh.log.xLogW import exh.merged.sql.models.MergedMangaReference -import exh.savedsearches.models.FeedSavedSearch -import exh.savedsearches.models.SavedSearch import exh.source.BlacklistedSources import exh.source.EH_SOURCE_ID import exh.source.HBROWSE_SOURCE_ID @@ -51,6 +50,7 @@ import exh.source.PERV_EDEN_IT_SOURCE_ID import exh.source.TSUMINO_SOURCE_ID import exh.util.nullIfBlank import exh.util.under +import kotlinx.coroutines.runBlocking import kotlinx.serialization.SerialName import kotlinx.serialization.Serializable import kotlinx.serialization.decodeFromString @@ -69,6 +69,7 @@ import java.net.URISyntaxException object EXHMigrations { private val db: DatabaseHelper by injectLazy() + private val database: DatabaseHandler by injectLazy() private val sourceManager: SourceManager by injectLazy() /** @@ -404,31 +405,31 @@ object EXHMigrations { BackupCreatorJob.setupTask(context) } if (oldVersion under 31) { - val savedSearches = prefs.getStringSet("eh_saved_searches", emptySet())?.mapNotNull { - kotlin.runCatching { - val content = Json.decodeFromString(it.substringAfter(':')) - SavedSearch( - id = null, - source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null, - content["name"]!!.jsonPrimitive.content, - content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(), - Json.encodeToString(content["filters"]!!.jsonArray), - ) - }.getOrNull() - }?.ifEmpty { null } - if (savedSearches != null) { - db.insertSavedSearches(savedSearches).executeAsBlocking() - } - val feed = prefs.getStringSet("latest_tab_sources", emptySet())?.map { - FeedSavedSearch( - id = null, - source = it.toLong(), - savedSearch = null, - global = true, - ) - }?.ifEmpty { null } - if (feed != null) { - db.insertFeedSavedSearches(feed).executeAsBlocking() + runBlocking { + database.await(true) { + prefs.getStringSet("eh_saved_searches", emptySet())?.forEach { + kotlin.runCatching { + val content = Json.decodeFromString(it.substringAfter(':')) + saved_searchQueries.insertSavedSearch( + _id = null, + source = it.substringBefore(':').toLongOrNull() ?: return@forEach, + name = content["name"]!!.jsonPrimitive.content, + query = content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(), + filters_json = Json.encodeToString(content["filters"]!!.jsonArray) + ) + } + } + } + database.await(true) { + prefs.getStringSet("latest_tab_sources", emptySet())?.forEach { + feed_saved_searchQueries.insertFeedSavedSearch( + _id = null, + source = it.toLong(), + saved_search = null, + global = true, + ) + } + } } prefs.edit(commit = true) { remove("eh_saved_searches") diff --git a/app/src/main/java/exh/debug/DebugFunctions.kt b/app/src/main/java/exh/debug/DebugFunctions.kt index 0892efec1..5aea14182 100644 --- a/app/src/main/java/exh/debug/DebugFunctions.kt +++ b/app/src/main/java/exh/debug/DebugFunctions.kt @@ -3,6 +3,7 @@ package exh.debug import android.app.Application import androidx.work.WorkManager import com.pushtorefresh.storio.sqlite.queries.RawQuery +import eu.kanade.data.DatabaseHandler import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.tables.MangaTable @@ -34,6 +35,7 @@ import java.util.UUID object DebugFunctions { val app: Application by injectLazy() val db: DatabaseHelper by injectLazy() + val database: DatabaseHandler by injectLazy() val prefs: PreferencesHelper by injectLazy() val sourceManager: SourceManager by injectLazy() @@ -164,7 +166,7 @@ object DebugFunctions { it.favorite && db.getSearchMetadataForManga(it.id!!).executeAsBlocking() == null } - fun clearSavedSearches() = db.deleteAllSavedSearches().executeAsBlocking() + fun clearSavedSearches() = runBlocking { database.await { saved_searchQueries.deleteAll() } } fun listAllSources() = sourceManager.getCatalogueSources().joinToString("\n") { "${it.id}: ${it.name} (${it.lang.uppercase()})" diff --git a/app/src/main/java/exh/favorites/sql/tables/FavoriteEntryTable.kt b/app/src/main/java/exh/favorites/sql/tables/FavoriteEntryTable.kt index e29e2914c..52b7b0f57 100644 --- a/app/src/main/java/exh/favorites/sql/tables/FavoriteEntryTable.kt +++ b/app/src/main/java/exh/favorites/sql/tables/FavoriteEntryTable.kt @@ -13,20 +13,4 @@ object FavoriteEntryTable { const val COL_TOKEN = "token" const val COL_CATEGORY = "category" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_TITLE TEXT NOT NULL, - $COL_GID TEXT NOT NULL, - $COL_TOKEN TEXT NOT NULL, - $COL_CATEGORY INTEGER NOT NULL - )""" - - val fixTableQuery: String - get() = createTableQuery.replace( - "CREATE TABLE", - "CREATE TABLE IF NOT EXISTS", - ) } diff --git a/app/src/main/java/exh/merged/sql/tables/MergedTable.kt b/app/src/main/java/exh/merged/sql/tables/MergedTable.kt index c54bf5cac..5648c188f 100644 --- a/app/src/main/java/exh/merged/sql/tables/MergedTable.kt +++ b/app/src/main/java/exh/merged/sql/tables/MergedTable.kt @@ -1,7 +1,5 @@ package exh.merged.sql.tables -import eu.kanade.tachiyomi.data.database.tables.MangaTable - object MergedTable { const val TABLE = "merged" @@ -27,30 +25,4 @@ object MergedTable { const val COL_MANGA_URL = "manga_url" const val COL_MANGA_SOURCE = "manga_source" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_IS_INFO_MANGA BOOLEAN NOT NULL, - $COL_GET_CHAPTER_UPDATES BOOLEAN NOT NULL, - $COL_CHAPTER_SORT_MODE INTEGER NOT NULL, - $COL_CHAPTER_PRIORITY INTEGER NOT NULL, - $COL_DOWNLOAD_CHAPTERS BOOLEAN NOT NULL, - $COL_MERGE_ID INTEGER NOT NULL, - $COL_MERGE_URL TEXT NOT NULL, - $COL_MANGA_ID INTEGER, - $COL_MANGA_URL TEXT NOT NULL, - $COL_MANGA_SOURCE INTEGER NOT NULL, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE SET NULL, - FOREIGN KEY($COL_MERGE_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" - - val dropTableQuery: String - get() = "DROP TABLE $TABLE" - - val createIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_MERGE_ID}_index ON $TABLE($COL_MERGE_ID)" } diff --git a/app/src/main/java/exh/metadata/sql/tables/SearchMetadataTable.kt b/app/src/main/java/exh/metadata/sql/tables/SearchMetadataTable.kt index 434f52f65..2196883c0 100755 --- a/app/src/main/java/exh/metadata/sql/tables/SearchMetadataTable.kt +++ b/app/src/main/java/exh/metadata/sql/tables/SearchMetadataTable.kt @@ -1,7 +1,5 @@ package exh.metadata.sql.tables -import eu.kanade.tachiyomi.data.database.tables.MangaTable - object SearchMetadataTable { const val TABLE = "search_metadata" @@ -14,23 +12,4 @@ object SearchMetadataTable { const val COL_INDEXED_EXTRA = "indexed_extra" const val COL_EXTRA_VERSION = "extra_version" - - // Insane foreign, primary key to avoid touch manga table - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_MANGA_ID INTEGER NOT NULL PRIMARY KEY, - $COL_UPLOADER TEXT, - $COL_EXTRA TEXT NOT NULL, - $COL_INDEXED_EXTRA TEXT, - $COL_EXTRA_VERSION INT NOT NULL, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" - - val createUploaderIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_UPLOADER}_index ON $TABLE($COL_UPLOADER)" - - val createIndexedExtraIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_INDEXED_EXTRA}_index ON $TABLE($COL_INDEXED_EXTRA)" } diff --git a/app/src/main/java/exh/metadata/sql/tables/SearchTagTable.kt b/app/src/main/java/exh/metadata/sql/tables/SearchTagTable.kt index a9b3e72c3..a7aabc7ac 100755 --- a/app/src/main/java/exh/metadata/sql/tables/SearchTagTable.kt +++ b/app/src/main/java/exh/metadata/sql/tables/SearchTagTable.kt @@ -1,7 +1,5 @@ package exh.metadata.sql.tables -import eu.kanade.tachiyomi.data.database.tables.MangaTable - object SearchTagTable { const val TABLE = "search_tags" @@ -14,22 +12,4 @@ object SearchTagTable { const val COL_NAME = "name" const val COL_TYPE = "type" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_MANGA_ID INTEGER NOT NULL, - $COL_NAMESPACE TEXT, - $COL_NAME TEXT NOT NULL, - $COL_TYPE INT NOT NULL, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" - - val createMangaIdIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)" - - val createNamespaceNameIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_NAMESPACE}_${COL_NAME}_index ON $TABLE($COL_NAMESPACE, $COL_NAME)" } diff --git a/app/src/main/java/exh/metadata/sql/tables/SearchTitleTable.kt b/app/src/main/java/exh/metadata/sql/tables/SearchTitleTable.kt index 48b71e422..98e383296 100755 --- a/app/src/main/java/exh/metadata/sql/tables/SearchTitleTable.kt +++ b/app/src/main/java/exh/metadata/sql/tables/SearchTitleTable.kt @@ -1,7 +1,5 @@ package exh.metadata.sql.tables -import eu.kanade.tachiyomi.data.database.tables.MangaTable - object SearchTitleTable { const val TABLE = "search_titles" @@ -12,21 +10,4 @@ object SearchTitleTable { const val COL_TITLE = "title" const val COL_TYPE = "type" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_MANGA_ID INTEGER NOT NULL, - $COL_TITLE TEXT NOT NULL, - $COL_TYPE INT NOT NULL, - FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID}) - ON DELETE CASCADE - )""" - - val createMangaIdIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)" - - val createTitleIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_TITLE}_index ON $TABLE($COL_TITLE)" } diff --git a/app/src/main/java/exh/savedsearches/queries/FeedSavedSearchQueries.kt b/app/src/main/java/exh/savedsearches/queries/FeedSavedSearchQueries.kt index fb67ddfce..fd1db5461 100644 --- a/app/src/main/java/exh/savedsearches/queries/FeedSavedSearchQueries.kt +++ b/app/src/main/java/exh/savedsearches/queries/FeedSavedSearchQueries.kt @@ -1,86 +1,5 @@ package exh.savedsearches.queries -import com.pushtorefresh.storio.sqlite.queries.DeleteQuery -import com.pushtorefresh.storio.sqlite.queries.Query -import com.pushtorefresh.storio.sqlite.queries.RawQuery import eu.kanade.tachiyomi.data.database.DbProvider -import eu.kanade.tachiyomi.data.database.queries.getGlobalFeedSavedSearchQuery -import eu.kanade.tachiyomi.data.database.queries.getSourceFeedSavedSearchQuery -import exh.savedsearches.models.FeedSavedSearch -import exh.savedsearches.models.SavedSearch -import exh.savedsearches.tables.FeedSavedSearchTable -interface FeedSavedSearchQueries : DbProvider { - fun getGlobalFeedSavedSearches() = db.get() - .listOfObjects(FeedSavedSearch::class.java) - .withQuery( - Query.builder() - .table(FeedSavedSearchTable.TABLE) - .where("${FeedSavedSearchTable.COL_GLOBAL} = 1") - .orderBy(FeedSavedSearchTable.COL_ID) - .build(), - ) - .prepare() - - fun getSourceFeedSavedSearches(sourceId: Long) = db.get() - .listOfObjects(FeedSavedSearch::class.java) - .withQuery( - Query.builder() - .table(FeedSavedSearchTable.TABLE) - .where("${FeedSavedSearchTable.COL_SOURCE} = ? AND ${FeedSavedSearchTable.COL_GLOBAL} = 0") - .whereArgs(sourceId) - .orderBy(FeedSavedSearchTable.COL_ID) - .build(), - ) - .prepare() - - fun insertFeedSavedSearch(savedSearch: FeedSavedSearch) = db.put().`object`(savedSearch).prepare() - - fun insertFeedSavedSearches(savedSearches: List) = db.put().objects(savedSearches).prepare() - - fun deleteFeedSavedSearch(savedSearch: FeedSavedSearch) = db.delete().`object`(savedSearch).prepare() - - fun deleteFeedSavedSearch(id: Long) = db.delete() - .byQuery( - DeleteQuery.builder() - .table(FeedSavedSearchTable.TABLE) - .where("${FeedSavedSearchTable.COL_ID} = ?") - .whereArgs(id) - .build(), - ).prepare() - - fun deleteAllFeedSavedSearches() = db.delete().byQuery( - DeleteQuery.builder() - .table(FeedSavedSearchTable.TABLE) - .build(), - ) - .prepare() - - fun getGlobalSavedSearchesFeed() = db.get() - .listOfObjects(SavedSearch::class.java) - .withQuery( - RawQuery.builder() - .query(getGlobalFeedSavedSearchQuery()) - .build(), - ) - .prepare() - - fun getSourceSavedSearchesFeed(sourceId: Long) = db.get() - .listOfObjects(SavedSearch::class.java) - .withQuery( - RawQuery.builder() - .query(getSourceFeedSavedSearchQuery()) - .args(sourceId) - .build(), - ) - .prepare() - - /*fun setMangasForMergedManga(mergedMangaId: Long, mergedMangases: List) { - db.inTransaction { - deleteSavedSearches(mergedMangaId).executeAsBlocking() - mergedMangases.chunked(100) { chunk -> - insertSavedSearches(chunk).executeAsBlocking() - } - } - }*/ -} +interface FeedSavedSearchQueries : DbProvider diff --git a/app/src/main/java/exh/savedsearches/queries/SavedSearchQueries.kt b/app/src/main/java/exh/savedsearches/queries/SavedSearchQueries.kt index 558c80c69..34d9f616e 100644 --- a/app/src/main/java/exh/savedsearches/queries/SavedSearchQueries.kt +++ b/app/src/main/java/exh/savedsearches/queries/SavedSearchQueries.kt @@ -1,93 +1,5 @@ package exh.savedsearches.queries -import com.pushtorefresh.storio.sqlite.queries.DeleteQuery -import com.pushtorefresh.storio.sqlite.queries.Query import eu.kanade.tachiyomi.data.database.DbProvider -import exh.savedsearches.models.SavedSearch -import exh.savedsearches.tables.SavedSearchTable -interface SavedSearchQueries : DbProvider { - fun getSavedSearches(source: Long) = db.get() - .listOfObjects(SavedSearch::class.java) - .withQuery( - Query.builder() - .table(SavedSearchTable.TABLE) - .where("${SavedSearchTable.COL_SOURCE} = ?") - .whereArgs(source) - .build(), - ) - .prepare() - - fun deleteSavedSearches(source: Long) = db.delete() - .byQuery( - DeleteQuery.builder() - .table(SavedSearchTable.TABLE) - .where("${SavedSearchTable.COL_SOURCE} = ?") - .whereArgs(source) - .build(), - ) - .prepare() - - fun getSavedSearches() = db.get() - .listOfObjects(SavedSearch::class.java) - .withQuery( - Query.builder() - .table(SavedSearchTable.TABLE) - .orderBy(SavedSearchTable.COL_ID) - .build(), - ) - .prepare() - - fun getSavedSearch(id: Long) = db.get() - .`object`(SavedSearch::class.java) - .withQuery( - Query.builder() - .table(SavedSearchTable.TABLE) - .where("${SavedSearchTable.COL_ID} = ?") - .whereArgs(id) - .build(), - ) - .prepare() - - fun getSavedSearches(ids: List) = db.get() - .listOfObjects(SavedSearch::class.java) - .withQuery( - Query.builder() - .table(SavedSearchTable.TABLE) - .where("${SavedSearchTable.COL_ID} IN (?)") - .whereArgs(ids.joinToString()) - .build(), - ) - .prepare() - - fun insertSavedSearch(savedSearch: SavedSearch) = db.put().`object`(savedSearch).prepare() - - fun insertSavedSearches(savedSearches: List) = db.put().objects(savedSearches).prepare() - - fun deleteSavedSearch(savedSearch: SavedSearch) = db.delete().`object`(savedSearch).prepare() - - fun deleteSavedSearch(id: Long) = db.delete() - .byQuery( - DeleteQuery.builder() - .table(SavedSearchTable.TABLE) - .where("${SavedSearchTable.COL_ID} = ?") - .whereArgs(id) - .build(), - ).prepare() - - fun deleteAllSavedSearches() = db.delete().byQuery( - DeleteQuery.builder() - .table(SavedSearchTable.TABLE) - .build(), - ) - .prepare() - - /*fun setMangasForMergedManga(mergedMangaId: Long, mergedMangases: List) { - db.inTransaction { - deleteSavedSearches(mergedMangaId).executeAsBlocking() - mergedMangases.chunked(100) { chunk -> - insertSavedSearches(chunk).executeAsBlocking() - } - } - }*/ -} +interface SavedSearchQueries : DbProvider diff --git a/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt b/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt index 6b6f5b631..b23dd1d8d 100644 --- a/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt +++ b/app/src/main/java/exh/savedsearches/tables/FeedSavedSearchTable.kt @@ -11,18 +11,4 @@ object FeedSavedSearchTable { const val COL_SAVED_SEARCH_ID = "saved_search" const val COL_GLOBAL = "global" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_SOURCE INTEGER NOT NULL, - $COL_SAVED_SEARCH_ID INTEGER, - $COL_GLOBAL BOOLEAN NOT NULL, - FOREIGN KEY($COL_SAVED_SEARCH_ID) REFERENCES ${SavedSearchTable.TABLE} (${SavedSearchTable.COL_ID}) - ON DELETE CASCADE - )""" - - val createSavedSearchIdIndexQuery: String - get() = "CREATE INDEX ${TABLE}_${COL_SAVED_SEARCH_ID}_index ON $TABLE($COL_SAVED_SEARCH_ID)" } diff --git a/app/src/main/java/exh/savedsearches/tables/SavedSearchTable.kt b/app/src/main/java/exh/savedsearches/tables/SavedSearchTable.kt index 3a345fb05..35e1839b7 100644 --- a/app/src/main/java/exh/savedsearches/tables/SavedSearchTable.kt +++ b/app/src/main/java/exh/savedsearches/tables/SavedSearchTable.kt @@ -13,14 +13,4 @@ object SavedSearchTable { const val COL_QUERY = "query" const val COL_FILTERS_JSON = "filters_json" - - val createTableQuery: String - get() = - """CREATE TABLE $TABLE( - $COL_ID INTEGER NOT NULL PRIMARY KEY, - $COL_SOURCE INTEGER NOT NULL, - $COL_NAME TEXT NOT NULL, - $COL_QUERY TEXT, - $COL_FILTERS_JSON TEXT - )""" } diff --git a/app/src/main/res/layout/compose_controller.xml b/app/src/main/res/layout/compose_controller.xml new file mode 100644 index 000000000..617287296 --- /dev/null +++ b/app/src/main/res/layout/compose_controller.xml @@ -0,0 +1,4 @@ + + diff --git a/app/src/main/res/layout/history_controller.xml b/app/src/main/res/layout/history_controller.xml deleted file mode 100644 index d33aa20ed..000000000 --- a/app/src/main/res/layout/history_controller.xml +++ /dev/null @@ -1,33 +0,0 @@ - - - - - - - - - - diff --git a/app/src/main/res/layout/history_item.xml b/app/src/main/res/layout/history_item.xml deleted file mode 100644 index ab407a01b..000000000 --- a/app/src/main/res/layout/history_item.xml +++ /dev/null @@ -1,85 +0,0 @@ - - - - - - - - - - - - - - - - - - diff --git a/app/src/main/sqldelight/data/categories.sq b/app/src/main/sqldelight/data/categories.sq new file mode 100644 index 000000000..af3ea448f --- /dev/null +++ b/app/src/main/sqldelight/data/categories.sq @@ -0,0 +1,7 @@ +CREATE TABLE categories( + _id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + sort INTEGER NOT NULL, + flags INTEGER NOT NULL, + manga_order TEXT NOT NULL +); \ No newline at end of file diff --git a/app/src/main/sqldelight/data/chapters.sq b/app/src/main/sqldelight/data/chapters.sq new file mode 100644 index 000000000..337b1163c --- /dev/null +++ b/app/src/main/sqldelight/data/chapters.sq @@ -0,0 +1,29 @@ +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 +); + +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; + +getChapterById: +SELECT * +FROM chapters +WHERE _id = :id; + +getChapterByMangaId: +SELECT * +FROM chapters +WHERE manga_id = :mangaId; \ No newline at end of file diff --git a/app/src/main/sqldelight/data/eh_favorites.sq b/app/src/main/sqldelight/data/eh_favorites.sq new file mode 100644 index 000000000..e3b7cace6 --- /dev/null +++ b/app/src/main/sqldelight/data/eh_favorites.sq @@ -0,0 +1,16 @@ +CREATE TABLE eh_favorites ( + _id INTEGER NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + gid TEXT NOT NULL, + token TEXT NOT NULL, + category INTEGER NOT NULL +); + +selectAll: +SELECT * FROM eh_favorites; + +insertEhFavorites: +INSERT INTO eh_favorites (_id, title, gid, token, category) VALUES (?, ?, ?, ?, ?); + +deleteAll: +DELETE FROM eh_favorites; \ No newline at end of file diff --git a/app/src/main/sqldelight/data/feed_saved_search.sq b/app/src/main/sqldelight/data/feed_saved_search.sq new file mode 100644 index 000000000..085ebf829 --- /dev/null +++ b/app/src/main/sqldelight/data/feed_saved_search.sq @@ -0,0 +1,41 @@ +CREATE TABLE feed_saved_search ( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + saved_search INTEGER, + global INTEGER AS Boolean NOT NULL, + FOREIGN KEY(saved_search) REFERENCES saved_search (_id) + ON DELETE CASCADE +); + +CREATE INDEX feed_saved_search_saved_search_index ON feed_saved_search(saved_search); + +selectAllGlobal: +SELECT * FROM feed_saved_search WHERE global = 1; + +selectBySource: +SELECT * FROM feed_saved_search WHERE source = ? AND global = 0; + +insertFeedSavedSearch: +INSERT INTO feed_saved_search (_id, source, saved_search, global) VALUES (?, ?, ?, ?); + +deleteById: +DELETE FROM feed_saved_search WHERE _id = ?; + +deleteAll: +DELETE FROM feed_saved_search; + +selectGlobalFeedSavedSearch: +SELECT saved_search.* +FROM ( + SELECT saved_search FROM feed_saved_search WHERE global = 1 +) AS M +JOIN saved_search +ON saved_search._id = M.saved_search; + +selectSourceFeedSavedSearch: +SELECT saved_search.* +FROM ( + SELECT saved_search FROM feed_saved_search WHERE global = 0 AND source = ? +) AS M +JOIN saved_search +ON saved_search._id = M.saved_search; \ No newline at end of file diff --git a/app/src/main/sqldelight/data/history.sq b/app/src/main/sqldelight/data/history.sq new file mode 100644 index 000000000..a798b325a --- /dev/null +++ b/app/src/main/sqldelight/data/history.sq @@ -0,0 +1,37 @@ +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 +); + +CREATE INDEX history_history_chapter_id_index ON history(history_chapter_id); + +resetHistoryById: +UPDATE history +SET history_last_read = 0 +WHERE history_id = :historyId; + +resetHistoryByMangaId: +UPDATE history +SET history_last_read = 0 +WHERE history_id IN ( + SELECT H.history_id + FROM mangas M + INNER JOIN chapters C + ON M._id = C.manga_id + INNER JOIN history H + ON C._id = H.history_chapter_id + WHERE M._id = :mangaId +); + +removeAllHistory: +DELETE FROM history; + +removeResettedHistory: +DELETE FROM history +WHERE history_last_read = 0; diff --git a/app/src/main/sqldelight/data/manga_sync.sq b/app/src/main/sqldelight/data/manga_sync.sq new file mode 100644 index 000000000..dcd18442b --- /dev/null +++ b/app/src/main/sqldelight/data/manga_sync.sq @@ -0,0 +1,18 @@ +CREATE TABLE manga_sync( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + sync_id INTEGER NOT NULL, + remote_id INTEGER NOT NULL, + library_id INTEGER, + title TEXT NOT NULL, + last_chapter_read REAL NOT NULL, + total_chapters INTEGER NOT NULL, + status INTEGER NOT NULL, + score REAL AS Float NOT NULL, + remote_url TEXT NOT NULL, + start_date INTEGER AS Long NOT NULL, + finish_date INTEGER AS Long NOT NULL, + UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); \ No newline at end of file diff --git a/app/src/main/sqldelight/data/mangas.sq b/app/src/main/sqldelight/data/mangas.sq new file mode 100644 index 000000000..f085ba170 --- /dev/null +++ b/app/src/main/sqldelight/data/mangas.sq @@ -0,0 +1,32 @@ +import java.lang.String; +import kotlin.collections.List; + +CREATE TABLE mangas( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + url TEXT NOT NULL, + artist TEXT, + author TEXT, + description TEXT, + genre TEXT AS List, + title TEXT NOT NULL, + status INTEGER NOT NULL, + thumbnail_url TEXT, + favorite INTEGER AS Boolean NOT NULL, + last_update INTEGER AS Long, + next_update INTEGER AS Long, + initialized INTEGER AS Boolean NOT NULL, + viewer INTEGER NOT NULL, + chapter_flags INTEGER NOT NULL, + cover_last_modified INTEGER AS Long NOT NULL, + date_added INTEGER AS Long NOT NULL, + filtered_scanlators TEXT +); + +CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; +CREATE INDEX mangas_url_index ON mangas(url); + +getMangaById: +SELECT * +FROM mangas +WHERE _id = :id; \ No newline at end of file diff --git a/app/src/main/sqldelight/data/mangas_categories.sq b/app/src/main/sqldelight/data/mangas_categories.sq new file mode 100644 index 000000000..6db91fe16 --- /dev/null +++ b/app/src/main/sqldelight/data/mangas_categories.sq @@ -0,0 +1,9 @@ +CREATE TABLE mangas_categories( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + FOREIGN KEY(category_id) REFERENCES categories (_id) + ON DELETE CASCADE, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); \ No newline at end of file diff --git a/app/src/main/sqldelight/data/merged.sq b/app/src/main/sqldelight/data/merged.sq new file mode 100644 index 000000000..4abf0ae15 --- /dev/null +++ b/app/src/main/sqldelight/data/merged.sq @@ -0,0 +1,85 @@ +CREATE TABLE merged( + _id INTEGER NOT NULL PRIMARY KEY, + info_manga INTEGER AS Boolean NOT NULL, + get_chapter_updates INTEGER AS Boolean NOT NULL, + chapter_sort_mode INTEGER NOT NULL, + chapter_priority INTEGER NOT NULL, + download_chapters INTEGER AS Boolean NOT NULL, + merge_id INTEGER NOT NULL, + merge_url TEXT NOT NULL, + manga_id INTEGER, + manga_url TEXT NOT NULL, + manga_source INTEGER NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE SET NULL, + FOREIGN KEY(merge_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +CREATE INDEX merged_merge_id_index ON merged(merge_id); + +selectByMergeId: +SELECT * FROM merged WHERE merge_id = ?; + +selectByMergeUrl: +SELECT * FROM merged WHERE merge_url = ?; + +deleteByMergeId: +DELETE FROM merged WHERE merge_id = ?; + +selectMergedMangasById: +SELECT mangas.* +FROM ( + SELECT manga_id FROM merged WHERE merge_id = ? +) AS M +JOIN mangas +ON mangas._id = M.manga_id; + +selectMergedMangasByUrl: +SELECT mangas.* +FROM ( + SELECT manga_id FROM merged WHERE merge_url = ? +) AS M +JOIN mangas +ON mangas._id = M.manga_id; + +selectAllMergedMangas: +SELECT mangas.* +FROM ( + SELECT manga_id FROM merged +) AS M +JOIN mangas +ON mangas._id = M.manga_id; + +deleteByMergeUrl: +DELETE FROM merged WHERE merge_url = ?; + +selectAll: +SELECT * FROM merged; + +selectChaptersByMergedId: +SELECT chapters.* +FROM ( + SELECT manga_id FROM merged WHERE merge_id = ? +) AS M +JOIN chapters +ON chapters.manga_id = M.manga_id; + +insertMerged: +INSERT INTO merged (_id, info_manga, get_chapter_updates, chapter_sort_mode, chapter_priority, download_chapters, merge_id, merge_url, manga_id, manga_url, manga_source) +VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?); + +updateSettingsById: +UPDATE merged +SET + get_chapter_updates = ?, + download_chapters = ?, + info_manga = ?, + chapter_priority = ? +WHERE _id = ?; + +deleteById: +DELETE FROM merged WHERE _id = ?; + +deleteBy: +DELETE FROM merged; diff --git a/app/src/main/sqldelight/data/saved_search.sq b/app/src/main/sqldelight/data/saved_search.sq new file mode 100644 index 000000000..90cbf82b8 --- /dev/null +++ b/app/src/main/sqldelight/data/saved_search.sq @@ -0,0 +1,32 @@ +CREATE TABLE saved_search( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + name TEXT NOT NULL, + query TEXT, + filters_json TEXT +); + +selectBySource: +SELECT * FROM saved_search WHERE source = ?; + +deleteBySource: +DELETE FROM saved_search WHERE source = ?; + +selectAll: +SELECT * FROM saved_search; + +selectById: +SELECT * FROM saved_search WHERE _id = ?; + +selectByIds: +SELECT * FROM saved_search WHERE _id IN ?; + +insertSavedSearch: +INSERT INTO saved_search (_id, source, name, query, filters_json) +VALUES (?, ?, ?, ?, ?); + +deleteById: +DELETE FROM saved_search WHERE _id = ?; + +deleteAll: +DELETE FROM saved_search; \ No newline at end of file diff --git a/app/src/main/sqldelight/data/search_metadata.sq b/app/src/main/sqldelight/data/search_metadata.sq new file mode 100644 index 000000000..9a184a99f --- /dev/null +++ b/app/src/main/sqldelight/data/search_metadata.sq @@ -0,0 +1,32 @@ +CREATE TABLE search_metadata ( + manga_id INTEGER NOT NULL PRIMARY KEY, + uploader TEXT, + extra TEXT NOT NULL, + indexed_extra TEXT, + extra_version INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +CREATE INDEX search_metadata_uploader_index ON search_metadata(uploader); +CREATE INDEX search_metadata_indexed_extra_index ON search_metadata(indexed_extra); + +selectAll: +SELECT * FROM search_metadata; + +selectByMangaId: +SELECT * FROM search_metadata WHERE manga_id = ?; + +selectByIndexedExtra: +SELECT * FROM search_metadata WHERE indexed_extra = ?; + +insert: +INSERT INTO search_metadata (manga_id, uploader, extra, indexed_extra, extra_version) +VALUES (?, ?, ?, ?, ?); + +insertNew: +INSERT INTO search_metadata (manga_id, uploader, extra, indexed_extra, extra_version) +VALUES ?; + +deleteAll: +DELETE FROM search_metadata; \ No newline at end of file diff --git a/app/src/main/sqldelight/data/search_tags.sq b/app/src/main/sqldelight/data/search_tags.sq new file mode 100644 index 000000000..5459070cc --- /dev/null +++ b/app/src/main/sqldelight/data/search_tags.sq @@ -0,0 +1,35 @@ +CREATE TABLE search_tags ( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + namespace TEXT, + name TEXT NOT NULL, + type INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +CREATE INDEX search_tags_manga_id_index ON search_tags(manga_id); +CREATE INDEX search_tags_namespace_name_index ON search_tags(namespace, name); + + +selectByMangaId: +SELECT * FROM search_tags +WHERE manga_id = ?; + +deleteByManga: +DELETE FROM search_tags WHERE manga_id = ?; + +insert: +INSERT INTO search_tags (_id, manga_id, namespace, name, type) +VALUES (?, ?, ?, ?, ?); + +insertNew: +INSERT INTO search_tags (manga_id, namespace, name, type) +VALUES (?, ?, ?, ?); + +insertItem: +INSERT INTO search_tags (_id, manga_id, namespace, name, type) +VALUES ?; + +deleteAll: +DELETE FROM search_titles; \ No newline at end of file diff --git a/app/src/main/sqldelight/data/search_titles.sq b/app/src/main/sqldelight/data/search_titles.sq new file mode 100644 index 000000000..e4d1195bb --- /dev/null +++ b/app/src/main/sqldelight/data/search_titles.sq @@ -0,0 +1,30 @@ +CREATE TABLE search_titles ( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + title TEXT NOT NULL, + type INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); + +CREATE INDEX search_titles_manga_id_index ON search_titles(manga_id); +CREATE INDEX search_titles_title_index ON search_titles(title); + +selectByMangaId: +SELECT * FROM search_titles +WHERE manga_id = ?; + +deleteByManga: +DELETE FROM search_titles WHERE manga_id = ?; + +insert: +INSERT INTO search_titles (_id, manga_id, title, type) VALUES (?, ?, ?, ?); + +insertNew: +INSERT INTO search_titles (manga_id, title, type) VALUES (?, ?, ?); + +insertItem: +INSERT INTO search_titles (_id, manga_id, title, type) VALUES ?; + +deleteAll: +DELETE FROM search_titles; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/1.sqm b/app/src/main/sqldelight/migrations/1.sqm new file mode 100644 index 000000000..6eb647300 --- /dev/null +++ b/app/src/main/sqldelight/migrations/1.sqm @@ -0,0 +1,2 @@ +ALTER TABLE mangas +ADD COLUMN cover_last_modified INTEGER NOT NULL DEFAULT 0; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/10.sqm b/app/src/main/sqldelight/migrations/10.sqm new file mode 100644 index 000000000..d0cd2b495 --- /dev/null +++ b/app/src/main/sqldelight/migrations/10.sqm @@ -0,0 +1,7 @@ +CREATE TABLE eh_favorites ( + _id INTEGER NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + gid TEXT NOT NULL, + token TEXT NOT NULL, + category INTEGER NOT NULL +); \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/11.sqm b/app/src/main/sqldelight/migrations/11.sqm new file mode 100644 index 000000000..b1fb3d5cb --- /dev/null +++ b/app/src/main/sqldelight/migrations/11.sqm @@ -0,0 +1,7 @@ +CREATE TABLE IF NOT EXISTS eh_favorites ( + _id INTEGER NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + gid TEXT NOT NULL, + token TEXT NOT NULL, + category INTEGER NOT NULL +); \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/12.sqm b/app/src/main/sqldelight/migrations/12.sqm new file mode 100644 index 000000000..28261b1d0 --- /dev/null +++ b/app/src/main/sqldelight/migrations/12.sqm @@ -0,0 +1,16 @@ +CREATE TABLE saved_search( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + name TEXT NOT NULL, + query TEXT, + filters_json TEXT +); +CREATE TABLE feed_saved_search ( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + saved_search INTEGER, + global INTEGER AS Boolean NOT NULL, + FOREIGN KEY(saved_search) REFERENCES saved_search (_id) + ON DELETE CASCADE +); +CREATE INDEX feed_saved_search_saved_search_index ON feed_saved_search(saved_search); \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/13.sqm b/app/src/main/sqldelight/migrations/13.sqm new file mode 100644 index 000000000..4aef6884e --- /dev/null +++ b/app/src/main/sqldelight/migrations/13.sqm @@ -0,0 +1,275 @@ +DROP INDEX IF EXISTS chapters_manga_id_index; +DROP INDEX IF EXISTS chapters_unread_by_manga_index; +DROP INDEX IF EXISTS history_history_chapter_id_index; +DROP INDEX IF EXISTS library_favorite_index; +DROP INDEX IF EXISTS mangas_url_index; + +DROP INDEX IF EXISTS search_metadata_uploader_index; +DROP INDEX IF EXISTS search_metadata_indexed_extra_index; +DROP INDEX IF EXISTS search_tags_manga_id_index; +DROP INDEX IF EXISTS search_tags_namespace_name_index; +DROP INDEX IF EXISTS search_titles_manga_id_index; +DROP INDEX IF EXISTS search_titles_title_index; +DROP INDEX IF EXISTS merged_merge_id_index; +DROP INDEX IF EXISTS feed_saved_search_saved_search_index; + +ALTER TABLE mangas RENAME TO manga_temp; +CREATE TABLE mangas( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + url TEXT NOT NULL, + artist TEXT, + author TEXT, + description TEXT, + genre TEXT, + title TEXT NOT NULL, + status INTEGER NOT NULL, + thumbnail_url TEXT, + favorite INTEGER NOT NULL, + last_update INTEGER AS Long, + next_update INTEGER AS Long, + initialized INTEGER AS Boolean NOT NULL, + viewer INTEGER NOT NULL, + chapter_flags INTEGER NOT NULL, + cover_last_modified INTEGER AS Long NOT NULL, + date_added INTEGER AS Long NOT NULL, + filtered_scanlators TEXT +); +INSERT INTO mangas +SELECT _id,source,url,artist,author,description,genre,title,status,thumbnail_url,favorite,last_update,next_update,initialized,viewer,chapter_flags,cover_last_modified,date_added,filtered_scanlators +FROM manga_temp; + +ALTER TABLE categories RENAME TO categories_temp; +CREATE TABLE categories( + _id INTEGER NOT NULL PRIMARY KEY, + name TEXT NOT NULL, + sort INTEGER NOT NULL, + flags INTEGER NOT NULL, + manga_order TEXT NOT NULL +); +INSERT INTO categories +SELECT _id,name,sort,flags,manga_order +FROM categories_temp; + +ALTER TABLE chapters RENAME TO chapters_temp; +CREATE TABLE chapters( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + url TEXT NOT NULL, + name TEXT NOT NULL, + scanlator TEXT, + read INTEGER AS Boolean NOT NULL, + bookmark INTEGER AS Boolean NOT NULL, + last_page_read INTEGER NOT NULL, + chapter_number REAL AS Float NOT NULL, + source_order INTEGER NOT NULL, + date_fetch INTEGER AS Long NOT NULL, + date_upload INTEGER AS Long NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +INSERT INTO chapters +SELECT _id,manga_id,url,name,scanlator,read,bookmark,last_page_read,chapter_number,source_order,date_fetch,date_upload +FROM chapters_temp; + +ALTER TABLE history RENAME TO history_temp; +CREATE TABLE history( + history_id INTEGER NOT NULL PRIMARY KEY, + history_chapter_id INTEGER NOT NULL UNIQUE, + history_last_read INTEGER AS Long, + history_time_read INTEGER AS Long, + FOREIGN KEY(history_chapter_id) REFERENCES chapters (_id) + ON DELETE CASCADE +); +INSERT INTO history +SELECT history_id, history_chapter_id, history_last_read, history_time_read +FROM history_temp; + +ALTER TABLE mangas_categories RENAME TO mangas_categories_temp; +CREATE TABLE mangas_categories( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + category_id INTEGER NOT NULL, + FOREIGN KEY(category_id) REFERENCES categories (_id) + ON DELETE CASCADE, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +INSERT INTO mangas_categories +SELECT _id, manga_id, category_id +FROM mangas_categories_temp; + +ALTER TABLE manga_sync RENAME TO manga_sync_temp; +CREATE TABLE manga_sync( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + sync_id INTEGER NOT NULL, + remote_id INTEGER NOT NULL, + library_id INTEGER, + title TEXT NOT NULL, + last_chapter_read REAL NOT NULL, + total_chapters INTEGER NOT NULL, + status INTEGER NOT NULL, + score REAL AS Float NOT NULL, + remote_url TEXT NOT NULL, + start_date INTEGER AS Long NOT NULL, + finish_date INTEGER AS Long NOT NULL, + UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +INSERT INTO manga_sync +SELECT _id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date +FROM manga_sync_temp; + +ALTER TABLE eh_favorites RENAME TO eh_favorites_temp; +CREATE TABLE eh_favorites ( + _id INTEGER NOT NULL PRIMARY KEY, + title TEXT NOT NULL, + gid TEXT NOT NULL, + token TEXT NOT NULL, + category INTEGER NOT NULL +); +INSERT INTO eh_favorites +SELECT _id,title,gid,token,category +FROM eh_favorites_temp; + +ALTER TABLE saved_search RENAME TO saved_search_temp; +CREATE TABLE saved_search( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + name TEXT NOT NULL, + query TEXT, + filters_json TEXT +); +INSERT INTO saved_search +SELECT _id,source,name,query,filters_json +FROM saved_search_temp; + +ALTER TABLE feed_saved_search RENAME TO feed_saved_search_temp; +CREATE TABLE feed_saved_search ( + _id INTEGER NOT NULL PRIMARY KEY, + source INTEGER NOT NULL, + saved_search INTEGER, + global INTEGER AS Boolean NOT NULL, + FOREIGN KEY(saved_search) REFERENCES saved_search (_id) + ON DELETE CASCADE +); +INSERT INTO feed_saved_search +SELECT _id, source, saved_search, global +FROM feed_saved_search_temp; + +ALTER TABLE search_metadata RENAME TO search_metadata_temp; +CREATE TABLE search_metadata ( + manga_id INTEGER NOT NULL PRIMARY KEY, + uploader TEXT, + extra TEXT NOT NULL, + indexed_extra TEXT, + extra_version INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +INSERT INTO search_metadata +SELECT manga_id, uploader, extra, indexed_extra, extra_version +FROM search_metadata_temp; + +ALTER TABLE search_tags RENAME TO search_tags_temp; +CREATE TABLE search_tags ( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + namespace TEXT, + name TEXT NOT NULL, + type INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +INSERT INTO search_tags +SELECT _id, manga_id, namespace, name, type +FROM search_tags_temp; + +ALTER TABLE search_titles RENAME TO search_titles_temp; +CREATE TABLE search_titles ( + _id INTEGER NOT NULL PRIMARY KEY, + manga_id INTEGER NOT NULL, + title TEXT NOT NULL, + type INTEGER AS Int NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +INSERT INTO search_titles +SELECT _id, manga_id, title, type +FROM search_titles_temp; + +ALTER TABLE merged RENAME TO merged_temp; +CREATE TABLE merged( + _id INTEGER NOT NULL PRIMARY KEY, + info_manga INTEGER AS Boolean NOT NULL, + get_chapter_updates INTEGER AS Boolean NOT NULL, + chapter_sort_mode INTEGER NOT NULL, + chapter_priority INTEGER NOT NULL, + download_chapters INTEGER AS Boolean NOT NULL, + merge_id INTEGER NOT NULL, + merge_url TEXT NOT NULL, + manga_id INTEGER, + manga_url TEXT NOT NULL, + manga_source INTEGER NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE SET NULL, + FOREIGN KEY(merge_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +INSERT INTO merged +SELECT _id,info_manga,get_chapter_updates,chapter_sort_mode,chapter_priority,download_chapters,merge_id,merge_url,manga_id,manga_url,manga_source +FROM merged_temp; + +CREATE INDEX chapters_manga_id_index ON chapters(manga_id); +CREATE INDEX chapters_unread_by_manga_index ON chapters(manga_id, read) WHERE read = 0; +CREATE INDEX history_history_chapter_id_index ON history(history_chapter_id); +CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1; +CREATE INDEX mangas_url_index ON mangas(url); +CREATE INDEX search_metadata_uploader_index ON search_metadata(uploader); +CREATE INDEX search_metadata_indexed_extra_index ON search_metadata(indexed_extra); +CREATE INDEX search_tags_manga_id_index ON search_tags(manga_id); +CREATE INDEX search_tags_namespace_name_index ON search_tags(namespace, name); +CREATE INDEX search_titles_manga_id_index ON search_titles(manga_id); +CREATE INDEX search_titles_title_index ON search_titles(title); +CREATE INDEX merged_merge_id_index ON merged(merge_id); +CREATE INDEX feed_saved_search_saved_search_index ON feed_saved_search(saved_search); + +CREATE VIEW IF NOT EXISTS historyView AS +SELECT +history.history_id AS id, +mangas._id AS mangaId, +chapters._id AS chapterId, +mangas.title, +mangas.thumbnail_url AS thumnailUrl, +chapters.chapter_number AS chapterNumber, +history.history_last_read AS readAt, +max_last_read.history_last_read AS maxReadAt, +max_last_read.history_chapter_id AS maxReadAtChapterId +FROM mangas +JOIN chapters +ON mangas._id = chapters.manga_id +JOIN history +ON chapters._id = history.history_chapter_id +JOIN ( +SELECT chapters.manga_id,chapters._id AS history_chapter_id, MAX(history.history_last_read) AS history_last_read +FROM chapters JOIN history +ON chapters._id = history.history_chapter_id +GROUP BY chapters.manga_id +) AS max_last_read +ON chapters.manga_id = max_last_read.manga_id; + +DROP TABLE IF EXISTS manga_sync_temp; +DROP TABLE IF EXISTS mangas_categories_temp; +DROP TABLE IF EXISTS history_temp; +DROP TABLE IF EXISTS chapters_temp; +DROP TABLE IF EXISTS categories_temp; +DROP TABLE IF EXISTS eh_favorites_temp; +DROP TABLE IF EXISTS saved_search_temp; +DROP TABLE IF EXISTS feed_saved_search_temp; +DROP TABLE IF EXISTS search_metadata_temp; +DROP TABLE IF EXISTS search_tags_temp; +DROP TABLE IF EXISTS search_titles_temp; +DROP TABLE IF EXISTS merged_temp; +DROP TABLE IF EXISTS manga_temp; diff --git a/app/src/main/sqldelight/migrations/2.sqm b/app/src/main/sqldelight/migrations/2.sqm new file mode 100644 index 000000000..20a2c8444 --- /dev/null +++ b/app/src/main/sqldelight/migrations/2.sqm @@ -0,0 +1,11 @@ +ALTER TABLE mangas +ADD COLUMN date_added INTEGER NOT NULL DEFAULT 0; + +UPDATE mangas +SET date_added = ( + SELECT MIN(date_fetch) + FROM mangas M + INNER JOIN chapters C + ON M._id = C.manga_id + GROUP BY M._id +); \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/3.sqm b/app/src/main/sqldelight/migrations/3.sqm new file mode 100644 index 000000000..553c44029 --- /dev/null +++ b/app/src/main/sqldelight/migrations/3.sqm @@ -0,0 +1,19 @@ +DROP TABLE merged; +CREATE TABLE merged( + _id INTEGER NOT NULL PRIMARY KEY, + info_manga INTEGER AS Boolean NOT NULL, + get_chapter_updates INTEGER AS Boolean NOT NULL, + chapter_sort_mode INTEGER NOT NULL, + chapter_priority INTEGER NOT NULL, + download_chapters INTEGER AS Boolean NOT NULL, + merge_id INTEGER NOT NULL, + merge_url TEXT NOT NULL, + manga_id INTEGER, + manga_url TEXT NOT NULL, + COL_MANGA_SOURCE INTEGER NOT NULL, + FOREIGN KEY(manga_id) REFERENCES mangas (_id) + ON DELETE SET NULL, + FOREIGN KEY(merge_id) REFERENCES mangas (_id) + ON DELETE CASCADE +); +CREATE INDEX merged_merge_id_index ON merged(merge_id); \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/4.sqm b/app/src/main/sqldelight/migrations/4.sqm new file mode 100644 index 000000000..e69de29bb diff --git a/app/src/main/sqldelight/migrations/5.sqm b/app/src/main/sqldelight/migrations/5.sqm new file mode 100644 index 000000000..4a70d4170 --- /dev/null +++ b/app/src/main/sqldelight/migrations/5.sqm @@ -0,0 +1,2 @@ +ALTER TABLE mangas +ADD COLUMN filtered_scanlators TEXT; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/6.sqm b/app/src/main/sqldelight/migrations/6.sqm new file mode 100644 index 000000000..7718ccb35 --- /dev/null +++ b/app/src/main/sqldelight/migrations/6.sqm @@ -0,0 +1 @@ +DROP TABLE IF EXISTS manga_related; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/7.sqm b/app/src/main/sqldelight/migrations/7.sqm new file mode 100644 index 000000000..23b429acd --- /dev/null +++ b/app/src/main/sqldelight/migrations/7.sqm @@ -0,0 +1,2 @@ +ALTER TABLE mangas +ADD COLUMN next_update INTEGER DEFAULT 0; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/8.sqm b/app/src/main/sqldelight/migrations/8.sqm new file mode 100644 index 000000000..c80623439 --- /dev/null +++ b/app/src/main/sqldelight/migrations/8.sqm @@ -0,0 +1,9 @@ +ALTER TABLE manga_sync +RENAME TO manga_sync_tmp; + +INSERT INTO manga_sync(_id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date) +SELECT _id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date +FROM manga_sync_tmp; + + +DROP TABLE manga_sync_tmp; \ No newline at end of file diff --git a/app/src/main/sqldelight/migrations/9.sqm b/app/src/main/sqldelight/migrations/9.sqm new file mode 100644 index 000000000..78e1ece21 --- /dev/null +++ b/app/src/main/sqldelight/migrations/9.sqm @@ -0,0 +1,3 @@ +UPDATE chapters +SET date_upload = date_fetch +WHERE date_upload = 0; \ No newline at end of file diff --git a/app/src/main/sqldelight/view/historyView.sq b/app/src/main/sqldelight/view/historyView.sq new file mode 100644 index 000000000..2471f85aa --- /dev/null +++ b/app/src/main/sqldelight/view/historyView.sq @@ -0,0 +1,46 @@ +CREATE VIEW historyView AS +SELECT +history.history_id AS id, +mangas._id AS mangaId, +chapters._id AS chapterId, +mangas.title, +mangas.thumbnail_url AS thumnailUrl, +chapters.chapter_number AS chapterNumber, +history.history_last_read AS readAt, +max_last_read.history_last_read AS maxReadAt, +max_last_read.history_chapter_id AS maxReadAtChapterId +FROM mangas +JOIN chapters +ON mangas._id = chapters.manga_id +JOIN history +ON chapters._id = history.history_chapter_id +JOIN ( +SELECT chapters.manga_id,chapters._id AS history_chapter_id, MAX(history.history_last_read) AS history_last_read +FROM chapters JOIN history +ON chapters._id = history.history_chapter_id +GROUP BY chapters.manga_id +) AS max_last_read +ON chapters.manga_id = max_last_read.manga_id; + +countHistory: +SELECT count(*) +FROM historyView +WHERE historyView.readAt > 0 +AND maxReadAtChapterId = historyView.chapterId +AND lower(historyView.title) LIKE ('%' || :query || '%'); + +history: +SELECT +id, +mangaId, +chapterId, +title, +thumnailUrl, +chapterNumber, +readAt +FROM historyView +WHERE historyView.readAt > 0 +AND maxReadAtChapterId = historyView.chapterId +AND lower(historyView.title) LIKE ('%' || :query || '%') +ORDER BY readAt DESC +LIMIT :limit OFFSET :offset; diff --git a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt b/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt index 75fe1320c..881533404 100644 --- a/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt +++ b/app/src/test/java/eu/kanade/tachiyomi/data/backup/BackupTest.kt @@ -344,7 +344,7 @@ class BackupTest { private fun clearDatabase() { db.deleteMangas().executeAsBlocking() - db.deleteHistory().executeAsBlocking() + db.dropHistoryTable().executeAsBlocking() } private fun getSingleHistory(chapter: Chapter): DHistory { diff --git a/build.gradle.kts b/build.gradle.kts index 8d647913d..10a3a5a37 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -4,6 +4,7 @@ buildscript { classpath(libs.google.services.gradle) classpath(libs.aboutlibraries.gradle) classpath(kotlinx.serialization.gradle) + classpath("com.squareup.sqldelight:gradle-plugin:1.5.3") classpath(sylibs.firebase.crashlytics.gradle) } } diff --git a/gradle/androidx.versions.toml b/gradle/androidx.versions.toml index b051c785b..6c49236bb 100644 --- a/gradle/androidx.versions.toml +++ b/gradle/androidx.versions.toml @@ -21,6 +21,9 @@ lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", ve work-runtime = "androidx.work:work-runtime-ktx:2.6.0" guava = "com.google.guava:guava:31.1-android" +paging-runtime = "androidx.paging:paging-runtime:3.1.1" +paging-compose = "androidx.paging:paging-compose:1.0.0-alpha14" + [bundles] lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"] workmanager = ["work-runtime", "guava"] diff --git a/gradle/compose.versions.toml b/gradle/compose.versions.toml new file mode 100644 index 000000000..6b271fc9b --- /dev/null +++ b/gradle/compose.versions.toml @@ -0,0 +1,9 @@ +[versions] +compose = "1.2.0-alpha07" + +[libraries] +foundation = { module = "androidx.compose.foundation:foundation", version.ref="compose" } +material3-core = "androidx.compose.material3:material3:1.0.0-alpha09" +material3-adapter = "com.google.android.material:compose-theme-adapter-3:1.0.6" +animation = { module = "androidx.compose.animation:animation", version.ref="compose" } +ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref="compose" } diff --git a/gradle/kotlinx.versions.toml b/gradle/kotlinx.versions.toml index 268e66b5f..74448c71b 100644 --- a/gradle/kotlinx.versions.toml +++ b/gradle/kotlinx.versions.toml @@ -1,5 +1,5 @@ [versions] -kotlin_version = "1.6.20" +kotlin_version = "1.6.10" coroutines_version = "1.6.1" serialization_version = "1.3.2" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index cb3cf78b6..3f332967a 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -7,6 +7,7 @@ conductor_version = "3.1.2" flowbinding_version = "1.2.0" shizuku_version = "12.1.0" robolectric_version = "3.1.4" +sqldelight = "1.5.3" [libraries] android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2" @@ -49,6 +50,7 @@ injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440" coil-core = { module = "io.coil-kt:coil", version.ref = "coil_version" } coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil_version" } +coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil_version" } subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:846abe0" image-decoder = "com.github.tachiyomiorg:image-decoder:7481a4a" @@ -96,18 +98,22 @@ robolectric-playservices = { module = "org.robolectric:shadows-play-services", v leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.7" +sqldelight-android-driver = { module = "com.squareup.sqldelight:android-driver", version.ref ="sqldelight" } +sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions-jvm", version.ref ="sqldelight" } +sqldelight-android-paging = { module = "com.squareup.sqldelight:android-paging3-extensions", version.ref ="sqldelight" } + [bundles] reactivex = ["rxandroid","rxjava","rxrelay"] okhttp = ["okhttp-core","okhttp-logging","okhttp-dnsoverhttps"] js-engine = ["quickjs-android", "duktape-android"] sqlite = ["sqlitektx", "sqlite-android"] nucleus = ["nucleus-core","nucleus-supportv7"] -coil = ["coil-core","coil-gif",] +coil = ["coil-core","coil-gif","coil-compose"] flowbinding = ["flowbinding-android","flowbinding-appcompat","flowbinding-recyclerview","flowbinding-swiperefreshlayout","flowbinding-viewpager"] conductor = ["conductor-core","conductor-viewpager","conductor-support-preference"] shizuku = ["shizuku-api","shizuku-provider"] robolectric = ["robolectric-core","robolectric-playservices"] [plugins] -kotlinter = { id = "org.jmailen.kotlinter", version = "3.10.0"} +kotlinter = { id = "org.jmailen.kotlinter", version = "3.6.0"} versionsx = { id = "com.github.ben-manes.versions", version = "0.42.0"} \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index 45778f8c2..6ca65f667 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,6 +22,9 @@ dependencyResolutionManagement { create("androidx") { from(files("gradle/androidx.versions.toml")) } + create("compose") { + from(files("gradle/compose.versions.toml")) + } create("sylibs") { from(files("gradle/sy.versions.toml")) }