Revert history Compose/SQLDelight changes
This commit is contained in:
parent
070e2d94c7
commit
6a0b523e86
@ -7,7 +7,6 @@ plugins {
|
||||
kotlin("plugin.parcelize")
|
||||
kotlin("plugin.serialization")
|
||||
id("com.github.zellius.shortcut-helper")
|
||||
id("com.squareup.sqldelight")
|
||||
}
|
||||
|
||||
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
|
||||
@ -93,7 +92,6 @@ android {
|
||||
|
||||
buildFeatures {
|
||||
viewBinding = true
|
||||
compose = true
|
||||
|
||||
// Disable some unused things
|
||||
aidl = false
|
||||
@ -107,10 +105,6 @@ android {
|
||||
checkReleaseBuilds = false
|
||||
}
|
||||
|
||||
composeOptions {
|
||||
kotlinCompilerExtensionVersion = compose.versions.compose.get()
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
sourceCompatibility = JavaVersion.VERSION_1_8
|
||||
targetCompatibility = JavaVersion.VERSION_1_8
|
||||
@ -122,19 +116,6 @@ 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)
|
||||
@ -288,9 +269,6 @@ 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",
|
||||
)
|
||||
}
|
||||
|
@ -1,94 +0,0 @@
|
||||
package eu.kanade.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import com.squareup.sqldelight.Query
|
||||
import com.squareup.sqldelight.Transacter
|
||||
import com.squareup.sqldelight.android.paging3.QueryPagingSource
|
||||
import com.squareup.sqldelight.db.SqlDriver
|
||||
import com.squareup.sqldelight.runtime.coroutines.asFlow
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToList
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToOne
|
||||
import com.squareup.sqldelight.runtime.coroutines.mapToOneOrNull
|
||||
import eu.kanade.tachiyomi.Database
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Dispatchers
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.withContext
|
||||
|
||||
class AndroidDatabaseHandler(
|
||||
val db: Database,
|
||||
private val driver: SqlDriver,
|
||||
val queryDispatcher: CoroutineDispatcher = Dispatchers.IO,
|
||||
val transactionDispatcher: CoroutineDispatcher = queryDispatcher
|
||||
) : DatabaseHandler {
|
||||
|
||||
val suspendingTransactionId = ThreadLocal<Int>()
|
||||
|
||||
override suspend fun <T> await(inTransaction: Boolean, block: suspend Database.() -> T): T {
|
||||
return dispatch(inTransaction, block)
|
||||
}
|
||||
|
||||
override suspend fun <T : Any> awaitList(
|
||||
inTransaction: Boolean,
|
||||
block: suspend Database.() -> Query<T>
|
||||
): List<T> {
|
||||
return dispatch(inTransaction) { block(db).executeAsList() }
|
||||
}
|
||||
|
||||
override suspend fun <T : Any> awaitOne(
|
||||
inTransaction: Boolean,
|
||||
block: suspend Database.() -> Query<T>
|
||||
): T {
|
||||
return dispatch(inTransaction) { block(db).executeAsOne() }
|
||||
}
|
||||
|
||||
override suspend fun <T : Any> awaitOneOrNull(
|
||||
inTransaction: Boolean,
|
||||
block: suspend Database.() -> Query<T>
|
||||
): T? {
|
||||
return dispatch(inTransaction) { block(db).executeAsOneOrNull() }
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>> {
|
||||
return block(db).asFlow().mapToList(queryDispatcher)
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T> {
|
||||
return block(db).asFlow().mapToOne(queryDispatcher)
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?> {
|
||||
return block(db).asFlow().mapToOneOrNull(queryDispatcher)
|
||||
}
|
||||
|
||||
override fun <T : Any> subscribeToPagingSource(
|
||||
countQuery: Database.() -> Query<Long>,
|
||||
transacter: Database.() -> Transacter,
|
||||
queryProvider: Database.(Long, Long) -> Query<T>
|
||||
): PagingSource<Long, T> {
|
||||
return QueryPagingSource(
|
||||
countQuery = countQuery(db),
|
||||
transacter = transacter(db),
|
||||
dispatcher = queryDispatcher,
|
||||
queryProvider = { limit, offset ->
|
||||
queryProvider.invoke(db, limit, offset)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
private suspend fun <T> dispatch(inTransaction: Boolean, block: suspend Database.() -> T): T {
|
||||
// Create a transaction if needed and run the calling block inside it.
|
||||
if (inTransaction) {
|
||||
return withTransaction { block(db) }
|
||||
}
|
||||
|
||||
// If we're currently in the transaction thread, there's no need to dispatch our query.
|
||||
if (driver.currentTransaction() != null) {
|
||||
return block(db)
|
||||
}
|
||||
|
||||
// Get the current database context and run the calling block.
|
||||
val context = getCurrentDatabaseContext()
|
||||
return withContext(context) { block(db) }
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
package eu.kanade.data
|
||||
|
||||
import com.squareup.sqldelight.ColumnAdapter
|
||||
import java.util.Date
|
||||
|
||||
val dateAdapter = object : ColumnAdapter<Date, Long> {
|
||||
override fun decode(databaseValue: Long): Date = Date(databaseValue)
|
||||
override fun encode(value: Date): Long = value.time
|
||||
}
|
||||
|
||||
private const val listOfStringsSeparator = ", "
|
||||
val listOfStringsAdapter = object : ColumnAdapter<List<String>, String> {
|
||||
override fun decode(databaseValue: String) =
|
||||
if (databaseValue.isEmpty()) {
|
||||
listOf()
|
||||
} else {
|
||||
databaseValue.split(listOfStringsSeparator)
|
||||
}
|
||||
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsSeparator)
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package eu.kanade.data
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import com.squareup.sqldelight.Query
|
||||
import com.squareup.sqldelight.Transacter
|
||||
import eu.kanade.tachiyomi.Database
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
|
||||
interface DatabaseHandler {
|
||||
|
||||
suspend fun <T> await(inTransaction: Boolean = false, block: suspend Database.() -> T): T
|
||||
|
||||
suspend fun <T : Any> awaitList(
|
||||
inTransaction: Boolean = false,
|
||||
block: suspend Database.() -> Query<T>
|
||||
): List<T>
|
||||
|
||||
suspend fun <T : Any> awaitOne(
|
||||
inTransaction: Boolean = false,
|
||||
block: suspend Database.() -> Query<T>
|
||||
): T
|
||||
|
||||
suspend fun <T : Any> awaitOneOrNull(
|
||||
inTransaction: Boolean = false,
|
||||
block: suspend Database.() -> Query<T>
|
||||
): T?
|
||||
|
||||
fun <T : Any> subscribeToList(block: Database.() -> Query<T>): Flow<List<T>>
|
||||
|
||||
fun <T : Any> subscribeToOne(block: Database.() -> Query<T>): Flow<T>
|
||||
|
||||
fun <T : Any> subscribeToOneOrNull(block: Database.() -> Query<T>): Flow<T?>
|
||||
|
||||
fun <T : Any> subscribeToPagingSource(
|
||||
countQuery: Database.() -> Query<Long>,
|
||||
transacter: Database.() -> Transacter,
|
||||
queryProvider: Database.(Long, Long) -> Query<T>
|
||||
): PagingSource<Long, T>
|
||||
}
|
@ -1,160 +0,0 @@
|
||||
package eu.kanade.data
|
||||
|
||||
import kotlinx.coroutines.CoroutineDispatcher
|
||||
import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.asContextElement
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.suspendCancellableCoroutine
|
||||
import kotlinx.coroutines.withContext
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.coroutines.ContinuationInterceptor
|
||||
import kotlin.coroutines.CoroutineContext
|
||||
import kotlin.coroutines.EmptyCoroutineContext
|
||||
import kotlin.coroutines.coroutineContext
|
||||
import kotlin.coroutines.resume
|
||||
|
||||
/**
|
||||
* Returns the transaction dispatcher if we are on a transaction, or the database dispatchers.
|
||||
*/
|
||||
internal suspend fun AndroidDatabaseHandler.getCurrentDatabaseContext(): CoroutineContext {
|
||||
return coroutineContext[TransactionElement]?.transactionDispatcher ?: queryDispatcher
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls the specified suspending [block] in a database transaction. The transaction will be
|
||||
* marked as successful unless an exception is thrown in the suspending [block] or the coroutine
|
||||
* is cancelled.
|
||||
*
|
||||
* SQLDelight will only perform at most one transaction at a time, additional transactions are queued
|
||||
* and executed on a first come, first serve order.
|
||||
*
|
||||
* Performing blocking database operations is not permitted in a coroutine scope other than the
|
||||
* one received by the suspending block. It is recommended that all [Dao] function invoked within
|
||||
* the [block] be suspending functions.
|
||||
*
|
||||
* The dispatcher used to execute the given [block] will utilize threads from SQLDelight's query executor.
|
||||
*/
|
||||
internal suspend fun <T> AndroidDatabaseHandler.withTransaction(block: suspend () -> T): T {
|
||||
// Use inherited transaction context if available, this allows nested suspending transactions.
|
||||
val transactionContext =
|
||||
coroutineContext[TransactionElement]?.transactionDispatcher ?: createTransactionContext()
|
||||
return withContext(transactionContext) {
|
||||
val transactionElement = coroutineContext[TransactionElement]!!
|
||||
transactionElement.acquire()
|
||||
try {
|
||||
db.transactionWithResult {
|
||||
runBlocking(transactionContext) {
|
||||
block()
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
transactionElement.release()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a [CoroutineContext] for performing database operations within a coroutine transaction.
|
||||
*
|
||||
* The context is a combination of a dispatcher, a [TransactionElement] and a thread local element.
|
||||
*
|
||||
* * The dispatcher will dispatch coroutines to a single thread that is taken over from the SQLDelight
|
||||
* query executor. If the coroutine context is switched, suspending DAO functions will be able to
|
||||
* dispatch to the transaction thread.
|
||||
*
|
||||
* * The [TransactionElement] serves as an indicator for inherited context, meaning, if there is a
|
||||
* switch of context, suspending DAO methods will be able to use the indicator to dispatch the
|
||||
* database operation to the transaction thread.
|
||||
*
|
||||
* * The thread local element serves as a second indicator and marks threads that are used to
|
||||
* execute coroutines within the coroutine transaction, more specifically it allows us to identify
|
||||
* if a blocking DAO method is invoked within the transaction coroutine. Never assign meaning to
|
||||
* this value, for now all we care is if its present or not.
|
||||
*/
|
||||
private suspend fun AndroidDatabaseHandler.createTransactionContext(): CoroutineContext {
|
||||
val controlJob = Job()
|
||||
// make sure to tie the control job to this context to avoid blocking the transaction if
|
||||
// context get cancelled before we can even start using this job. Otherwise, the acquired
|
||||
// transaction thread will forever wait for the controlJob to be cancelled.
|
||||
// see b/148181325
|
||||
coroutineContext[Job]?.invokeOnCompletion {
|
||||
controlJob.cancel()
|
||||
}
|
||||
|
||||
val dispatcher = transactionDispatcher.acquireTransactionThread(controlJob)
|
||||
val transactionElement = TransactionElement(controlJob, dispatcher)
|
||||
val threadLocalElement =
|
||||
suspendingTransactionId.asContextElement(System.identityHashCode(controlJob))
|
||||
return dispatcher + transactionElement + threadLocalElement
|
||||
}
|
||||
|
||||
/**
|
||||
* Acquires a thread from the executor and returns a [ContinuationInterceptor] to dispatch
|
||||
* coroutines to the acquired thread. The [controlJob] is used to control the release of the
|
||||
* thread by cancelling the job.
|
||||
*/
|
||||
private suspend fun CoroutineDispatcher.acquireTransactionThread(
|
||||
controlJob: Job
|
||||
): ContinuationInterceptor {
|
||||
return suspendCancellableCoroutine { continuation ->
|
||||
continuation.invokeOnCancellation {
|
||||
// We got cancelled while waiting to acquire a thread, we can't stop our attempt to
|
||||
// acquire a thread, but we can cancel the controlling job so once it gets acquired it
|
||||
// is quickly released.
|
||||
controlJob.cancel()
|
||||
}
|
||||
try {
|
||||
dispatch(EmptyCoroutineContext) {
|
||||
runBlocking {
|
||||
// Thread acquired, resume coroutine.
|
||||
continuation.resume(coroutineContext[ContinuationInterceptor]!!)
|
||||
controlJob.join()
|
||||
}
|
||||
}
|
||||
} catch (ex: RejectedExecutionException) {
|
||||
// Couldn't acquire a thread, cancel coroutine.
|
||||
continuation.cancel(
|
||||
IllegalStateException(
|
||||
"Unable to acquire a thread to perform the database transaction.", ex
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A [CoroutineContext.Element] that indicates there is an on-going database transaction.
|
||||
*/
|
||||
private class TransactionElement(
|
||||
private val transactionThreadControlJob: Job,
|
||||
val transactionDispatcher: ContinuationInterceptor
|
||||
) : CoroutineContext.Element {
|
||||
|
||||
companion object Key : CoroutineContext.Key<TransactionElement>
|
||||
|
||||
override val key: CoroutineContext.Key<TransactionElement>
|
||||
get() = TransactionElement
|
||||
|
||||
/**
|
||||
* Number of transactions (including nested ones) started with this element.
|
||||
* Call [acquire] to increase the count and [release] to decrease it. If the count reaches zero
|
||||
* when [release] is invoked then the transaction job is cancelled and the transaction thread
|
||||
* is released.
|
||||
*/
|
||||
private val referenceCount = AtomicInteger(0)
|
||||
|
||||
fun acquire() {
|
||||
referenceCount.incrementAndGet()
|
||||
}
|
||||
|
||||
fun release() {
|
||||
val count = referenceCount.decrementAndGet()
|
||||
if (count < 0) {
|
||||
throw IllegalStateException("Transaction was never started or was already released.")
|
||||
} else if (count == 0) {
|
||||
// Cancel the job that controls the transaction thread, causing it to be released.
|
||||
transactionThreadControlJob.cancel()
|
||||
}
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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,
|
||||
)
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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
|
||||
)
|
||||
}
|
@ -1,14 +0,0 @@
|
||||
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
|
||||
)
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
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
|
||||
)
|
||||
}
|
@ -1,91 +0,0 @@
|
||||
package eu.kanade.data.history
|
||||
|
||||
import androidx.paging.PagingSource
|
||||
import eu.kanade.data.DatabaseHandler
|
||||
import eu.kanade.data.chapter.chapterMapper
|
||||
import eu.kanade.data.manga.mangaMapper
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.domain.history.model.HistoryWithRelations
|
||||
import eu.kanade.domain.history.repository.HistoryRepository
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
import eu.kanade.tachiyomi.util.system.logcat
|
||||
|
||||
class HistoryRepositoryImpl(
|
||||
private val handler: DatabaseHandler
|
||||
) : HistoryRepository {
|
||||
|
||||
override fun getHistory(query: String): PagingSource<Long, HistoryWithRelations> {
|
||||
return handler.subscribeToPagingSource(
|
||||
countQuery = { historyViewQueries.countHistory(query) },
|
||||
transacter = { historyViewQueries },
|
||||
queryProvider = { limit, offset ->
|
||||
historyViewQueries.history(query, limit, offset, historyWithRelationsMapper)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override suspend fun getNextChapterForManga(mangaId: Long, chapterId: Long): Chapter? {
|
||||
val chapter = handler.awaitOne { chaptersQueries.getChapterById(chapterId, chapterMapper) }
|
||||
val manga = handler.awaitOne { mangasQueries.getMangaById(mangaId, mangaMapper) }
|
||||
|
||||
if (!chapter.read) {
|
||||
return chapter
|
||||
}
|
||||
|
||||
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
|
||||
Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.sourceOrder.compareTo(c1.sourceOrder) }
|
||||
Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapterNumber.compareTo(c2.chapterNumber) }
|
||||
Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.dateUpload.compareTo(c2.dateUpload) }
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
|
||||
val chapters = handler.awaitList { chaptersQueries.getChapterByMangaId(mangaId, chapterMapper) }
|
||||
.sortedWith(sortFunction)
|
||||
|
||||
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
|
||||
return when (manga.sorting) {
|
||||
Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1)
|
||||
Manga.CHAPTER_SORTING_NUMBER -> {
|
||||
val chapterNumber = chapter.chapterNumber
|
||||
|
||||
((currChapterIndex + 1) until chapters.size)
|
||||
.map { chapters[it] }
|
||||
.firstOrNull {
|
||||
it.chapterNumber > chapterNumber &&
|
||||
it.chapterNumber <= chapterNumber + 1
|
||||
}
|
||||
}
|
||||
Manga.CHAPTER_SORTING_UPLOAD_DATE -> {
|
||||
chapters.drop(currChapterIndex + 1)
|
||||
.firstOrNull { it.dateUpload >= chapter.dateUpload }
|
||||
}
|
||||
else -> throw NotImplementedError("Unknown sorting method")
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resetHistory(historyId: Long) {
|
||||
try {
|
||||
handler.await { historyQueries.resetHistoryById(historyId) }
|
||||
} catch (e: Exception) {
|
||||
logcat(throwable = e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun resetHistoryByMangaId(mangaId: Long) {
|
||||
try {
|
||||
handler.await { historyQueries.resetHistoryByMangaId(mangaId) }
|
||||
} catch (e: Exception) {
|
||||
logcat(throwable = e)
|
||||
}
|
||||
}
|
||||
|
||||
override suspend fun deleteAllHistory(): Boolean {
|
||||
return try {
|
||||
handler.await { historyQueries.removeAllHistory() }
|
||||
true
|
||||
} catch (e: Exception) {
|
||||
logcat(throwable = e)
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
@ -1,27 +0,0 @@
|
||||
package eu.kanade.data.manga
|
||||
|
||||
import eu.kanade.domain.manga.model.Manga
|
||||
|
||||
val mangaMapper: (Long, Long, String, String?, String?, String?, List<String>?, String, Long, String?, Boolean, Long?, Long?, Boolean, Long, Long, Long, Long, 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
|
||||
)
|
||||
}
|
@ -1,26 +0,0 @@
|
||||
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<HistoryRepository> { HistoryRepositoryImpl(get()) }
|
||||
addFactory { DeleteHistoryTable(get()) }
|
||||
addFactory { GetHistory(get()) }
|
||||
addFactory { GetNextChapterForManga(get()) }
|
||||
addFactory { RemoveHistoryById(get()) }
|
||||
addFactory { RemoveHistoryByMangaId(get()) }
|
||||
}
|
||||
}
|
@ -1,16 +0,0 @@
|
||||
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?
|
||||
)
|
@ -1,12 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
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<PagingData<HistoryWithRelations>> {
|
||||
return Pager(
|
||||
PagingConfig(pageSize = 25)
|
||||
) {
|
||||
repository.getHistory(query)
|
||||
}.flow
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,13 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -1,9 +0,0 @@
|
||||
package eu.kanade.domain.history.model
|
||||
|
||||
import java.util.Date
|
||||
|
||||
data class History(
|
||||
val id: Long?,
|
||||
val chapterId: Long,
|
||||
val readAt: Date?
|
||||
)
|
@ -1,13 +0,0 @@
|
||||
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?
|
||||
)
|
@ -1,18 +0,0 @@
|
||||
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<Long, HistoryWithRelations>
|
||||
|
||||
suspend fun getNextChapterForManga(mangaId: Long, chapterId: Long): Chapter?
|
||||
|
||||
suspend fun resetHistory(historyId: Long)
|
||||
|
||||
suspend fun resetHistoryByMangaId(mangaId: Long)
|
||||
|
||||
suspend fun deleteAllHistory(): Boolean
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
package eu.kanade.domain.manga.model
|
||||
|
||||
data class Manga(
|
||||
val id: Long,
|
||||
val source: Long,
|
||||
val favorite: Boolean,
|
||||
val lastUpdate: Long,
|
||||
val dateAdded: Long,
|
||||
val viewerFlags: Long,
|
||||
val chapterFlags: Long,
|
||||
val coverLastModified: Long,
|
||||
val url: String,
|
||||
val title: String,
|
||||
val artist: String?,
|
||||
val author: String?,
|
||||
val description: String?,
|
||||
val genre: List<String>?,
|
||||
val status: Long,
|
||||
val thumbnailUrl: String?,
|
||||
val initialized: Boolean,
|
||||
// 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
|
||||
}
|
||||
}
|
@ -1,49 +0,0 @@
|
||||
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<EmptyView.Action>? = null,
|
||||
) {
|
||||
EmptyScreen(
|
||||
message = stringResource(id = textResource),
|
||||
actions = actions,
|
||||
)
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun EmptyScreen(
|
||||
message: String,
|
||||
actions: List<EmptyView.Action>? = 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)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,39 +0,0 @@
|
||||
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)
|
@ -1,297 +0,0 @@
|
||||
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<UiModel>,
|
||||
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<HistoryWithRelations?>(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 = '.' },
|
||||
)
|
@ -1,20 +0,0 @@
|
||||
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
|
||||
)
|
||||
}
|
@ -1,5 +0,0 @@
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import androidx.compose.ui.unit.dp
|
||||
|
||||
val horizontalPadding = 16.dp
|
@ -1,5 +0,0 @@
|
||||
package eu.kanade.presentation.util
|
||||
|
||||
import androidx.compose.foundation.lazy.LazyListState
|
||||
|
||||
fun LazyListState.isScrolledToEnd() = layoutInfo.visibleItemsInfo.lastOrNull()?.index == layoutInfo.totalItemsCount - 1
|
@ -38,7 +38,6 @@ 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
|
||||
@ -102,7 +101,6 @@ 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) {
|
||||
|
@ -2,18 +2,9 @@ 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
|
||||
@ -36,37 +27,11 @@ class AppModule(val app: Application) : InjektModule {
|
||||
override fun InjektRegistrar.registerInjectables() {
|
||||
addSingleton(app)
|
||||
|
||||
addSingletonFactory { DbOpenCallback() }
|
||||
|
||||
addSingletonFactory<SqlDriver> {
|
||||
AndroidSqliteDriver(
|
||||
schema = Database.Schema,
|
||||
context = app,
|
||||
name = DbOpenCallback.DATABASE_NAME,
|
||||
callback = get<DbOpenCallback>()
|
||||
)
|
||||
}
|
||||
|
||||
addSingletonFactory {
|
||||
Database(
|
||||
driver = get(),
|
||||
historyAdapter = History.Adapter(
|
||||
history_last_readAdapter = dateAdapter,
|
||||
history_time_readAdapter = dateAdapter
|
||||
),
|
||||
mangasAdapter = Mangas.Adapter(
|
||||
genreAdapter = listOfStringsAdapter
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
addSingletonFactory<DatabaseHandler> { AndroidDatabaseHandler(get(), get()) }
|
||||
|
||||
addSingletonFactory { Json { ignoreUnknownKeys = true } }
|
||||
|
||||
addSingletonFactory { PreferencesHelper(app) }
|
||||
|
||||
addSingletonFactory { DatabaseHelper(app, get()) }
|
||||
addSingletonFactory { DatabaseHelper(app) }
|
||||
|
||||
addSingletonFactory { ChapterCache(app) }
|
||||
|
||||
@ -100,8 +65,6 @@ class AppModule(val app: Application) : InjektModule {
|
||||
|
||||
get<SourceManager>()
|
||||
|
||||
get<Database>()
|
||||
|
||||
get<DatabaseHelper>()
|
||||
|
||||
get<DownloadManager>()
|
||||
|
@ -2,7 +2,6 @@ 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
|
||||
@ -21,7 +20,6 @@ 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()
|
||||
|
@ -3,7 +3,6 @@ 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
|
||||
@ -40,11 +39,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
|
||||
@ -168,15 +167,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
* @return list of [BackupSavedSearch] to be backed up
|
||||
*/
|
||||
private fun backupSavedSearches(): List<BackupSavedSearch> {
|
||||
return runBlocking {
|
||||
databaseHandler.awaitList { saved_searchQueries.selectAll(savedSearchMapper) }.map {
|
||||
BackupSavedSearch(
|
||||
it.name,
|
||||
it.query.orEmpty(),
|
||||
it.filtersJson ?: "[]",
|
||||
it.source,
|
||||
)
|
||||
}
|
||||
return databaseHelper.getSavedSearches().executeAsBlocking().map {
|
||||
BackupSavedSearch(
|
||||
it.name,
|
||||
it.query.orEmpty(),
|
||||
it.filtersJson ?: "[]",
|
||||
it.source,
|
||||
)
|
||||
}
|
||||
}
|
||||
// SY <--
|
||||
@ -360,7 +357,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -434,24 +431,25 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
|
||||
}
|
||||
|
||||
// SY -->
|
||||
internal suspend fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
|
||||
val currentSavedSearches = databaseHandler.awaitList {
|
||||
saved_searchQueries.selectAll(savedSearchMapper)
|
||||
}
|
||||
internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
|
||||
val currentSavedSearches = databaseHelper.getSavedSearches()
|
||||
.executeAsBlocking()
|
||||
|
||||
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 == "[]" },
|
||||
)
|
||||
}
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -76,7 +76,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private suspend fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
|
||||
private fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
|
||||
backupManager.restoreSavedSearches(backupSavedSearches)
|
||||
|
||||
restoreProgress += 1
|
||||
|
@ -2,7 +2,6 @@ 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
|
||||
@ -207,7 +206,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
}
|
||||
}
|
||||
}
|
||||
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
@ -290,37 +289,28 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
|
||||
}
|
||||
|
||||
// SY -->
|
||||
internal suspend fun restoreSavedSearches(jsonSavedSearches: String) {
|
||||
internal fun restoreSavedSearches(jsonSavedSearches: String) {
|
||||
val backupSavedSearches = jsonSavedSearches.split("***").toSet()
|
||||
|
||||
val currentSavedSearches = databaseHandler.awaitList {
|
||||
saved_searchQueries.selectAll(savedSearchMapper)
|
||||
}
|
||||
val currentSavedSearches = databaseHelper.getSavedSearches().executeAsBlocking()
|
||||
|
||||
databaseHandler.await(true) {
|
||||
backupSavedSearches.mapNotNull {
|
||||
runCatching {
|
||||
val content = parser.decodeFromString<JsonObject>(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 == "[]" },
|
||||
val newSavedSearches = backupSavedSearches.mapNotNull {
|
||||
runCatching {
|
||||
val content = parser.decodeFromString<JsonObject>(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 }
|
||||
}.ifEmpty { null }
|
||||
|
||||
if (newSavedSearches != null) {
|
||||
databaseHelper.insertSavedSearches(newSavedSearches)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -73,7 +73,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
|
||||
}
|
||||
|
||||
// SY -->
|
||||
private suspend fun restoreSavedSearches(savedSearches: String) {
|
||||
private fun restoreSavedSearches(savedSearches: String) {
|
||||
backupManager.restoreSavedSearches(savedSearches)
|
||||
|
||||
restoreProgress += 1
|
||||
|
@ -47,10 +47,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
|
||||
/**
|
||||
* This class provides operations to manage the database through its interfaces.
|
||||
*/
|
||||
open class DatabaseHelper(
|
||||
context: Context,
|
||||
callback: DbOpenCallback
|
||||
) :
|
||||
open class DatabaseHelper(context: Context) :
|
||||
MangaQueries,
|
||||
ChapterQueries,
|
||||
TrackQueries,
|
||||
@ -69,7 +66,7 @@ open class DatabaseHelper(
|
||||
|
||||
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
|
||||
.name(DbOpenCallback.DATABASE_NAME)
|
||||
.callback(callback)
|
||||
.callback(DbOpenCallback())
|
||||
.build()
|
||||
|
||||
override val db = DefaultStorIOSQLite.builder()
|
||||
|
@ -2,28 +2,115 @@ package eu.kanade.tachiyomi.data.database
|
||||
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase
|
||||
import androidx.sqlite.db.SupportSQLiteOpenHelper
|
||||
import com.squareup.sqldelight.android.AndroidSqliteDriver
|
||||
import eu.kanade.tachiyomi.Database
|
||||
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
|
||||
|
||||
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(Database.Schema.version) {
|
||||
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_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) {
|
||||
Database.Schema.create(AndroidSqliteDriver(database = db, cacheSize = 1))
|
||||
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 onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
|
||||
Database.Schema.migrate(
|
||||
driver = AndroidSqliteDriver(database = db, cacheSize = 1),
|
||||
oldVersion = oldVersion,
|
||||
newVersion = newVersion
|
||||
)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onConfigure(db: SupportSQLiteDatabase) {
|
||||
|
@ -4,12 +4,40 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
|
||||
import com.pushtorefresh.storio.sqlite.queries.RawQuery
|
||||
import eu.kanade.tachiyomi.data.database.DbProvider
|
||||
import eu.kanade.tachiyomi.data.database.models.History
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryChapterIdPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryUpsertResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
|
||||
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
|
||||
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
|
||||
import java.util.Date
|
||||
|
||||
interface HistoryQueries : DbProvider {
|
||||
|
||||
/**
|
||||
* Insert history into database
|
||||
* @param history object containing history information
|
||||
*/
|
||||
fun insertHistory(history: History) = db.put().`object`(history).prepare()
|
||||
|
||||
/**
|
||||
* Returns history of recent manga containing last read chapter
|
||||
* @param date recent date range
|
||||
* @param limit the limit of manga to grab
|
||||
* @param offset offset the db by
|
||||
* @param search what to search in the db history
|
||||
*/
|
||||
fun getRecentManga(date: Date, limit: Int = 25, offset: Int = 0, search: String = "") = db.get()
|
||||
.listOfObjects(MangaChapterHistory::class.java)
|
||||
.withQuery(
|
||||
RawQuery.builder()
|
||||
.query(getRecentMangasQuery(search))
|
||||
.args(date.time, limit, offset)
|
||||
.observesTables(HistoryTable.TABLE)
|
||||
.build(),
|
||||
)
|
||||
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
|
||||
.prepare()
|
||||
|
||||
fun getHistoryByMangaId(mangaId: Long) = db.get()
|
||||
.listOfObjects(History::class.java)
|
||||
.withQuery(
|
||||
@ -37,9 +65,9 @@ interface HistoryQueries : DbProvider {
|
||||
* Inserts history object if not yet in database
|
||||
* @param history history object
|
||||
*/
|
||||
fun upsertHistoryLastRead(history: History) = db.put()
|
||||
fun updateHistoryLastRead(history: History) = db.put()
|
||||
.`object`(history)
|
||||
.withPutResolver(HistoryUpsertResolver())
|
||||
.withPutResolver(HistoryLastReadPutResolver())
|
||||
.prepare()
|
||||
|
||||
/**
|
||||
@ -47,12 +75,12 @@ interface HistoryQueries : DbProvider {
|
||||
* Inserts history object if not yet in database
|
||||
* @param historyList history object list
|
||||
*/
|
||||
fun upsertHistoryLastRead(historyList: List<History>) = db.put()
|
||||
fun updateHistoryLastRead(historyList: List<History>) = db.put()
|
||||
.objects(historyList)
|
||||
.withPutResolver(HistoryUpsertResolver())
|
||||
.withPutResolver(HistoryLastReadPutResolver())
|
||||
.prepare()
|
||||
|
||||
fun dropHistoryTable() = db.delete()
|
||||
fun deleteHistory() = db.delete()
|
||||
.byQuery(
|
||||
DeleteQuery.builder()
|
||||
.table(HistoryTable.TABLE)
|
||||
|
@ -1,6 +1,8 @@
|
||||
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
|
||||
@ -74,6 +76,32 @@ 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.
|
||||
*/
|
||||
@ -162,8 +190,7 @@ fun getRecentMangasQuery(search: String = "") =
|
||||
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
|
||||
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
|
||||
) AS max_last_read
|
||||
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
|
||||
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
|
||||
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
|
||||
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
|
||||
|
@ -11,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 HistoryUpsertResolver : HistoryPutResolver() {
|
||||
class HistoryLastReadPutResolver : HistoryPutResolver() {
|
||||
|
||||
/**
|
||||
* Updates last_read time of chapter
|
@ -15,4 +15,19 @@ 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 <--
|
||||
}
|
||||
|
@ -27,4 +27,42 @@ object ChapterTable {
|
||||
const val COL_CHAPTER_NUMBER = "chapter_number"
|
||||
|
||||
const val COL_SOURCE_ORDER = "source_order"
|
||||
|
||||
val createTableQuery: String
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_MANGA_ID INTEGER NOT NULL,
|
||||
$COL_URL TEXT NOT NULL,
|
||||
$COL_NAME TEXT NOT NULL,
|
||||
$COL_SCANLATOR TEXT,
|
||||
$COL_READ BOOLEAN NOT NULL,
|
||||
$COL_BOOKMARK BOOLEAN NOT NULL,
|
||||
$COL_LAST_PAGE_READ INT NOT NULL,
|
||||
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
|
||||
$COL_SOURCE_ORDER INTEGER NOT NULL,
|
||||
$COL_DATE_FETCH LONG NOT NULL,
|
||||
$COL_DATE_UPLOAD LONG NOT NULL,
|
||||
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
|
||||
ON DELETE CASCADE
|
||||
)"""
|
||||
|
||||
val createMangaIdIndexQuery: String
|
||||
get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)"
|
||||
|
||||
val createUnreadChaptersIndexQuery: String
|
||||
get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " +
|
||||
"WHERE $COL_READ = 0"
|
||||
|
||||
val sourceOrderUpdateQuery: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
|
||||
|
||||
val bookmarkUpdateQuery: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
|
||||
|
||||
val addScanlator: String
|
||||
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
|
||||
|
||||
val fixDateUploadIfNeeded: String
|
||||
get() = "UPDATE $TABLE SET $COL_DATE_UPLOAD = $COL_DATE_FETCH WHERE $COL_DATE_UPLOAD = 0"
|
||||
}
|
||||
|
@ -26,4 +26,24 @@ object HistoryTable {
|
||||
* Time read column name
|
||||
*/
|
||||
const val COL_TIME_READ = "${TABLE}_time_read"
|
||||
|
||||
/**
|
||||
* query to create history table
|
||||
*/
|
||||
val createTableQuery: String
|
||||
get() =
|
||||
"""CREATE TABLE $TABLE(
|
||||
$COL_ID INTEGER NOT NULL PRIMARY KEY,
|
||||
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
|
||||
$COL_LAST_READ LONG,
|
||||
$COL_TIME_READ LONG,
|
||||
FOREIGN KEY($COL_CHAPTER_ID) REFERENCES ${ChapterTable.TABLE} (${ChapterTable.COL_ID})
|
||||
ON DELETE CASCADE
|
||||
)"""
|
||||
|
||||
/**
|
||||
* query to index history chapter id
|
||||
*/
|
||||
val createChapterIdIndexQuery: String
|
||||
get() = "CREATE INDEX ${TABLE}_${COL_CHAPTER_ID}_index ON $TABLE($COL_CHAPTER_ID)"
|
||||
}
|
||||
|
@ -9,4 +9,16 @@ 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
|
||||
)"""
|
||||
}
|
||||
|
@ -51,4 +51,59 @@ 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 <--
|
||||
}
|
||||
|
@ -30,6 +30,43 @@ 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() =
|
||||
"""
|
||||
@ -37,4 +74,7 @@ object TrackTable {
|
||||
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|
||||
|FROM ${TABLE}_tmp
|
||||
""".trimMargin()
|
||||
|
||||
val dropTempTable: String
|
||||
get() = "DROP TABLE ${TABLE}_tmp"
|
||||
}
|
||||
|
@ -166,7 +166,7 @@ class NotificationReceiver : BroadcastReceiver() {
|
||||
* @param chapterId id of chapter
|
||||
*/
|
||||
private fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
|
||||
val db = Injekt.get<DatabaseHelper>()
|
||||
val db = DatabaseHelper(context)
|
||||
val manga = db.getManga(mangaId).executeAsBlocking()
|
||||
val chapter = db.getChapter(chapterId).executeAsBlocking()
|
||||
if (manga != null && chapter != null) {
|
||||
|
@ -1,26 +0,0 @@
|
||||
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<P : Presenter<*>> : NucleusController<ComposeControllerBinding, P>() {
|
||||
|
||||
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()
|
||||
}
|
@ -18,7 +18,6 @@ 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
|
||||
@ -67,45 +66,41 @@ open class FeedController :
|
||||
}
|
||||
|
||||
private fun addFeed() {
|
||||
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()
|
||||
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
|
||||
|
||||
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) {
|
||||
viewScope.launchUI {
|
||||
val items = presenter.getSourceSavedSearches(source)
|
||||
val itemsStrings = listOf(activity!!.getString(R.string.latest)) + items.map { it.name }
|
||||
var selectedIndex = 0
|
||||
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()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1,9 +1,6 @@
|
||||
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
|
||||
@ -18,12 +15,9 @@ 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
|
||||
@ -41,12 +35,11 @@ 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 database manages the database calls.
|
||||
* @param db 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<FeedController>() {
|
||||
@ -69,11 +62,14 @@ open class FeedPresenter(
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
database.subscribeToList { feed_saved_searchQueries.selectAllGlobal() }
|
||||
.onEach {
|
||||
db.getGlobalFeedSavedSearches()
|
||||
.asRxObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnEach {
|
||||
getFeed()
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
.subscribe()
|
||||
.let(::add)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@ -82,10 +78,8 @@ open class FeedPresenter(
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
suspend fun hasTooManyFeeds(): Boolean {
|
||||
return withIOContext {
|
||||
database.awaitList { feed_saved_searchQueries.selectAllGlobal() }.size > 10
|
||||
}
|
||||
fun hasTooManyFeeds(): Boolean {
|
||||
return db.getGlobalFeedSavedSearches().executeAsBlocking().size > 10
|
||||
}
|
||||
|
||||
fun getEnabledSources(): List<CatalogueSource> {
|
||||
@ -99,38 +93,33 @@ open class FeedPresenter(
|
||||
return list.sortedBy { it.id.toString() !in pinnedSources }
|
||||
}
|
||||
|
||||
suspend fun getSourceSavedSearches(source: CatalogueSource): List<SavedSearch> {
|
||||
return withIOContext {
|
||||
database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }
|
||||
}
|
||||
fun getSourceSavedSearches(source: CatalogueSource): List<SavedSearch> {
|
||||
return db.getSavedSearches(source.id).executeAsBlocking()
|
||||
}
|
||||
|
||||
fun createFeed(source: CatalogueSource, savedSearch: SavedSearch?) {
|
||||
launchIO {
|
||||
database.await {
|
||||
feed_saved_searchQueries.insertFeedSavedSearch(
|
||||
_id = null,
|
||||
db.insertFeedSavedSearch(
|
||||
FeedSavedSearch(
|
||||
id = null,
|
||||
source = source.id,
|
||||
saved_search = savedSearch?.id,
|
||||
savedSearch = savedSearch?.id,
|
||||
global = true,
|
||||
)
|
||||
}
|
||||
),
|
||||
).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFeed(feed: FeedSavedSearch) {
|
||||
launchIO {
|
||||
database.await {
|
||||
feed_saved_searchQueries.deleteById(feed.id ?: return@await)
|
||||
}
|
||||
db.deleteFeedSavedSearch(feed).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSourcesToGetFeed(): List<Pair<FeedSavedSearch, SavedSearch?>> {
|
||||
val savedSearches = database.awaitList {
|
||||
feed_saved_searchQueries.selectGlobalFeedSavedSearch(savedSearchMapper)
|
||||
}.associateBy { it.id }
|
||||
return database.awaitList { feed_saved_searchQueries.selectAllGlobal(feedSavedSearchMapper) }
|
||||
private fun getSourcesToGetFeed(): List<Pair<FeedSavedSearch, SavedSearch?>> {
|
||||
val savedSearches = db.getGlobalSavedSearchesFeed().executeAsBlocking()
|
||||
.associateBy { it.id!! }
|
||||
return db.getGlobalFeedSavedSearches().executeAsBlocking()
|
||||
.map { it to savedSearches[it.savedSearch] }
|
||||
}
|
||||
|
||||
@ -149,7 +138,7 @@ open class FeedPresenter(
|
||||
/**
|
||||
* Initiates get manga per feed.
|
||||
*/
|
||||
suspend fun getFeed() {
|
||||
fun getFeed() {
|
||||
// Create image fetch subscription
|
||||
initializeFetchImageSubscription()
|
||||
|
||||
|
@ -137,7 +137,7 @@ class MigrationProcessAdapter(
|
||||
}
|
||||
}
|
||||
db.insertChapters(dbChapters).executeAsBlocking()
|
||||
db.upsertHistoryLastRead(historyList).executeAsBlocking()
|
||||
db.updateHistoryLastRead(historyList).executeAsBlocking()
|
||||
}
|
||||
// Update categories
|
||||
if (MigrationFlags.hasCategories(flags)) {
|
||||
|
@ -37,8 +37,8 @@ class SearchController(
|
||||
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking(),
|
||||
sources.map { Injekt.get<SourceManager>().getOrStub(it) }.filterIsInstance<CatalogueSource>(),
|
||||
) {
|
||||
this.targetController = targetController
|
||||
}
|
||||
this.targetController = targetController
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : this(
|
||||
|
@ -44,7 +44,6 @@ 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
|
||||
@ -202,7 +201,7 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
// SY -->
|
||||
this,
|
||||
presenter.source,
|
||||
emptyList(),
|
||||
presenter.loadSearches(),
|
||||
// SY <--
|
||||
onFilterClicked = {
|
||||
showProgressBar()
|
||||
@ -217,58 +216,54 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
},
|
||||
// EXH -->
|
||||
onSaveClicked = {
|
||||
viewScope.launchUI {
|
||||
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) {
|
||||
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)
|
||||
.setTitle(R.string.save_search_failed_to_load)
|
||||
.setMessage(R.string.save_search_failed_to_load_message)
|
||||
.show()
|
||||
}
|
||||
return@cb
|
||||
}
|
||||
},
|
||||
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()
|
||||
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 = { idToDelete, name ->
|
||||
onSavedSearchDeleteClicked = cb@{ idToDelete, name ->
|
||||
filterSheet?.context?.let {
|
||||
MaterialAlertDialogBuilder(it)
|
||||
.setTitle(R.string.save_search_delete)
|
||||
@ -282,9 +277,6 @@ open class BrowseSourceController(bundle: Bundle) :
|
||||
},
|
||||
// EXH <--
|
||||
)
|
||||
launchUI {
|
||||
filterSheet?.setSavedSearches(presenter.loadSearches())
|
||||
}
|
||||
filterSheet?.setFilters(presenter.filterItems)
|
||||
|
||||
filterSheet?.setOnShowListener { actionFab?.hide() }
|
||||
|
@ -2,8 +2,6 @@ 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
|
||||
@ -39,7 +37,6 @@ 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
|
||||
@ -53,10 +50,8 @@ 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
|
||||
@ -83,7 +78,6 @@ 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<BrowseSourceController>() {
|
||||
@ -146,11 +140,7 @@ open class BrowseSourcePresenter(
|
||||
val jsonFilters = filters
|
||||
if (savedSearchFilters != null) {
|
||||
runCatching {
|
||||
val savedSearch = runBlocking {
|
||||
database.awaitOneOrNull {
|
||||
saved_searchQueries.selectById(savedSearchFilters, savedSearchMapper)
|
||||
}
|
||||
} ?: return@runCatching
|
||||
val savedSearch = db.getSavedSearch(savedSearchFilters).executeAsBlocking() ?: return@runCatching
|
||||
query = savedSearch.query.orEmpty()
|
||||
val filtersJson = savedSearch.filtersJson
|
||||
?: return@runCatching
|
||||
@ -166,14 +156,18 @@ open class BrowseSourcePresenter(
|
||||
}
|
||||
}
|
||||
|
||||
database.subscribeToList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }
|
||||
.map { loadSearches(it) }
|
||||
.onEach {
|
||||
withUIContext {
|
||||
view?.setSavedSearches(it)
|
||||
}
|
||||
db.getSavedSearches(source.id)
|
||||
.asRxObservable()
|
||||
.map {
|
||||
loadSearches(it)
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(
|
||||
{ controller, savedSearches ->
|
||||
controller.setSavedSearches(savedSearches)
|
||||
},
|
||||
)
|
||||
// SY <--
|
||||
|
||||
if (savedState != null) {
|
||||
@ -491,90 +485,83 @@ open class BrowseSourcePresenter(
|
||||
fun saveSearch(name: String, query: String, filterList: FilterList) {
|
||||
launchIO {
|
||||
kotlin.runCatching {
|
||||
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) },
|
||||
)
|
||||
}
|
||||
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()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteSearch(searchId: Long) {
|
||||
launchIO {
|
||||
database.await { saved_searchQueries.deleteById(searchId) }
|
||||
db.deleteSavedSearch(searchId).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
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<JsonArray>(it) }
|
||||
?: return@runCatching null,
|
||||
)
|
||||
originalFilters
|
||||
}.getOrNull(),
|
||||
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<JsonArray>(it) }
|
||||
?: return@runCatching null,
|
||||
)
|
||||
originalFilters
|
||||
}.getOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
fun loadSearches(searches: List<SavedSearch> = db.getSavedSearches(source.id).executeAsBlocking()): List<EXHSavedSearch> {
|
||||
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<JsonArray>(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<SavedSearch>? = null): List<EXHSavedSearch> {
|
||||
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<JsonArray>(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 <--
|
||||
|
@ -24,8 +24,6 @@ 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
|
||||
@ -186,7 +184,7 @@ open class SourceFeedController :
|
||||
// SY -->
|
||||
this,
|
||||
presenter.source,
|
||||
emptyList(),
|
||||
presenter.loadSearches(),
|
||||
// SY <--
|
||||
onFilterClicked = {
|
||||
val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
|
||||
@ -204,60 +202,51 @@ open class SourceFeedController :
|
||||
},
|
||||
onResetClicked = {},
|
||||
onSaveClicked = {},
|
||||
onSavedSearchClicked = { idOfSearch ->
|
||||
viewScope.launchUI {
|
||||
val search = presenter.loadSearch(idOfSearch)
|
||||
onSavedSearchClicked = cb@{ idOfSearch ->
|
||||
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()
|
||||
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)
|
||||
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@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
|
||||
|
@ -2,9 +2,6 @@ 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
|
||||
@ -19,15 +16,11 @@ 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
|
||||
@ -53,12 +46,11 @@ sealed class SourceFeed {
|
||||
* Function calls should be done from here. UI calls should be done from the controller.
|
||||
*
|
||||
* @param source the source.
|
||||
* @param database manages the database calls.
|
||||
* @param db 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<SourceFeedController>() {
|
||||
@ -98,11 +90,14 @@ open class SourceFeedPresenter(
|
||||
|
||||
sourceFilters = source.getFilterList()
|
||||
|
||||
database.subscribeToList { feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id, savedSearchMapper) }
|
||||
.onEach {
|
||||
db.getSourceFeedSavedSearches(source.id)
|
||||
.asRxObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnEach {
|
||||
getFeed()
|
||||
}
|
||||
.launchIn(presenterScope)
|
||||
.subscribe()
|
||||
.let(::add)
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
@ -111,39 +106,35 @@ open class SourceFeedPresenter(
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
suspend fun hasTooManyFeeds(): Boolean {
|
||||
return withIOContext {
|
||||
database.awaitList {
|
||||
feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id)
|
||||
}.size > 10
|
||||
}
|
||||
fun hasTooManyFeeds(): Boolean {
|
||||
return db.getSourceFeedSavedSearches(source.id).executeAsBlocking().size > 10
|
||||
}
|
||||
|
||||
suspend fun getSourceSavedSearches(): List<SavedSearch> {
|
||||
return database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }
|
||||
fun getSourceSavedSearches(): List<SavedSearch> {
|
||||
return db.getSavedSearches(source.id).executeAsBlocking()
|
||||
}
|
||||
|
||||
fun createFeed(savedSearchId: Long) {
|
||||
launchIO {
|
||||
database.await {
|
||||
feed_saved_searchQueries.insertFeedSavedSearch(
|
||||
_id = null,
|
||||
db.insertFeedSavedSearch(
|
||||
FeedSavedSearch(
|
||||
id = null,
|
||||
source = source.id,
|
||||
saved_search = savedSearchId,
|
||||
global = false
|
||||
)
|
||||
}
|
||||
savedSearch = savedSearchId,
|
||||
global = false,
|
||||
),
|
||||
).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteFeed(feed: FeedSavedSearch) {
|
||||
launchIO {
|
||||
database.await { feed_saved_searchQueries.deleteById(feed.id ?: return@await) }
|
||||
db.deleteFeedSavedSearch(feed).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
private suspend fun getSourcesToGetFeed(): List<SourceFeed> {
|
||||
val savedSearches = database.awaitList { feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id, savedSearchMapper) }
|
||||
private fun getSourcesToGetFeed(): List<SourceFeed> {
|
||||
val savedSearches = db.getSourceSavedSearchesFeed(source.id).executeAsBlocking()
|
||||
.associateBy { it.id!! }
|
||||
|
||||
return listOfNotNull(
|
||||
@ -151,7 +142,7 @@ open class SourceFeedPresenter(
|
||||
SourceFeed.Latest
|
||||
} else null,
|
||||
SourceFeed.Browse,
|
||||
) + database.awaitList { feed_saved_searchQueries.selectBySource(source.id, feedSavedSearchMapper) }
|
||||
) + db.getSourceFeedSavedSearches(source.id).executeAsBlocking()
|
||||
.map { SourceFeed.SourceSavedSearch(it, savedSearches[it.savedSearch]!!) }
|
||||
}
|
||||
|
||||
@ -168,7 +159,7 @@ open class SourceFeedPresenter(
|
||||
/**
|
||||
* Initiates get manga per feed.
|
||||
*/
|
||||
suspend fun getFeed() {
|
||||
fun getFeed() {
|
||||
// Create image fetch subscription
|
||||
initializeFetchImageSubscription()
|
||||
|
||||
@ -309,69 +300,62 @@ open class SourceFeedPresenter(
|
||||
return localManga
|
||||
}
|
||||
|
||||
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<JsonArray>(it) }
|
||||
?: return@runCatching null,
|
||||
)
|
||||
originalFilters
|
||||
}.getOrNull(),
|
||||
)
|
||||
}
|
||||
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<JsonArray>(it) }
|
||||
?: return@runCatching null,
|
||||
)
|
||||
originalFilters
|
||||
}.getOrNull(),
|
||||
)
|
||||
}
|
||||
|
||||
suspend fun loadSearches(): List<EXHSavedSearch> {
|
||||
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<JsonArray>(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,
|
||||
)
|
||||
fun loadSearches(): List<EXHSavedSearch> {
|
||||
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<JsonArray>(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(
|
||||
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,
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -35,7 +35,6 @@ 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
|
||||
@ -137,8 +136,6 @@ 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),
|
||||
|
@ -0,0 +1,24 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -127,19 +127,14 @@ import kotlin.time.Duration.Companion.seconds
|
||||
class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
||||
|
||||
companion object {
|
||||
|
||||
fun newIntent(context: Context, mangaId: Long?, chapterId: Long?): Intent {
|
||||
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
|
||||
return Intent(context, ReaderActivity::class.java).apply {
|
||||
putExtra("manga", mangaId)
|
||||
putExtra("chapter", chapterId)
|
||||
putExtra("manga", manga.id)
|
||||
putExtra("chapter", chapter.id)
|
||||
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"
|
||||
|
@ -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.upsertHistoryLastRead(history).asRxCompletable()
|
||||
db.updateHistoryLastRead(history).asRxCompletable()
|
||||
.onErrorComplete()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
|
@ -1,21 +0,0 @@
|
||||
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()
|
||||
}
|
||||
}
|
@ -0,0 +1,51 @@
|
||||
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<IFlexible<*>>(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)
|
||||
}
|
||||
}
|
@ -1,53 +1,193 @@
|
||||
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.compose.runtime.Composable
|
||||
import eu.kanade.domain.chapter.model.Chapter
|
||||
import eu.kanade.presentation.history.HistoryScreen
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import dev.chrisbanes.insetter.applyInsetter
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
|
||||
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.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
|
||||
|
||||
class HistoryController : ComposeController<HistoryPresenter>(), RootController {
|
||||
/**
|
||||
* Fragment that shows recently read manga.
|
||||
*/
|
||||
class HistoryController :
|
||||
NucleusController<HistoryControllerBinding, HistoryPresenter>(),
|
||||
RootController,
|
||||
FlexibleAdapter.OnUpdateListener,
|
||||
FlexibleAdapter.EndlessScrollListener,
|
||||
HistoryAdapter.OnRemoveClickListener,
|
||||
HistoryAdapter.OnResumeClickListener,
|
||||
HistoryAdapter.OnItemClickListener,
|
||||
RemoveHistoryDialog.Listener {
|
||||
|
||||
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() = resources?.getString(R.string.label_recent_manga)
|
||||
override fun getTitle(): String? {
|
||||
return resources?.getString(R.string.label_recent_manga)
|
||||
}
|
||||
|
||||
override fun createPresenter() = HistoryPresenter()
|
||||
override fun createPresenter(): HistoryPresenter {
|
||||
return HistoryPresenter()
|
||||
}
|
||||
|
||||
@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 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<HistoryItem>, 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)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
@ -61,33 +201,46 @@ class HistoryController : ComposeController<HistoryPresenter>(), RootController
|
||||
searchView.clearFocus()
|
||||
}
|
||||
searchView.queryTextChanges()
|
||||
.drop(1) // Drop first event after subscribed
|
||||
.filter { router.backstack.lastOrNull()?.controller == this }
|
||||
.onEach {
|
||||
query = it.toString()
|
||||
presenter.search(query)
|
||||
presenter.updateList(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 {
|
||||
return when (item.itemId) {
|
||||
when (item.itemId) {
|
||||
R.id.action_clear_history -> {
|
||||
val dialog = ClearHistoryDialogController()
|
||||
dialog.targetController = this@HistoryController
|
||||
dialog.showDialog(router)
|
||||
true
|
||||
val ctrl = ClearHistoryDialogController()
|
||||
ctrl.targetController = this@HistoryController
|
||||
ctrl.showDialog(router)
|
||||
}
|
||||
else -> super.onOptionsItemSelected(item)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
private fun clearHistory() {
|
||||
db.deleteHistory().executeAsBlocking()
|
||||
activity?.toast(R.string.clear_history_completed)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,71 @@
|
||||
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)
|
||||
}
|
||||
}
|
@ -0,0 +1,42 @@
|
||||
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<HistoryHolder, DateSectionItem>(header) {
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.history_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): HistoryHolder {
|
||||
return HistoryHolder(view, adapter as HistoryAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(
|
||||
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: HistoryHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?,
|
||||
) {
|
||||
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()
|
||||
}
|
||||
}
|
@ -1,127 +1,157 @@
|
||||
package eu.kanade.tachiyomi.ui.recent.history
|
||||
|
||||
import android.os.Bundle
|
||||
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.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 eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.lang.launchIO
|
||||
import eu.kanade.tachiyomi.util.lang.launchUI
|
||||
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
|
||||
import eu.kanade.tachiyomi.util.lang.toDateKey
|
||||
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 rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import uy.kohesive.injekt.injectLazy
|
||||
import java.text.DateFormat
|
||||
import java.util.Calendar
|
||||
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(
|
||||
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<HistoryController>() {
|
||||
class HistoryPresenter : BasePresenter<HistoryController>() {
|
||||
|
||||
private var _query: MutableStateFlow<String> = MutableStateFlow("")
|
||||
private var _state: MutableStateFlow<HistoryState> = MutableStateFlow(HistoryState.EMPTY)
|
||||
val state: StateFlow<HistoryState> = _state
|
||||
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
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
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),
|
||||
)
|
||||
// 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<List<HistoryItem>> {
|
||||
// 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<Date, MutableList<MangaChapterHistory>> { 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())
|
||||
}
|
||||
|
||||
fun search(query: String) {
|
||||
presenterScope.launchIO {
|
||||
_query.emit(query)
|
||||
}
|
||||
/**
|
||||
* 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()
|
||||
}
|
||||
|
||||
fun removeFromHistory(history: HistoryWithRelations) {
|
||||
presenterScope.launchIO {
|
||||
removeHistoryById.await(history)
|
||||
}
|
||||
/**
|
||||
* 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) {
|
||||
presenterScope.launchIO {
|
||||
removeHistoryByMangaId.await(mangaId)
|
||||
}
|
||||
}
|
||||
|
||||
fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
|
||||
presenterScope.launchIO {
|
||||
val chapter = getNextChapterForManga.await(mangaId, chapterId)
|
||||
launchUI {
|
||||
view?.openChapter(chapter)
|
||||
db.getHistoryByMangaId(mangaId).asRxSingle()
|
||||
.map { list ->
|
||||
list.forEach { it.last_read = 0L }
|
||||
db.updateHistoryLastRead(list).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
fun deleteAllHistory() {
|
||||
presenterScope.launchIO {
|
||||
val result = deleteHistoryTable.await()
|
||||
if (!result) return@launchIO
|
||||
launchUI {
|
||||
view?.activity?.toast(R.string.clear_history_completed)
|
||||
/**
|
||||
* 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
|
||||
}
|
||||
}
|
||||
Manga.CHAPTER_SORTING_UPLOAD_DATE -> {
|
||||
chapters.drop(currChapterIndex + 1)
|
||||
.firstOrNull { it.date_upload >= chapter.date_upload }
|
||||
}
|
||||
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<PagingData<UiModel>>? = null,
|
||||
) {
|
||||
|
||||
companion object {
|
||||
val EMPTY = HistoryState(null)
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,54 @@
|
||||
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<T>(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)
|
||||
}
|
||||
}
|
@ -37,6 +37,7 @@ class ClearDatabaseController :
|
||||
private var menu: Menu? = null
|
||||
|
||||
private var actionFab: ExtendedFloatingActionButton? = null
|
||||
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
@ -142,6 +143,7 @@ class ClearDatabaseController :
|
||||
|
||||
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
|
||||
actionFab?.setOnClickListener(null)
|
||||
actionFabScrollListener?.let { recycler?.removeOnScrollListener(it) }
|
||||
actionFab = null
|
||||
}
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
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
|
||||
@ -14,7 +13,6 @@ import uy.kohesive.injekt.api.get
|
||||
class ClearDatabasePresenter : BasePresenter<ClearDatabaseController>() {
|
||||
|
||||
private val db = Injekt.get<DatabaseHelper>()
|
||||
private val database = Injekt.get<Database>()
|
||||
|
||||
private val sourceManager = Injekt.get<SourceManager>()
|
||||
|
||||
@ -34,7 +32,7 @@ class ClearDatabasePresenter : BasePresenter<ClearDatabaseController>() {
|
||||
db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking()
|
||||
}
|
||||
// SY <--
|
||||
database.historyQueries.removeResettedHistory()
|
||||
db.deleteHistoryNoLastRead().executeAsBlocking()
|
||||
}
|
||||
|
||||
private fun getDatabaseSourcesObservable(): Observable<List<ClearDatabaseSourceItem>> {
|
||||
|
@ -5,10 +5,8 @@ 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
|
||||
@ -65,16 +63,7 @@ class TachiyomiCoordinatorLayout @JvmOverloads constructor(
|
||||
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed)
|
||||
// Disable elevation overlay when tabs are visible
|
||||
if (canLiftAppBarOnScroll) {
|
||||
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
|
||||
}
|
||||
appBarLayout?.isLifted = (dyConsumed != 0 || dyUnconsumed >= 0) && tabLayout?.isVisible == false
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -6,7 +6,6 @@ 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
|
||||
@ -41,6 +40,8 @@ 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
|
||||
@ -50,7 +51,6 @@ 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,7 +69,6 @@ import java.net.URISyntaxException
|
||||
|
||||
object EXHMigrations {
|
||||
private val db: DatabaseHelper by injectLazy()
|
||||
private val database: DatabaseHandler by injectLazy()
|
||||
private val sourceManager: SourceManager by injectLazy()
|
||||
|
||||
/**
|
||||
@ -405,31 +404,31 @@ object EXHMigrations {
|
||||
BackupCreatorJob.setupTask(context)
|
||||
}
|
||||
if (oldVersion under 31) {
|
||||
runBlocking {
|
||||
database.await(true) {
|
||||
prefs.getStringSet("eh_saved_searches", emptySet())?.forEach {
|
||||
kotlin.runCatching {
|
||||
val content = Json.decodeFromString<JsonObject>(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,
|
||||
)
|
||||
}
|
||||
}
|
||||
val savedSearches = prefs.getStringSet("eh_saved_searches", emptySet())?.mapNotNull {
|
||||
kotlin.runCatching {
|
||||
val content = Json.decodeFromString<JsonObject>(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()
|
||||
}
|
||||
prefs.edit(commit = true) {
|
||||
remove("eh_saved_searches")
|
||||
|
@ -3,7 +3,6 @@ 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
|
||||
@ -35,7 +34,6 @@ 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()
|
||||
|
||||
@ -166,7 +164,7 @@ object DebugFunctions {
|
||||
it.favorite && db.getSearchMetadataForManga(it.id!!).executeAsBlocking() == null
|
||||
}
|
||||
|
||||
fun clearSavedSearches() = runBlocking { database.await { saved_searchQueries.deleteAll() } }
|
||||
fun clearSavedSearches() = db.deleteAllSavedSearches().executeAsBlocking()
|
||||
|
||||
fun listAllSources() = sourceManager.getCatalogueSources().joinToString("\n") {
|
||||
"${it.id}: ${it.name} (${it.lang.uppercase()})"
|
||||
|
@ -13,4 +13,20 @@ 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",
|
||||
)
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package exh.merged.sql.tables
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
object MergedTable {
|
||||
|
||||
const val TABLE = "merged"
|
||||
@ -25,4 +27,30 @@ 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)"
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package exh.metadata.sql.tables
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
object SearchMetadataTable {
|
||||
const val TABLE = "search_metadata"
|
||||
|
||||
@ -12,4 +14,23 @@ 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)"
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package exh.metadata.sql.tables
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
object SearchTagTable {
|
||||
const val TABLE = "search_tags"
|
||||
|
||||
@ -12,4 +14,22 @@ 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)"
|
||||
}
|
||||
|
@ -1,5 +1,7 @@
|
||||
package exh.metadata.sql.tables
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable
|
||||
|
||||
object SearchTitleTable {
|
||||
const val TABLE = "search_titles"
|
||||
|
||||
@ -10,4 +12,21 @@ 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)"
|
||||
}
|
||||
|
@ -1,5 +1,86 @@
|
||||
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
|
||||
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<FeedSavedSearch>) = 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<SavedSearch>) {
|
||||
db.inTransaction {
|
||||
deleteSavedSearches(mergedMangaId).executeAsBlocking()
|
||||
mergedMangases.chunked(100) { chunk ->
|
||||
insertSavedSearches(chunk).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
@ -1,5 +1,93 @@
|
||||
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
|
||||
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<Long>) = 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<SavedSearch>) = 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<SavedSearch>) {
|
||||
db.inTransaction {
|
||||
deleteSavedSearches(mergedMangaId).executeAsBlocking()
|
||||
mergedMangases.chunked(100) { chunk ->
|
||||
insertSavedSearches(chunk).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
}*/
|
||||
}
|
||||
|
@ -11,4 +11,18 @@ 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)"
|
||||
}
|
||||
|
@ -13,4 +13,14 @@ 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
|
||||
)"""
|
||||
}
|
||||
|
@ -1,4 +0,0 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.compose.ui.platform.ComposeView xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent" />
|
33
app/src/main/res/layout/history_controller.xml
Normal file
33
app/src/main/res/layout/history_controller.xml
Normal file
@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:orientation="vertical">
|
||||
|
||||
<androidx.recyclerview.widget.RecyclerView
|
||||
android:id="@+id/recycler"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="match_parent"
|
||||
android:clipToPadding="false"
|
||||
android:paddingTop="4dp"
|
||||
android:paddingBottom="@dimen/action_toolbar_list_padding"
|
||||
tools:listitem="@layout/history_item" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.MaterialFastScroll
|
||||
android:id="@+id/fast_scroller"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="match_parent"
|
||||
android:layout_gravity="end"
|
||||
app:fastScrollerBubbleEnabled="false"
|
||||
tools:visibility="visible" />
|
||||
|
||||
<eu.kanade.tachiyomi.widget.EmptyView
|
||||
android:id="@+id/empty_view"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_gravity="center"
|
||||
android:visibility="gone" />
|
||||
|
||||
</FrameLayout>
|
85
app/src/main/res/layout/history_item.xml
Normal file
85
app/src/main/res/layout/history_item.xml
Normal file
@ -0,0 +1,85 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:id="@+id/holder"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="96dp"
|
||||
android:paddingStart="16dp"
|
||||
android:paddingTop="8dp"
|
||||
android:paddingEnd="8dp"
|
||||
android:paddingBottom="8dp"
|
||||
android:background="?attr/selectableItemBackground"
|
||||
android:orientation="horizontal">
|
||||
|
||||
<com.google.android.material.imageview.ShapeableImageView
|
||||
android:id="@+id/cover"
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="match_parent"
|
||||
android:contentDescription="@string/description_cover"
|
||||
android:scaleType="centerCrop"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintDimensionRatio="h,3:2"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:shapeAppearance="@style/ShapeAppearanceOverlay.Cover"
|
||||
tools:src="@mipmap/ic_launcher" />
|
||||
|
||||
<LinearLayout
|
||||
android:layout_width="0dp"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginStart="16dp"
|
||||
android:layout_marginEnd="8dp"
|
||||
android:orientation="vertical"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/remove"
|
||||
app:layout_constraintStart_toEndOf="@+id/cover"
|
||||
app:layout_constraintTop_toTopOf="parent">
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_title"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:ellipsize="end"
|
||||
android:maxLines="2"
|
||||
android:textAppearance="?attr/textAppearanceTitleSmall"
|
||||
tools:text="Title" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/manga_subtitle"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="wrap_content"
|
||||
android:layout_marginTop="2dp"
|
||||
android:textAppearance="?attr/textAppearanceBodyMedium"
|
||||
android:textColor="?android:attr/textColorSecondary"
|
||||
tools:text="Subtitle" />
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/remove"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/action_resume"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toStartOf="@+id/resume"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_delete_24dp"
|
||||
app:tint="?android:attr/textColorPrimary" />
|
||||
|
||||
<ImageButton
|
||||
android:id="@+id/resume"
|
||||
android:layout_width="wrap_content"
|
||||
android:layout_height="wrap_content"
|
||||
android:background="?attr/selectableItemBackgroundBorderless"
|
||||
android:contentDescription="@string/action_resume"
|
||||
android:padding="8dp"
|
||||
app:layout_constraintBottom_toBottomOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toTopOf="parent"
|
||||
app:srcCompat="@drawable/ic_play_arrow_24dp"
|
||||
app:tint="?android:attr/textColorPrimary" />
|
||||
|
||||
</androidx.constraintlayout.widget.ConstraintLayout>
|
@ -1,7 +0,0 @@
|
||||
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
|
||||
);
|
@ -1,29 +0,0 @@
|
||||
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;
|
@ -1,16 +0,0 @@
|
||||
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;
|
@ -1,41 +0,0 @@
|
||||
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;
|
@ -1,37 +0,0 @@
|
||||
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;
|
@ -1,18 +0,0 @@
|
||||
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
|
||||
);
|
@ -1,32 +0,0 @@
|
||||
import java.lang.String;
|
||||
import kotlin.collections.List;
|
||||
|
||||
CREATE TABLE mangas(
|
||||
_id INTEGER NOT NULL PRIMARY KEY,
|
||||
source INTEGER NOT NULL,
|
||||
url TEXT NOT NULL,
|
||||
artist TEXT,
|
||||
author TEXT,
|
||||
description TEXT,
|
||||
genre TEXT AS List<String>,
|
||||
title TEXT NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
thumbnail_url TEXT,
|
||||
favorite INTEGER AS Boolean NOT NULL,
|
||||
last_update INTEGER AS Long,
|
||||
next_update INTEGER AS Long,
|
||||
initialized INTEGER AS Boolean NOT NULL,
|
||||
viewer INTEGER NOT NULL,
|
||||
chapter_flags INTEGER NOT NULL,
|
||||
cover_last_modified INTEGER AS Long NOT NULL,
|
||||
date_added INTEGER AS Long NOT NULL,
|
||||
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;
|
@ -1,9 +0,0 @@
|
||||
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
|
||||
);
|
@ -1,85 +0,0 @@
|
||||
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;
|
@ -1,32 +0,0 @@
|
||||
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;
|
@ -1,32 +0,0 @@
|
||||
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;
|
@ -1,35 +0,0 @@
|
||||
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;
|
@ -1,30 +0,0 @@
|
||||
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;
|
@ -1,2 +0,0 @@
|
||||
ALTER TABLE mangas
|
||||
ADD COLUMN cover_last_modified INTEGER NOT NULL DEFAULT 0;
|
@ -1,7 +0,0 @@
|
||||
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
|
||||
);
|
@ -1,7 +0,0 @@
|
||||
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
|
||||
);
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user