Revert history Compose/SQLDelight changes

This commit is contained in:
Jobobby04 2022-04-22 19:24:50 -04:00
parent 070e2d94c7
commit 6a0b523e86
118 changed files with 1704 additions and 2694 deletions

View File

@ -7,7 +7,6 @@ plugins {
kotlin("plugin.parcelize") kotlin("plugin.parcelize")
kotlin("plugin.serialization") kotlin("plugin.serialization")
id("com.github.zellius.shortcut-helper") id("com.github.zellius.shortcut-helper")
id("com.squareup.sqldelight")
} }
if (gradle.startParameter.taskRequests.toString().contains("Standard")) { if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
@ -93,7 +92,6 @@ android {
buildFeatures { buildFeatures {
viewBinding = true viewBinding = true
compose = true
// Disable some unused things // Disable some unused things
aidl = false aidl = false
@ -107,10 +105,6 @@ android {
checkReleaseBuilds = false checkReleaseBuilds = false
} }
composeOptions {
kotlinCompilerExtensionVersion = compose.versions.compose.get()
}
compileOptions { compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8 targetCompatibility = JavaVersion.VERSION_1_8
@ -122,19 +116,6 @@ android {
} }
dependencies { 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.reflect)
implementation(kotlinx.bundles.coroutines) implementation(kotlinx.bundles.coroutines)
@ -288,9 +269,6 @@ tasks {
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi", "-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi", "-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-Xopt-in=coil.annotation.ExperimentalCoilApi", "-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", "-Xopt-in=kotlin.time.ExperimentalTime",
) )
} }

View File

@ -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) }
}
}

View File

@ -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)
}

View File

@ -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>
}

View File

@ -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()
}
}
}

View File

@ -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,
)
}

View File

@ -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
)
}

View File

@ -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
)
}

View File

@ -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
)
}

View File

@ -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
}
}
}

View File

@ -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
)
}

View File

@ -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()) }
}
}

View File

@ -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?
)

View File

@ -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()
}
}

View File

@ -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
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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)
}
}

View File

@ -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?
)

View File

@ -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?
)

View File

@ -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
}

View File

@ -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
}
}

View File

@ -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)
}
}
}

View File

@ -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)

View File

@ -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 = '.' },
)

View File

@ -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
)
}

View File

@ -1,5 +0,0 @@
package eu.kanade.presentation.util
import androidx.compose.ui.unit.dp
val horizontalPadding = 16.dp

View File

@ -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

View File

@ -38,7 +38,6 @@ import com.google.firebase.analytics.ktx.analytics
import com.google.firebase.ktx.Firebase import com.google.firebase.ktx.Firebase
import com.ms_square.debugoverlay.DebugOverlay import com.ms_square.debugoverlay.DebugOverlay
import com.ms_square.debugoverlay.modules.FpsModule 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.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
@ -102,7 +101,6 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
} }
Injekt.importModule(AppModule(this)) Injekt.importModule(AppModule(this))
Injekt.importModule(DomainModule())
setupNotificationChannels() setupNotificationChannels()
if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) { if ((BuildConfig.DEBUG || BuildConfig.BUILD_TYPE == "releaseTest") && DebugToggles.ENABLE_DEBUG_OVERLAY.enabled) {

View File

@ -2,18 +2,9 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import androidx.core.content.ContextCompat 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.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper 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.download.DownloadManager
import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -36,37 +27,11 @@ class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() { override fun InjektRegistrar.registerInjectables() {
addSingleton(app) 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 { Json { ignoreUnknownKeys = true } }
addSingletonFactory { PreferencesHelper(app) } addSingletonFactory { PreferencesHelper(app) }
addSingletonFactory { DatabaseHelper(app, get()) } addSingletonFactory { DatabaseHelper(app) }
addSingletonFactory { ChapterCache(app) } addSingletonFactory { ChapterCache(app) }
@ -100,8 +65,6 @@ class AppModule(val app: Application) : InjektModule {
get<SourceManager>() get<SourceManager>()
get<Database>()
get<DatabaseHelper>() get<DatabaseHelper>()
get<DownloadManager>() get<DownloadManager>()

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.backup
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.data.DatabaseHandler
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@ -21,7 +20,6 @@ import uy.kohesive.injekt.injectLazy
abstract class AbstractBackupManager(protected val context: Context) { abstract class AbstractBackupManager(protected val context: Context) {
internal val databaseHelper: DatabaseHelper by injectLazy() internal val databaseHelper: DatabaseHelper by injectLazy()
internal val databaseHandler: DatabaseHandler by injectLazy()
internal val sourceManager: SourceManager by injectLazy() internal val sourceManager: SourceManager by injectLazy()
internal val trackManager: TrackManager by injectLazy() internal val trackManager: TrackManager by injectLazy()
protected val preferences: PreferencesHelper by injectLazy() protected val preferences: PreferencesHelper by injectLazy()

View File

@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.backup.full
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY 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 eu.kanade.tachiyomi.util.system.logcat
import exh.metadata.metadata.base.getFlatMetadataForManga import exh.metadata.metadata.base.getFlatMetadataForManga
import exh.metadata.metadata.base.insertFlatMetadataAsync import exh.metadata.metadata.base.insertFlatMetadataAsync
import exh.savedsearches.models.SavedSearch
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.source.getMainSource import exh.source.getMainSource
import exh.util.executeOnIO import exh.util.executeOnIO
import exh.util.nullIfBlank import exh.util.nullIfBlank
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.protobuf.ProtoBuf import kotlinx.serialization.protobuf.ProtoBuf
import logcat.LogPriority import logcat.LogPriority
import okio.buffer import okio.buffer
@ -168,8 +167,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* @return list of [BackupSavedSearch] to be backed up * @return list of [BackupSavedSearch] to be backed up
*/ */
private fun backupSavedSearches(): List<BackupSavedSearch> { private fun backupSavedSearches(): List<BackupSavedSearch> {
return runBlocking { return databaseHelper.getSavedSearches().executeAsBlocking().map {
databaseHandler.awaitList { saved_searchQueries.selectAll(savedSearchMapper) }.map {
BackupSavedSearch( BackupSavedSearch(
it.name, it.name,
it.query.orEmpty(), it.query.orEmpty(),
@ -178,7 +176,6 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
) )
} }
} }
}
// SY <-- // 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 --> // SY -->
internal suspend fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) { internal fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
val currentSavedSearches = databaseHandler.awaitList { val currentSavedSearches = databaseHelper.getSavedSearches()
saved_searchQueries.selectAll(savedSearchMapper) .executeAsBlocking()
}
databaseHandler.await(true) { val newSavedSearches = backupSavedSearches.filter { backupSavedSearch ->
backupSavedSearches.filter { backupSavedSearch ->
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.forEach { }.map {
saved_searchQueries.insertSavedSearch( SavedSearch(
_id = null, id = null,
source = it.source, it.source,
name = it.name, it.name,
query = it.query.nullIfBlank(), it.query.nullIfBlank(),
filters_json = it.filterList.nullIfBlank() filtersJson = it.filterList.nullIfBlank()
?.takeUnless { it == "[]" }, ?.takeUnless { it == "[]" },
) )
} }.ifEmpty { null }
if (newSavedSearches != null) {
databaseHelper.insertSavedSearches(newSavedSearches)
} }
} }

View File

@ -76,7 +76,7 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBa
} }
// SY --> // SY -->
private suspend fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) { private fun restoreSavedSearches(backupSavedSearches: List<BackupSavedSearch>) {
backupManager.restoreSavedSearches(backupSavedSearches) backupManager.restoreSavedSearches(backupSavedSearches)
restoreProgress += 1 restoreProgress += 1

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.data.backup.legacy
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import eu.kanade.data.exh.savedSearchMapper
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.Companion.CURRENT_VERSION 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,15 +289,12 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
} }
// SY --> // SY -->
internal suspend fun restoreSavedSearches(jsonSavedSearches: String) { internal fun restoreSavedSearches(jsonSavedSearches: String) {
val backupSavedSearches = jsonSavedSearches.split("***").toSet() val backupSavedSearches = jsonSavedSearches.split("***").toSet()
val currentSavedSearches = databaseHandler.awaitList { val currentSavedSearches = databaseHelper.getSavedSearches().executeAsBlocking()
saved_searchQueries.selectAll(savedSearchMapper)
}
databaseHandler.await(true) { val newSavedSearches = backupSavedSearches.mapNotNull {
backupSavedSearches.mapNotNull {
runCatching { runCatching {
val content = parser.decodeFromString<JsonObject>(it.substringAfter(':')) val content = parser.decodeFromString<JsonObject>(it.substringAfter(':'))
SavedSearch( SavedSearch(
@ -311,16 +307,10 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
}.getOrNull() }.getOrNull()
}.filter { backupSavedSearch -> }.filter { backupSavedSearch ->
currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source } currentSavedSearches.none { it.name == backupSavedSearch.name && it.source == backupSavedSearch.source }
}.forEach { }.ifEmpty { null }
saved_searchQueries.insertSavedSearch(
_id = null, if (newSavedSearches != null) {
source = it.source, databaseHelper.insertSavedSearches(newSavedSearches)
name = it.name,
query = it.query.nullIfBlank(),
filters_json = it.filtersJson.nullIfBlank()
?.takeUnless { it == "[]" },
)
}
} }
} }

View File

@ -73,7 +73,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
} }
// SY --> // SY -->
private suspend fun restoreSavedSearches(savedSearches: String) { private fun restoreSavedSearches(savedSearches: String) {
backupManager.restoreSavedSearches(savedSearches) backupManager.restoreSavedSearches(savedSearches)
restoreProgress += 1 restoreProgress += 1

View File

@ -47,10 +47,7 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
/** /**
* This class provides operations to manage the database through its interfaces. * This class provides operations to manage the database through its interfaces.
*/ */
open class DatabaseHelper( open class DatabaseHelper(context: Context) :
context: Context,
callback: DbOpenCallback
) :
MangaQueries, MangaQueries,
ChapterQueries, ChapterQueries,
TrackQueries, TrackQueries,
@ -69,7 +66,7 @@ open class DatabaseHelper(
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context) private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME) .name(DbOpenCallback.DATABASE_NAME)
.callback(callback) .callback(DbOpenCallback())
.build() .build()
override val db = DefaultStorIOSQLite.builder() override val db = DefaultStorIOSQLite.builder()

View File

@ -2,28 +2,115 @@ package eu.kanade.tachiyomi.data.database
import androidx.sqlite.db.SupportSQLiteDatabase import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper import androidx.sqlite.db.SupportSQLiteOpenHelper
import com.squareup.sqldelight.android.AndroidSqliteDriver import eu.kanade.tachiyomi.data.database.tables.CategoryTable
import eu.kanade.tachiyomi.Database 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 { companion object {
/** /**
* Name of the database file. * Name of the database file.
*/ */
const val DATABASE_NAME = "tachiyomi.db" const val DATABASE_NAME = "tachiyomi.db"
/**
* Version of the database.
*/
const val DATABASE_VERSION = /* SY --> */ 13 // SY <--
} }
override fun onCreate(db: SupportSQLiteDatabase) { override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
Database.Schema.create(AndroidSqliteDriver(database = db, cacheSize = 1)) 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) { override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
Database.Schema.migrate( if (oldVersion < 2) {
driver = AndroidSqliteDriver(database = db, cacheSize = 1), db.execSQL(MangaTable.addCoverLastModified)
oldVersion = oldVersion, }
newVersion = newVersion 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) { override fun onConfigure(db: SupportSQLiteDatabase) {

View File

@ -4,12 +4,40 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.RawQuery import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.History 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.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 eu.kanade.tachiyomi.data.database.tables.HistoryTable
import java.util.Date
interface HistoryQueries : DbProvider { 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() fun getHistoryByMangaId(mangaId: Long) = db.get()
.listOfObjects(History::class.java) .listOfObjects(History::class.java)
.withQuery( .withQuery(
@ -37,9 +65,9 @@ interface HistoryQueries : DbProvider {
* Inserts history object if not yet in database * Inserts history object if not yet in database
* @param history history object * @param history history object
*/ */
fun upsertHistoryLastRead(history: History) = db.put() fun updateHistoryLastRead(history: History) = db.put()
.`object`(history) .`object`(history)
.withPutResolver(HistoryUpsertResolver()) .withPutResolver(HistoryLastReadPutResolver())
.prepare() .prepare()
/** /**
@ -47,12 +75,12 @@ interface HistoryQueries : DbProvider {
* Inserts history object if not yet in database * Inserts history object if not yet in database
* @param historyList history object list * @param historyList history object list
*/ */
fun upsertHistoryLastRead(historyList: List<History>) = db.put() fun updateHistoryLastRead(historyList: List<History>) = db.put()
.objects(historyList) .objects(historyList)
.withPutResolver(HistoryUpsertResolver()) .withPutResolver(HistoryLastReadPutResolver())
.prepare() .prepare()
fun dropHistoryTable() = db.delete() fun deleteHistory() = db.delete()
.byQuery( .byQuery(
DeleteQuery.builder() DeleteQuery.builder()
.table(HistoryTable.TABLE) .table(HistoryTable.TABLE)

View File

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.data.database.queries package eu.kanade.tachiyomi.data.database.queries
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver 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 exh.source.MERGED_SOURCE_ID
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter 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. * 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} 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} FROM ${Chapter.TABLE} JOIN ${History.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
) AS max_last_read
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID} ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID} AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}

View File

@ -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.models.History
import eu.kanade.tachiyomi.data.database.tables.HistoryTable import eu.kanade.tachiyomi.data.database.tables.HistoryTable
class HistoryUpsertResolver : HistoryPutResolver() { class HistoryLastReadPutResolver : HistoryPutResolver() {
/** /**
* Updates last_read time of chapter * Updates last_read time of chapter

View File

@ -15,4 +15,19 @@ object CategoryTable {
// SY --> // SY -->
const val COL_MANGA_ORDER = "manga_order" const val COL_MANGA_ORDER = "manga_order"
// SY <-- // 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 <--
} }

View File

@ -27,4 +27,42 @@ object ChapterTable {
const val COL_CHAPTER_NUMBER = "chapter_number" const val COL_CHAPTER_NUMBER = "chapter_number"
const val COL_SOURCE_ORDER = "source_order" 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"
} }

View File

@ -26,4 +26,24 @@ object HistoryTable {
* Time read column name * Time read column name
*/ */
const val COL_TIME_READ = "${TABLE}_time_read" 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)"
} }

View File

@ -9,4 +9,16 @@ object MangaCategoryTable {
const val COL_MANGA_ID = "manga_id" const val COL_MANGA_ID = "manga_id"
const val COL_CATEGORY_ID = "category_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
)"""
} }

View File

@ -51,4 +51,59 @@ object MangaTable {
const val COMPUTED_COL_UNREAD_COUNT = "unread_count" const val COMPUTED_COL_UNREAD_COUNT = "unread_count"
const val COMPUTED_COL_READ_COUNT = "read_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 <--
} }

View File

@ -30,6 +30,43 @@ object TrackTable {
const val COL_FINISH_DATE = "finish_date" 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 val insertFromTempTable: String
get() = 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 |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 |FROM ${TABLE}_tmp
""".trimMargin() """.trimMargin()
val dropTempTable: String
get() = "DROP TABLE ${TABLE}_tmp"
} }

View File

@ -166,7 +166,7 @@ class NotificationReceiver : BroadcastReceiver() {
* @param chapterId id of chapter * @param chapterId id of chapter
*/ */
private fun openChapter(context: Context, mangaId: Long, chapterId: Long) { 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 manga = db.getManga(mangaId).executeAsBlocking()
val chapter = db.getChapter(chapterId).executeAsBlocking() val chapter = db.getChapter(chapterId).executeAsBlocking()
if (manga != null && chapter != null) { if (manga != null && chapter != null) {

View File

@ -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()
}

View File

@ -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.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch import exh.savedsearches.models.SavedSearch
@ -67,10 +66,9 @@ open class FeedController :
} }
private fun addFeed() { private fun addFeed() {
viewScope.launchUI {
if (presenter.hasTooManyFeeds()) { if (presenter.hasTooManyFeeds()) {
activity?.toast(R.string.too_many_in_feed) activity?.toast(R.string.too_many_in_feed)
return@launchUI return
} }
val items = presenter.getEnabledSources() val items = presenter.getEnabledSources()
val itemsStrings = items.map { it.toString() } val itemsStrings = items.map { it.toString() }
@ -87,10 +85,8 @@ open class FeedController :
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
}
private fun addFeedSearch(source: CatalogueSource) { private fun addFeedSearch(source: CatalogueSource) {
viewScope.launchUI {
val items = presenter.getSourceSavedSearches(source) val items = presenter.getSourceSavedSearches(source)
val itemsStrings = listOf(activity!!.getString(R.string.latest)) + items.map { it.name } val itemsStrings = listOf(activity!!.getString(R.string.latest)) + items.map { it.name }
var selectedIndex = 0 var selectedIndex = 0
@ -106,7 +102,6 @@ open class FeedController :
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
} }
}
/** /**
* Called when manga in global search is clicked, opens manga. * Called when manga in global search is clicked, opens manga.

View File

@ -1,9 +1,6 @@
package eu.kanade.tachiyomi.ui.browse.feed package eu.kanade.tachiyomi.ui.browse.feed
import android.os.Bundle 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.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo 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.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch import exh.savedsearches.models.SavedSearch
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import logcat.LogPriority 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. * Function calls should be done from here. UI calls should be done from the controller.
* *
* @param sourceManager manages the different sources. * @param sourceManager manages the different sources.
* @param database manages the database calls. * @param db manages the database calls.
* @param preferences manages the preference calls. * @param preferences manages the preference calls.
*/ */
open class FeedPresenter( open class FeedPresenter(
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
val database: DatabaseHandler = Injekt.get(),
val db: DatabaseHelper = Injekt.get(), val db: DatabaseHelper = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
) : BasePresenter<FeedController>() { ) : BasePresenter<FeedController>() {
@ -69,11 +62,14 @@ open class FeedPresenter(
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
database.subscribeToList { feed_saved_searchQueries.selectAllGlobal() } db.getGlobalFeedSavedSearches()
.onEach { .asRxObservable()
.observeOn(AndroidSchedulers.mainThread())
.doOnEach {
getFeed() getFeed()
} }
.launchIn(presenterScope) .subscribe()
.let(::add)
} }
override fun onDestroy() { override fun onDestroy() {
@ -82,10 +78,8 @@ open class FeedPresenter(
super.onDestroy() super.onDestroy()
} }
suspend fun hasTooManyFeeds(): Boolean { fun hasTooManyFeeds(): Boolean {
return withIOContext { return db.getGlobalFeedSavedSearches().executeAsBlocking().size > 10
database.awaitList { feed_saved_searchQueries.selectAllGlobal() }.size > 10
}
} }
fun getEnabledSources(): List<CatalogueSource> { fun getEnabledSources(): List<CatalogueSource> {
@ -99,38 +93,33 @@ open class FeedPresenter(
return list.sortedBy { it.id.toString() !in pinnedSources } return list.sortedBy { it.id.toString() !in pinnedSources }
} }
suspend fun getSourceSavedSearches(source: CatalogueSource): List<SavedSearch> { fun getSourceSavedSearches(source: CatalogueSource): List<SavedSearch> {
return withIOContext { return db.getSavedSearches(source.id).executeAsBlocking()
database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }
}
} }
fun createFeed(source: CatalogueSource, savedSearch: SavedSearch?) { fun createFeed(source: CatalogueSource, savedSearch: SavedSearch?) {
launchIO { launchIO {
database.await { db.insertFeedSavedSearch(
feed_saved_searchQueries.insertFeedSavedSearch( FeedSavedSearch(
_id = null, id = null,
source = source.id, source = source.id,
saved_search = savedSearch?.id, savedSearch = savedSearch?.id,
global = true, global = true,
) ),
} ).executeAsBlocking()
} }
} }
fun deleteFeed(feed: FeedSavedSearch) { fun deleteFeed(feed: FeedSavedSearch) {
launchIO { launchIO {
database.await { db.deleteFeedSavedSearch(feed).executeAsBlocking()
feed_saved_searchQueries.deleteById(feed.id ?: return@await)
}
} }
} }
private suspend fun getSourcesToGetFeed(): List<Pair<FeedSavedSearch, SavedSearch?>> { private fun getSourcesToGetFeed(): List<Pair<FeedSavedSearch, SavedSearch?>> {
val savedSearches = database.awaitList { val savedSearches = db.getGlobalSavedSearchesFeed().executeAsBlocking()
feed_saved_searchQueries.selectGlobalFeedSavedSearch(savedSearchMapper) .associateBy { it.id!! }
}.associateBy { it.id } return db.getGlobalFeedSavedSearches().executeAsBlocking()
return database.awaitList { feed_saved_searchQueries.selectAllGlobal(feedSavedSearchMapper) }
.map { it to savedSearches[it.savedSearch] } .map { it to savedSearches[it.savedSearch] }
} }
@ -149,7 +138,7 @@ open class FeedPresenter(
/** /**
* Initiates get manga per feed. * Initiates get manga per feed.
*/ */
suspend fun getFeed() { fun getFeed() {
// Create image fetch subscription // Create image fetch subscription
initializeFetchImageSubscription() initializeFetchImageSubscription()

View File

@ -137,7 +137,7 @@ class MigrationProcessAdapter(
} }
} }
db.insertChapters(dbChapters).executeAsBlocking() db.insertChapters(dbChapters).executeAsBlocking()
db.upsertHistoryLastRead(historyList).executeAsBlocking() db.updateHistoryLastRead(historyList).executeAsBlocking()
} }
// Update categories // Update categories
if (MigrationFlags.hasCategories(flags)) { if (MigrationFlags.hasCategories(flags)) {

View File

@ -44,7 +44,6 @@ import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.more.MoreController import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity 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.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.connectivityManager import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.openInBrowser import eu.kanade.tachiyomi.util.system.openInBrowser
@ -202,7 +201,7 @@ open class BrowseSourceController(bundle: Bundle) :
// SY --> // SY -->
this, this,
presenter.source, presenter.source,
emptyList(), presenter.loadSearches(),
// SY <-- // SY <--
onFilterClicked = { onFilterClicked = {
showProgressBar() showProgressBar()
@ -217,7 +216,6 @@ open class BrowseSourceController(bundle: Bundle) :
}, },
// EXH --> // EXH -->
onSaveClicked = { onSaveClicked = {
viewScope.launchUI {
filterSheet?.context?.let { filterSheet?.context?.let {
val names = presenter.loadSearches().map { it.name } val names = presenter.loadSearches().map { it.name }
var searchName = "" var searchName = ""
@ -236,10 +234,8 @@ open class BrowseSourceController(bundle: Bundle) :
.setNegativeButton(R.string.action_cancel, null) .setNegativeButton(R.string.action_cancel, null)
.show() .show()
} }
}
}, },
onSavedSearchClicked = { idOfSearch -> onSavedSearchClicked = cb@{ idOfSearch ->
viewScope.launchUI {
val search = presenter.loadSearch(idOfSearch) val search = presenter.loadSearch(idOfSearch)
if (search == null) { if (search == null) {
@ -249,12 +245,12 @@ open class BrowseSourceController(bundle: Bundle) :
.setMessage(R.string.save_search_failed_to_load_message) .setMessage(R.string.save_search_failed_to_load_message)
.show() .show()
} }
return@launchUI return@cb
} }
if (search.filterList == null) { if (search.filterList == null) {
activity?.toast(R.string.save_search_invalid) activity?.toast(R.string.save_search_invalid)
return@launchUI return@cb
} }
presenter.sourceFilters = FilterList(search.filterList) presenter.sourceFilters = FilterList(search.filterList)
@ -266,9 +262,8 @@ open class BrowseSourceController(bundle: Bundle) :
filterSheet?.dismiss() filterSheet?.dismiss()
presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters) presenter.restartPager(search.query, if (allDefault) FilterList() else presenter.sourceFilters)
activity?.invalidateOptionsMenu() activity?.invalidateOptionsMenu()
}
}, },
onSavedSearchDeleteClicked = { idToDelete, name -> onSavedSearchDeleteClicked = cb@{ idToDelete, name ->
filterSheet?.context?.let { filterSheet?.context?.let {
MaterialAlertDialogBuilder(it) MaterialAlertDialogBuilder(it)
.setTitle(R.string.save_search_delete) .setTitle(R.string.save_search_delete)
@ -282,9 +277,6 @@ open class BrowseSourceController(bundle: Bundle) :
}, },
// EXH <-- // EXH <--
) )
launchUI {
filterSheet?.setSavedSearches(presenter.loadSearches())
}
filterSheet?.setFilters(presenter.filterItems) filterSheet?.setFilters(presenter.filterItems)
filterSheet?.setOnShowListener { actionFab?.hide() } filterSheet?.setOnShowListener { actionFab?.hide() }

View File

@ -2,8 +2,6 @@ package eu.kanade.tachiyomi.ui.browse.source.browse
import android.os.Bundle import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible 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.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category 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.ChapterSettingsHelper
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay
import eu.kanade.tachiyomi.util.lang.launchIO 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.lang.withUIContext
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.system.logcat 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.catch
import kotlinx.coroutines.flow.collect import kotlinx.coroutines.flow.collect
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
@ -83,7 +78,6 @@ open class BrowseSourcePresenter(
// SY <-- // SY <--
private val sourceManager: SourceManager = Injekt.get(), private val sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get(), private val db: DatabaseHelper = Injekt.get(),
private val database: DatabaseHandler = Injekt.get(),
private val prefs: PreferencesHelper = Injekt.get(), private val prefs: PreferencesHelper = Injekt.get(),
private val coverCache: CoverCache = Injekt.get(), private val coverCache: CoverCache = Injekt.get(),
) : BasePresenter<BrowseSourceController>() { ) : BasePresenter<BrowseSourceController>() {
@ -146,11 +140,7 @@ open class BrowseSourcePresenter(
val jsonFilters = filters val jsonFilters = filters
if (savedSearchFilters != null) { if (savedSearchFilters != null) {
runCatching { runCatching {
val savedSearch = runBlocking { val savedSearch = db.getSavedSearch(savedSearchFilters).executeAsBlocking() ?: return@runCatching
database.awaitOneOrNull {
saved_searchQueries.selectById(savedSearchFilters, savedSearchMapper)
}
} ?: return@runCatching
query = savedSearch.query.orEmpty() query = savedSearch.query.orEmpty()
val filtersJson = savedSearch.filtersJson val filtersJson = savedSearch.filtersJson
?: return@runCatching ?: return@runCatching
@ -166,14 +156,18 @@ open class BrowseSourcePresenter(
} }
} }
database.subscribeToList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) } db.getSavedSearches(source.id)
.map { loadSearches(it) } .asRxObservable()
.onEach { .map {
withUIContext { loadSearches(it)
view?.setSavedSearches(it)
} }
} .subscribeOn(Schedulers.io())
.launchIn(presenterScope) .observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(
{ controller, savedSearches ->
controller.setSavedSearches(savedSearches)
},
)
// SY <-- // SY <--
if (savedState != null) { if (savedState != null) {
@ -491,31 +485,28 @@ open class BrowseSourcePresenter(
fun saveSearch(name: String, query: String, filterList: FilterList) { fun saveSearch(name: String, query: String, filterList: FilterList) {
launchIO { launchIO {
kotlin.runCatching { kotlin.runCatching {
database.await { val savedSearch = SavedSearch(
saved_searchQueries.insertSavedSearch( id = null,
_id = null,
source = source.id, source = source.id,
name = name.trim(), name = name.trim(),
query = query.nullIfBlank(), query = query.nullIfBlank(),
filters_json = filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) }, filtersJson = filterSerializer.serialize(filterList).ifEmpty { null }?.let { Json.encodeToString(it) },
) )
}
db.insertSavedSearch(savedSearch).executeAsBlocking()
} }
} }
} }
fun deleteSearch(searchId: Long) { fun deleteSearch(searchId: Long) {
launchIO { launchIO {
database.await { saved_searchQueries.deleteById(searchId) } db.deleteSavedSearch(searchId).executeAsBlocking()
} }
} }
suspend fun loadSearch(searchId: Long): EXHSavedSearch? { fun loadSearch(searchId: Long): EXHSavedSearch? {
return withIOContext { val search = db.getSavedSearch(searchId).executeAsBlocking() ?: return null
val search = database.awaitOneOrNull { return EXHSavedSearch(
saved_searchQueries.selectById(searchId, savedSearchMapper)
} ?: return@withIOContext null
EXHSavedSearch(
id = search.id!!, id = search.id!!,
name = search.name, name = search.name,
query = search.query.orEmpty(), query = search.query.orEmpty(),
@ -531,12 +522,9 @@ open class BrowseSourcePresenter(
}.getOrNull(), }.getOrNull(),
) )
} }
}
suspend fun loadSearches(searches: List<SavedSearch>? = null): List<EXHSavedSearch> { fun loadSearches(searches: List<SavedSearch> = db.getSavedSearches(source.id).executeAsBlocking()): List<EXHSavedSearch> {
return withIOContext { return searches.map {
(searches ?: (database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }))
.map {
val filtersJson = it.filtersJson ?: return@map EXHSavedSearch( val filtersJson = it.filtersJson ?: return@map EXHSavedSearch(
id = it.id!!, id = it.id!!,
name = it.name, name = it.name,
@ -576,6 +564,5 @@ open class BrowseSourcePresenter(
} }
} }
} }
}
// EXH <-- // EXH <--
} }

View File

@ -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.browse.SourceFilterSheet
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.manga.MangaController 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 eu.kanade.tachiyomi.util.system.toast
import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch import exh.savedsearches.models.SavedSearch
@ -186,7 +184,7 @@ open class SourceFeedController :
// SY --> // SY -->
this, this,
presenter.source, presenter.source,
emptyList(), presenter.loadSearches(),
// SY <-- // SY <--
onFilterClicked = { onFilterClicked = {
val allDefault = presenter.sourceFilters == presenter.source.getFilterList() val allDefault = presenter.sourceFilters == presenter.source.getFilterList()
@ -204,8 +202,7 @@ open class SourceFeedController :
}, },
onResetClicked = {}, onResetClicked = {},
onSaveClicked = {}, onSaveClicked = {},
onSavedSearchClicked = { idOfSearch -> onSavedSearchClicked = cb@{ idOfSearch ->
viewScope.launchUI {
val search = presenter.loadSearch(idOfSearch) val search = presenter.loadSearch(idOfSearch)
if (search == null) { if (search == null) {
@ -215,12 +212,12 @@ open class SourceFeedController :
.setMessage(R.string.save_search_failed_to_load_message) .setMessage(R.string.save_search_failed_to_load_message)
.show() .show()
} }
return@launchUI return@cb
} }
if (search.filterList == null) { if (search.filterList == null) {
activity?.toast(R.string.save_search_invalid) activity?.toast(R.string.save_search_invalid)
return@launchUI return@cb
} }
presenter.sourceFilters = FilterList(search.filterList) presenter.sourceFilters = FilterList(search.filterList)
@ -234,15 +231,12 @@ open class SourceFeedController :
savedSearch = search.id, savedSearch = search.id,
) )
} }
}
}, },
onSavedSearchDeleteClicked = { idOfSearch, name -> onSavedSearchDeleteClicked = cb@{ idOfSearch, name ->
viewScope.launchUI {
if (presenter.hasTooManyFeeds()) { if (presenter.hasTooManyFeeds()) {
activity?.toast(R.string.too_many_in_feed) activity?.toast(R.string.too_many_in_feed)
return@launchUI return@cb
} }
withUIContext {
MaterialAlertDialogBuilder(activity!!) MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.feed) .setTitle(R.string.feed)
.setMessage(activity!!.getString(R.string.feed_add, name)) .setMessage(activity!!.getString(R.string.feed_add, name))
@ -251,13 +245,8 @@ open class SourceFeedController :
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.show() .show()
}
}
}, },
) )
launchUI {
filterSheet?.setSavedSearches(presenter.loadSearches())
}
filterSheet?.setFilters(presenter.filterItems) filterSheet?.setFilters(presenter.filterItems)
// TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly // TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly

View File

@ -2,9 +2,6 @@ package eu.kanade.tachiyomi.ui.browse.source.feed
import android.os.Bundle import android.os.Bundle
import eu.davidea.flexibleadapter.items.IFlexible 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.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo 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.ui.browse.source.browse.BrowseSourcePresenter.Companion.toItems
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.runAsObservable import eu.kanade.tachiyomi.util.lang.runAsObservable
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import exh.log.xLogE import exh.log.xLogE
import exh.savedsearches.EXHSavedSearch import exh.savedsearches.EXHSavedSearch
import exh.savedsearches.models.FeedSavedSearch import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch 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.decodeFromString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonArray 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. * Function calls should be done from here. UI calls should be done from the controller.
* *
* @param source the source. * @param source the source.
* @param database manages the database calls. * @param db manages the database calls.
* @param preferences manages the preference calls. * @param preferences manages the preference calls.
*/ */
open class SourceFeedPresenter( open class SourceFeedPresenter(
val source: CatalogueSource, val source: CatalogueSource,
val database: DatabaseHandler = Injekt.get(),
val db: DatabaseHelper = Injekt.get(), val db: DatabaseHelper = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
) : BasePresenter<SourceFeedController>() { ) : BasePresenter<SourceFeedController>() {
@ -98,11 +90,14 @@ open class SourceFeedPresenter(
sourceFilters = source.getFilterList() sourceFilters = source.getFilterList()
database.subscribeToList { feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id, savedSearchMapper) } db.getSourceFeedSavedSearches(source.id)
.onEach { .asRxObservable()
.observeOn(AndroidSchedulers.mainThread())
.doOnEach {
getFeed() getFeed()
} }
.launchIn(presenterScope) .subscribe()
.let(::add)
} }
override fun onDestroy() { override fun onDestroy() {
@ -111,39 +106,35 @@ open class SourceFeedPresenter(
super.onDestroy() super.onDestroy()
} }
suspend fun hasTooManyFeeds(): Boolean { fun hasTooManyFeeds(): Boolean {
return withIOContext { return db.getSourceFeedSavedSearches(source.id).executeAsBlocking().size > 10
database.awaitList {
feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id)
}.size > 10
}
} }
suspend fun getSourceSavedSearches(): List<SavedSearch> { fun getSourceSavedSearches(): List<SavedSearch> {
return database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) } return db.getSavedSearches(source.id).executeAsBlocking()
} }
fun createFeed(savedSearchId: Long) { fun createFeed(savedSearchId: Long) {
launchIO { launchIO {
database.await { db.insertFeedSavedSearch(
feed_saved_searchQueries.insertFeedSavedSearch( FeedSavedSearch(
_id = null, id = null,
source = source.id, source = source.id,
saved_search = savedSearchId, savedSearch = savedSearchId,
global = false global = false,
) ),
} ).executeAsBlocking()
} }
} }
fun deleteFeed(feed: FeedSavedSearch) { fun deleteFeed(feed: FeedSavedSearch) {
launchIO { launchIO {
database.await { feed_saved_searchQueries.deleteById(feed.id ?: return@await) } db.deleteFeedSavedSearch(feed).executeAsBlocking()
} }
} }
private suspend fun getSourcesToGetFeed(): List<SourceFeed> { private fun getSourcesToGetFeed(): List<SourceFeed> {
val savedSearches = database.awaitList { feed_saved_searchQueries.selectSourceFeedSavedSearch(source.id, savedSearchMapper) } val savedSearches = db.getSourceSavedSearchesFeed(source.id).executeAsBlocking()
.associateBy { it.id!! } .associateBy { it.id!! }
return listOfNotNull( return listOfNotNull(
@ -151,7 +142,7 @@ open class SourceFeedPresenter(
SourceFeed.Latest SourceFeed.Latest
} else null, } else null,
SourceFeed.Browse, SourceFeed.Browse,
) + database.awaitList { feed_saved_searchQueries.selectBySource(source.id, feedSavedSearchMapper) } ) + db.getSourceFeedSavedSearches(source.id).executeAsBlocking()
.map { SourceFeed.SourceSavedSearch(it, savedSearches[it.savedSearch]!!) } .map { SourceFeed.SourceSavedSearch(it, savedSearches[it.savedSearch]!!) }
} }
@ -168,7 +159,7 @@ open class SourceFeedPresenter(
/** /**
* Initiates get manga per feed. * Initiates get manga per feed.
*/ */
suspend fun getFeed() { fun getFeed() {
// Create image fetch subscription // Create image fetch subscription
initializeFetchImageSubscription() initializeFetchImageSubscription()
@ -309,12 +300,9 @@ open class SourceFeedPresenter(
return localManga return localManga
} }
suspend fun loadSearch(searchId: Long): EXHSavedSearch? { fun loadSearch(searchId: Long): EXHSavedSearch? {
return withIOContext { val search = db.getSavedSearch(searchId).executeAsBlocking() ?: return null
val search = database.awaitOneOrNull { return EXHSavedSearch(
saved_searchQueries.selectById(searchId, savedSearchMapper)
} ?: return@withIOContext null
EXHSavedSearch(
id = search.id!!, id = search.id!!,
name = search.name, name = search.name,
query = search.query.orEmpty(), query = search.query.orEmpty(),
@ -330,11 +318,9 @@ open class SourceFeedPresenter(
}.getOrNull(), }.getOrNull(),
) )
} }
}
suspend fun loadSearches(): List<EXHSavedSearch> { fun loadSearches(): List<EXHSavedSearch> {
return withIOContext { return db.getSavedSearches(source.id).executeAsBlocking().map {
database.awaitList { saved_searchQueries.selectBySource(source.id, savedSearchMapper) }.map {
val filtersJson = it.filtersJson ?: return@map EXHSavedSearch( val filtersJson = it.filtersJson ?: return@map EXHSavedSearch(
id = it.id!!, id = it.id!!,
name = it.name, name = it.name,
@ -344,7 +330,6 @@ open class SourceFeedPresenter(
val filters = try { val filters = try {
Json.decodeFromString<JsonArray>(filtersJson) Json.decodeFromString<JsonArray>(filtersJson)
} catch (e: Exception) { } catch (e: Exception) {
if (e is CancellationException) throw e
null null
} ?: return@map EXHSavedSearch( } ?: return@map EXHSavedSearch(
id = it.id!!, id = it.id!!,
@ -374,5 +359,4 @@ open class SourceFeedPresenter(
} }
} }
} }
}
} }

View File

@ -35,7 +35,6 @@ import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -137,8 +136,6 @@ class MangaController :
DownloadCustomChaptersDialog.Listener, DownloadCustomChaptersDialog.Listener,
DeleteChaptersDialog.Listener { DeleteChaptersDialog.Listener {
constructor(history: HistoryWithRelations) : this(history.mangaId)
constructor(manga: Manga?, fromSource: Boolean = false, smartSearchConfig: SourceController.SmartSearchConfig? = null, update: Boolean = false) : super( constructor(manga: Manga?, fromSource: Boolean = false, smartSearchConfig: SourceController.SmartSearchConfig? = null, update: Boolean = false) : super(
bundleOf( bundleOf(
MANGA_EXTRA to (manga?.id ?: 0), MANGA_EXTRA to (manga?.id ?: 0),

View File

@ -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)
}
}

View File

@ -127,19 +127,14 @@ import kotlin.time.Duration.Companion.seconds
class ReaderActivity : BaseRxActivity<ReaderPresenter>() { class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
companion object { companion object {
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
fun newIntent(context: Context, mangaId: Long?, chapterId: Long?): Intent {
return Intent(context, ReaderActivity::class.java).apply { return Intent(context, ReaderActivity::class.java).apply {
putExtra("manga", mangaId) putExtra("manga", manga.id)
putExtra("chapter", chapterId) putExtra("chapter", chapter.id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP) 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 SHIFT_DOUBLE_PAGES = "shiftingDoublePages"
const val SHIFTED_PAGE_INDEX = "shiftedPageIndex" const val SHIFTED_PAGE_INDEX = "shiftedPageIndex"
const val SHIFTED_CHAP_INDEX = "shiftedChapterIndex" const val SHIFTED_CHAP_INDEX = "shiftedChapterIndex"

View File

@ -542,7 +542,7 @@ class ReaderPresenter(
private fun saveChapterHistory(chapter: ReaderChapter) { private fun saveChapterHistory(chapter: ReaderChapter) {
if (!incognitoMode) { if (!incognitoMode) {
val history = History.create(chapter.chapter).apply { last_read = Date().time } val history = History.create(chapter.chapter).apply { last_read = Date().time }
db.upsertHistoryLastRead(history).asRxCompletable() db.updateHistoryLastRead(history).asRxCompletable()
.onErrorComplete() .onErrorComplete()
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.subscribe() .subscribe()

View File

@ -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()
}
}

View File

@ -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)
}
}

View File

@ -1,53 +1,193 @@
package eu.kanade.tachiyomi.ui.recent.history 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.Menu
import android.view.MenuInflater import android.view.MenuInflater
import android.view.MenuItem import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.Composable import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.domain.chapter.model.Chapter import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.presentation.history.HistoryScreen import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R 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.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction 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.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity 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.system.toast
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import logcat.LogPriority
import reactivecircus.flowbinding.appcompat.queryTextChanges 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 = "" 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 createBinding(inflater: LayoutInflater) = HistoryControllerBinding.inflate(inflater)
override fun ComposeContent() {
HistoryScreen( override fun onViewCreated(view: View) {
composeView = binding.root, super.onViewCreated(view)
presenter = presenter,
onClickItem = { history -> binding.recycler.applyInsetter {
router.pushController(MangaController(history).withFadeTransaction()) type(navigationBars = true) {
}, padding()
onClickResume = { history -> }
presenter.getNextChapterForManga(history.mangaId, history.chapterId) }
},
onClickDelete = { history, all -> // 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) { if (all) {
// Reset last read of chapter to 0L // Reset last read of chapter to 0L
presenter.removeAllFromHistory(history.mangaId) presenter.removeAllFromHistory(manga.id!!)
} else { } else {
// Remove all chapters belonging to manga from library // Remove all chapters belonging to manga from library
presenter.removeFromHistory(history) presenter.removeFromHistory(history)
} }
},
)
} }
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -61,33 +201,46 @@ class HistoryController : ComposeController<HistoryPresenter>(), RootController
searchView.clearFocus() searchView.clearFocus()
} }
searchView.queryTextChanges() searchView.queryTextChanges()
.drop(1) // Drop first event after subscribed
.filter { router.backstack.lastOrNull()?.controller == this } .filter { router.backstack.lastOrNull()?.controller == this }
.onEach { .onEach {
query = it.toString() query = it.toString()
presenter.search(query) presenter.updateList(query)
} }
.launchIn(viewScope) .launchIn(viewScope)
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.fixExpand(
onExpand = { invalidateMenuOnExpand() },
)
} }
override fun onOptionsItemSelected(item: MenuItem): Boolean { override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) { when (item.itemId) {
R.id.action_clear_history -> { R.id.action_clear_history -> {
val dialog = ClearHistoryDialogController() val ctrl = ClearHistoryDialogController()
dialog.targetController = this@HistoryController ctrl.targetController = this@HistoryController
dialog.showDialog(router) ctrl.showDialog(router)
true
}
else -> super.onOptionsItemSelected(item)
} }
} }
fun openChapter(chapter: Chapter?) { return super.onOptionsItemSelected(item)
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)
} }
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()
}
}
private fun clearHistory() {
db.deleteHistory().executeAsBlocking()
activity?.toast(R.string.clear_history_completed)
} }
} }

View File

@ -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)
}
}

View File

@ -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()
}
}

View File

@ -1,127 +1,157 @@
package eu.kanade.tachiyomi.ui.recent.history package eu.kanade.tachiyomi.ui.recent.history
import android.os.Bundle import android.os.Bundle
import androidx.paging.PagingData import eu.kanade.tachiyomi.data.database.DatabaseHelper
import androidx.paging.cachedIn import eu.kanade.tachiyomi.data.database.models.Chapter
import androidx.paging.insertSeparators import eu.kanade.tachiyomi.data.database.models.History
import androidx.paging.map import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.domain.history.interactor.DeleteHistoryTable import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.domain.history.interactor.GetHistory import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.domain.history.interactor.GetNextChapterForManga
import eu.kanade.domain.history.interactor.RemoveHistoryById
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.ui.recent.DateSectionItem
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.toDateKey import eu.kanade.tachiyomi.util.lang.toDateKey
import eu.kanade.tachiyomi.util.system.toast import rx.Observable
import kotlinx.coroutines.flow.Flow import rx.Subscription
import kotlinx.coroutines.flow.MutableStateFlow import rx.android.schedulers.AndroidSchedulers
import kotlinx.coroutines.flow.StateFlow import uy.kohesive.injekt.injectLazy
import kotlinx.coroutines.flow.flatMapLatest import java.text.DateFormat
import kotlinx.coroutines.flow.map import java.util.Calendar
import kotlinx.coroutines.flow.update
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date import java.util.Date
import java.util.TreeMap
/** /**
* Presenter of HistoryFragment. * Presenter of HistoryFragment.
* Contains information and data for fragment. * Contains information and data for fragment.
* Observable updates should be called from here. * Observable updates should be called from here.
*/ */
class HistoryPresenter( class HistoryPresenter : BasePresenter<HistoryController>() {
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>() {
private var _query: MutableStateFlow<String> = MutableStateFlow("") private val db: DatabaseHelper by injectLazy()
private var _state: MutableStateFlow<HistoryState> = MutableStateFlow(HistoryState.EMPTY) private val preferences: PreferencesHelper by injectLazy()
val state: StateFlow<HistoryState> = _state
private val relativeTime: Int = preferences.relativeTime().get()
private val dateFormat: DateFormat = preferences.dateFormat()
private var recentMangaSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
presenterScope.launchIO { // Used to get a list of recently read manga
_state.update { state -> updateList()
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) fun requestNext(offset: Int, search: String = "") {
val afterDate = after?.item?.readAt?.time?.toDateKey() ?: Date(0) getRecentMangaObservable(offset = offset, search = search)
when { .subscribeLatestCache(
beforeDate.time != afterDate.time && afterDate.time != 0L -> UiModel.Header(afterDate) { view, mangas ->
// Return null to avoid adding a separator between two items. view.onNextManga(mangas)
else -> null },
} HistoryController::onAddPageError,
}
}
}
.cachedIn(presenterScope),
) )
} }
}
/**
* 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)
} }
fun search(query: String) { return db.getRecentManga(cal.time, limit, offset, search).asRxObservable()
presenterScope.launchIO { .map { recents ->
_query.emit(query) 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 removeFromHistory(history: HistoryWithRelations) { /**
presenterScope.launchIO { * Reset last read of chapter to 0L
removeHistoryById.await(history) * @param history history belonging to chapter
} */
fun removeFromHistory(history: History) {
history.last_read = 0L
db.updateHistoryLastRead(history).asRxObservable()
.subscribe()
} }
/**
* Pull a list of history from the db
* @param search a search query to use for filtering
*/
fun updateList(search: String = "") {
recentMangaSubscription?.unsubscribe()
recentMangaSubscription = getRecentMangaObservable(search = search)
.subscribeLatestCache(
{ view, mangas ->
view.onNextManga(mangas, true)
},
HistoryController::onAddPageError,
)
}
/**
* Removes all chapters belonging to manga from history.
* @param mangaId id of manga
*/
fun removeAllFromHistory(mangaId: Long) { fun removeAllFromHistory(mangaId: Long) {
presenterScope.launchIO { db.getHistoryByMangaId(mangaId).asRxSingle()
removeHistoryByMangaId.await(mangaId) .map { list ->
list.forEach { it.last_read = 0L }
db.updateHistoryLastRead(list).executeAsBlocking()
} }
.subscribe()
} }
fun getNextChapterForManga(mangaId: Long, chapterId: Long) { /**
presenterScope.launchIO { * Retrieves the next chapter of the given one.
val chapter = getNextChapterForManga.await(mangaId, chapterId) *
launchUI { * @param chapter the chapter of the history object.
view?.openChapter(chapter) * @param manga the manga of the chapter.
} */
} fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? {
if (!chapter.read) {
return chapter
} }
fun deleteAllHistory() { val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
presenterScope.launchIO { Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
val result = deleteHistoryTable.await() Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
if (!result) return@launchIO Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
launchUI { else -> throw NotImplementedError("Unknown sorting method")
view?.activity?.toast(R.string.clear_history_completed)
} }
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)
}
}

View File

@ -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)
}
}

View File

@ -37,6 +37,7 @@ class ClearDatabaseController :
private var menu: Menu? = null private var menu: Menu? = null
private var actionFab: ExtendedFloatingActionButton? = null private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
@ -142,6 +143,7 @@ class ClearDatabaseController :
override fun cleanupFab(fab: ExtendedFloatingActionButton) { override fun cleanupFab(fab: ExtendedFloatingActionButton) {
actionFab?.setOnClickListener(null) actionFab?.setOnClickListener(null)
actionFabScrollListener?.let { recycler?.removeOnScrollListener(it) }
actionFab = null actionFab = null
} }

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.setting.database package eu.kanade.tachiyomi.ui.setting.database
import android.os.Bundle import android.os.Bundle
import eu.kanade.tachiyomi.Database
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
@ -14,7 +13,6 @@ import uy.kohesive.injekt.api.get
class ClearDatabasePresenter : BasePresenter<ClearDatabaseController>() { class ClearDatabasePresenter : BasePresenter<ClearDatabaseController>() {
private val db = Injekt.get<DatabaseHelper>() private val db = Injekt.get<DatabaseHelper>()
private val database = Injekt.get<Database>()
private val sourceManager = Injekt.get<SourceManager>() private val sourceManager = Injekt.get<SourceManager>()
@ -34,7 +32,7 @@ class ClearDatabasePresenter : BasePresenter<ClearDatabaseController>() {
db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking() db.deleteMangasNotInLibraryBySourceIds(sources).executeAsBlocking()
} }
// SY <-- // SY <--
database.historyQueries.removeResettedHistory() db.deleteHistoryNoLastRead().executeAsBlocking()
} }
private fun getDatabaseSourcesObservable(): Observable<List<ClearDatabaseSourceItem>> { private fun getDatabaseSourcesObservable(): Observable<List<ClearDatabaseSourceItem>> {

View File

@ -5,10 +5,8 @@ import android.os.Parcel
import android.os.Parcelable import android.os.Parcelable
import android.util.AttributeSet import android.util.AttributeSet
import android.view.View import android.view.View
import androidx.compose.ui.platform.ComposeView
import androidx.coordinatorlayout.R import androidx.coordinatorlayout.R
import androidx.coordinatorlayout.widget.CoordinatorLayout import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.view.doOnLayout import androidx.core.view.doOnLayout
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.customview.view.AbsSavedState import androidx.customview.view.AbsSavedState
@ -65,18 +63,9 @@ class TachiyomiCoordinatorLayout @JvmOverloads constructor(
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed) super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed)
// Disable elevation overlay when tabs are visible // Disable elevation overlay when tabs are visible
if (canLiftAppBarOnScroll) { 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
} }
} }
}
override fun onAttachedToWindow() { override fun onAttachedToWindow() {
super.onAttachedToWindow() super.onAttachedToWindow()

View File

@ -6,7 +6,6 @@ import androidx.preference.PreferenceManager
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.data.DatabaseHandler
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -41,6 +40,8 @@ import exh.eh.EHentaiUpdateWorker
import exh.log.xLogE import exh.log.xLogE
import exh.log.xLogW import exh.log.xLogW
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.savedsearches.models.FeedSavedSearch
import exh.savedsearches.models.SavedSearch
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.HBROWSE_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.source.TSUMINO_SOURCE_ID
import exh.util.nullIfBlank import exh.util.nullIfBlank
import exh.util.under import exh.util.under
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.SerialName import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString import kotlinx.serialization.decodeFromString
@ -69,7 +69,6 @@ import java.net.URISyntaxException
object EXHMigrations { object EXHMigrations {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
private val database: DatabaseHandler by injectLazy()
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
/** /**
@ -405,31 +404,31 @@ object EXHMigrations {
BackupCreatorJob.setupTask(context) BackupCreatorJob.setupTask(context)
} }
if (oldVersion under 31) { if (oldVersion under 31) {
runBlocking { val savedSearches = prefs.getStringSet("eh_saved_searches", emptySet())?.mapNotNull {
database.await(true) {
prefs.getStringSet("eh_saved_searches", emptySet())?.forEach {
kotlin.runCatching { kotlin.runCatching {
val content = Json.decodeFromString<JsonObject>(it.substringAfter(':')) val content = Json.decodeFromString<JsonObject>(it.substringAfter(':'))
saved_searchQueries.insertSavedSearch( SavedSearch(
_id = null, id = null,
source = it.substringBefore(':').toLongOrNull() ?: return@forEach, source = it.substringBefore(':').toLongOrNull() ?: return@mapNotNull null,
name = content["name"]!!.jsonPrimitive.content, content["name"]!!.jsonPrimitive.content,
query = content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(), content["query"]!!.jsonPrimitive.contentOrNull?.nullIfBlank(),
filters_json = Json.encodeToString(content["filters"]!!.jsonArray) 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(
database.await(true) { id = null,
prefs.getStringSet("latest_tab_sources", emptySet())?.forEach {
feed_saved_searchQueries.insertFeedSavedSearch(
_id = null,
source = it.toLong(), source = it.toLong(),
saved_search = null, savedSearch = null,
global = true, global = true,
) )
} }?.ifEmpty { null }
} if (feed != null) {
db.insertFeedSavedSearches(feed).executeAsBlocking()
} }
prefs.edit(commit = true) { prefs.edit(commit = true) {
remove("eh_saved_searches") remove("eh_saved_searches")

View File

@ -3,7 +3,6 @@ package exh.debug
import android.app.Application import android.app.Application
import androidx.work.WorkManager import androidx.work.WorkManager
import com.pushtorefresh.storio.sqlite.queries.RawQuery 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.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.database.tables.MangaTable import eu.kanade.tachiyomi.data.database.tables.MangaTable
@ -35,7 +34,6 @@ import java.util.UUID
object DebugFunctions { object DebugFunctions {
val app: Application by injectLazy() val app: Application by injectLazy()
val db: DatabaseHelper by injectLazy() val db: DatabaseHelper by injectLazy()
val database: DatabaseHandler by injectLazy()
val prefs: PreferencesHelper by injectLazy() val prefs: PreferencesHelper by injectLazy()
val sourceManager: SourceManager by injectLazy() val sourceManager: SourceManager by injectLazy()
@ -166,7 +164,7 @@ object DebugFunctions {
it.favorite && db.getSearchMetadataForManga(it.id!!).executeAsBlocking() == null 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") { fun listAllSources() = sourceManager.getCatalogueSources().joinToString("\n") {
"${it.id}: ${it.name} (${it.lang.uppercase()})" "${it.id}: ${it.name} (${it.lang.uppercase()})"

View File

@ -13,4 +13,20 @@ object FavoriteEntryTable {
const val COL_TOKEN = "token" const val COL_TOKEN = "token"
const val COL_CATEGORY = "category" 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",
)
} }

View File

@ -1,5 +1,7 @@
package exh.merged.sql.tables package exh.merged.sql.tables
import eu.kanade.tachiyomi.data.database.tables.MangaTable
object MergedTable { object MergedTable {
const val TABLE = "merged" const val TABLE = "merged"
@ -25,4 +27,30 @@ object MergedTable {
const val COL_MANGA_URL = "manga_url" const val COL_MANGA_URL = "manga_url"
const val COL_MANGA_SOURCE = "manga_source" 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)"
} }

View File

@ -1,5 +1,7 @@
package exh.metadata.sql.tables package exh.metadata.sql.tables
import eu.kanade.tachiyomi.data.database.tables.MangaTable
object SearchMetadataTable { object SearchMetadataTable {
const val TABLE = "search_metadata" const val TABLE = "search_metadata"
@ -12,4 +14,23 @@ object SearchMetadataTable {
const val COL_INDEXED_EXTRA = "indexed_extra" const val COL_INDEXED_EXTRA = "indexed_extra"
const val COL_EXTRA_VERSION = "extra_version" 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)"
} }

View File

@ -1,5 +1,7 @@
package exh.metadata.sql.tables package exh.metadata.sql.tables
import eu.kanade.tachiyomi.data.database.tables.MangaTable
object SearchTagTable { object SearchTagTable {
const val TABLE = "search_tags" const val TABLE = "search_tags"
@ -12,4 +14,22 @@ object SearchTagTable {
const val COL_NAME = "name" const val COL_NAME = "name"
const val COL_TYPE = "type" 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)"
} }

View File

@ -1,5 +1,7 @@
package exh.metadata.sql.tables package exh.metadata.sql.tables
import eu.kanade.tachiyomi.data.database.tables.MangaTable
object SearchTitleTable { object SearchTitleTable {
const val TABLE = "search_titles" const val TABLE = "search_titles"
@ -10,4 +12,21 @@ object SearchTitleTable {
const val COL_TITLE = "title" const val COL_TITLE = "title"
const val COL_TYPE = "type" 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)"
} }

View File

@ -1,5 +1,86 @@
package exh.savedsearches.queries 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.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()
}
}
}*/
}

View File

@ -1,5 +1,93 @@
package exh.savedsearches.queries 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 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()
}
}
}*/
}

View File

@ -11,4 +11,18 @@ object FeedSavedSearchTable {
const val COL_SAVED_SEARCH_ID = "saved_search" const val COL_SAVED_SEARCH_ID = "saved_search"
const val COL_GLOBAL = "global" 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)"
} }

View File

@ -13,4 +13,14 @@ object SavedSearchTable {
const val COL_QUERY = "query" const val COL_QUERY = "query"
const val COL_FILTERS_JSON = "filters_json" 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
)"""
} }

View File

@ -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" />

View 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>

View 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>

View File

@ -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
);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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
);

View File

@ -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;

View File

@ -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
);

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -1,2 +0,0 @@
ALTER TABLE mangas
ADD COLUMN cover_last_modified INTEGER NOT NULL DEFAULT 0;

View File

@ -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
);

View File

@ -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
);

View File

@ -1,16 +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
);
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);

Some files were not shown because too many files have changed in this diff Show More