Revert history Compose/SQLDelight changes

This commit is contained in:
arkon 2022-04-22 17:20:54 -04:00
parent c0214103a9
commit 96c894ce5b
91 changed files with 989 additions and 1809 deletions

View file

@ -6,7 +6,6 @@ plugins {
kotlin("android")
kotlin("plugin.serialization")
id("com.github.zellius.shortcut-helper")
id("com.squareup.sqldelight")
}
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
@ -110,7 +109,6 @@ android {
buildFeatures {
viewBinding = true
compose = true
// Disable some unused things
aidl = false
@ -124,10 +122,6 @@ android {
checkReleaseBuilds = false
}
composeOptions {
kotlinCompilerExtensionVersion = compose.versions.compose.get()
}
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
@ -139,19 +133,6 @@ android {
}
dependencies {
implementation(compose.foundation)
implementation(compose.material3.core)
implementation(compose.material3.adapter)
implementation(compose.animation)
implementation(compose.ui.tooling)
implementation(androidx.paging.runtime)
implementation(androidx.paging.compose)
implementation(libs.sqldelight.android.driver)
implementation(libs.sqldelight.coroutines)
implementation(libs.sqldelight.android.paging)
implementation(kotlinx.reflect)
implementation(kotlinx.bundles.coroutines)
@ -282,9 +263,6 @@ tasks {
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-Xopt-in=coil.annotation.ExperimentalCoilApi",
"-Xopt-in=androidx.compose.material3.ExperimentalMaterial3Api",
"-Xopt-in=androidx.compose.ui.ExperimentalComposeUiApi",
"-Xopt-in=androidx.compose.foundation.ExperimentalFoundationApi"
)
}

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.*
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,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.*
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,26 +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) -> Manga =
{ id, source, url, artist, author, description, genre, title, status, thumbnailUrl, favorite, lastUpdate, _, initialized, viewer, chapterFlags, coverLastModified, dateAdded ->
Manga(
id = id,
source = source,
favorite = favorite,
lastUpdate = lastUpdate ?: 0,
dateAdded = dateAdded,
viewerFlags = viewer,
chapterFlags = chapterFlags,
coverLastModified = coverLastModified,
url = url,
title = title,
artist = artist,
author = author,
description = description,
genre = genre,
status = status,
thumbnailUrl = thumbnailUrl,
initialized = initialized,
)
}

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.*
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.*
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,36 +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
) {
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

@ -24,7 +24,6 @@ import coil.decode.GifDecoder
import coil.decode.ImageDecoderDecoder
import coil.disk.DiskCache
import coil.util.DebugLogger
import eu.kanade.domain.DomainModule
import eu.kanade.tachiyomi.data.coil.MangaCoverFetcher
import eu.kanade.tachiyomi.data.coil.MangaCoverKeyer
import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
@ -76,7 +75,6 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
}
Injekt.importModule(AppModule(this))
Injekt.importModule(DomainModule())
setupAcra()
setupNotificationChannels()

View file

@ -2,18 +2,9 @@ package eu.kanade.tachiyomi
import android.app.Application
import androidx.core.content.ContextCompat
import com.squareup.sqldelight.android.AndroidSqliteDriver
import com.squareup.sqldelight.db.SqlDriver
import data.History
import data.Mangas
import eu.kanade.data.AndroidDatabaseHandler
import eu.kanade.data.DatabaseHandler
import eu.kanade.data.dateAdapter
import eu.kanade.data.listOfStringsAdapter
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.DbOpenCallback
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.saver.ImageSaver
@ -34,37 +25,11 @@ class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)
addSingletonFactory { DbOpenCallback() }
addSingletonFactory<SqlDriver> {
AndroidSqliteDriver(
schema = Database.Schema,
context = app,
name = DbOpenCallback.DATABASE_NAME,
callback = get<DbOpenCallback>()
)
}
addSingletonFactory {
Database(
driver = get(),
historyAdapter = History.Adapter(
history_last_readAdapter = dateAdapter,
history_time_readAdapter = dateAdapter
),
mangasAdapter = Mangas.Adapter(
genreAdapter = listOfStringsAdapter
)
)
}
addSingletonFactory<DatabaseHandler> { AndroidDatabaseHandler(get(), get()) }
addSingletonFactory { Json { ignoreUnknownKeys = true } }
addSingletonFactory { PreferencesHelper(app) }
addSingletonFactory { DatabaseHelper(app, get()) }
addSingletonFactory { DatabaseHelper(app) }
addSingletonFactory { ChapterCache(app) }
@ -92,8 +57,6 @@ class AppModule(val app: Application) : InjektModule {
get<SourceManager>()
get<Database>()
get<DatabaseHelper>()
get<DownloadManager>()

View file

@ -299,7 +299,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
}
}
}
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
}
/**

View file

@ -168,7 +168,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
}
}
}
databaseHelper.upsertHistoryLastRead(historyToBeUpdated).executeAsBlocking()
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
}
/**

View file

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

View file

@ -2,29 +2,99 @@ package eu.kanade.tachiyomi.data.database
import androidx.sqlite.db.SupportSQLiteDatabase
import androidx.sqlite.db.SupportSQLiteOpenHelper
import com.squareup.sqldelight.android.AndroidSqliteDriver
import eu.kanade.tachiyomi.Database
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.data.database.tables.TrackTable
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(Database.Schema.version) {
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
companion object {
/**
* Name of the database file.
*/
const val DATABASE_NAME = "tachiyomi.db"
/**
* Version of the database.
*/
const val DATABASE_VERSION = 14
}
override fun onCreate(db: SupportSQLiteDatabase) {
Database.Schema.create(AndroidSqliteDriver(database = db, cacheSize = 1))
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
execSQL(MangaTable.createTableQuery)
execSQL(ChapterTable.createTableQuery)
execSQL(TrackTable.createTableQuery)
execSQL(CategoryTable.createTableQuery)
execSQL(MangaCategoryTable.createTableQuery)
execSQL(HistoryTable.createTableQuery)
// DB indexes
execSQL(MangaTable.createUrlIndexQuery)
execSQL(MangaTable.createLibraryIndexQuery)
execSQL(ChapterTable.createMangaIdIndexQuery)
execSQL(ChapterTable.createUnreadChaptersIndexQuery)
execSQL(HistoryTable.createChapterIdIndexQuery)
}
override fun onUpgrade(db: SupportSQLiteDatabase, oldVersion: Int, newVersion: Int) {
Database.Schema.migrate(
driver = AndroidSqliteDriver(database = db, cacheSize = 1),
oldVersion = oldVersion,
newVersion = newVersion
if (oldVersion < 2) {
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
// Fix kissmanga covers after supporting cloudflare
db.execSQL(
"""UPDATE mangas SET thumbnail_url =
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""",
)
}
if (oldVersion < 3) {
// Initialize history tables
db.execSQL(HistoryTable.createTableQuery)
db.execSQL(HistoryTable.createChapterIdIndexQuery)
}
if (oldVersion < 4) {
db.execSQL(ChapterTable.bookmarkUpdateQuery)
}
if (oldVersion < 5) {
db.execSQL(ChapterTable.addScanlator)
}
if (oldVersion < 6) {
db.execSQL(TrackTable.addTrackingUrl)
}
if (oldVersion < 7) {
db.execSQL(TrackTable.addLibraryId)
}
if (oldVersion < 8) {
db.execSQL("DROP INDEX IF EXISTS mangas_favorite_index")
db.execSQL(MangaTable.createLibraryIndexQuery)
db.execSQL(ChapterTable.createUnreadChaptersIndexQuery)
}
if (oldVersion < 9) {
db.execSQL(TrackTable.addStartDate)
db.execSQL(TrackTable.addFinishDate)
}
if (oldVersion < 10) {
db.execSQL(MangaTable.addCoverLastModified)
}
if (oldVersion < 11) {
db.execSQL(MangaTable.addDateAdded)
db.execSQL(MangaTable.backfillDateAdded)
}
if (oldVersion < 12) {
db.execSQL(MangaTable.addNextUpdateCol)
}
if (oldVersion < 13) {
db.execSQL(TrackTable.renameTableToTemp)
db.execSQL(TrackTable.createTableQuery)
db.execSQL(TrackTable.insertFromTempTable)
db.execSQL(TrackTable.dropTempTable)
}
if (oldVersion < 14) {
db.execSQL(ChapterTable.fixDateUploadIfNeeded)
}
}
override fun onConfigure(db: SupportSQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true)

View file

@ -4,11 +4,39 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.resolvers.HistoryUpsertResolver
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import java.util.Date
interface HistoryQueries : DbProvider {
/**
* Insert history into database
* @param history object containing history information
*/
fun insertHistory(history: History) = db.put().`object`(history).prepare()
/**
* Returns history of recent manga containing last read chapter
* @param date recent date range
* @param limit the limit of manga to grab
* @param offset offset the db by
* @param search what to search in the db history
*/
fun getRecentManga(date: Date, limit: Int = 25, offset: Int = 0, search: String = "") = db.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(
RawQuery.builder()
.query(getRecentMangasQuery(search))
.args(date.time, limit, offset)
.observesTables(HistoryTable.TABLE)
.build(),
)
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
fun getHistoryByMangaId(mangaId: Long) = db.get()
.listOfObjects(History::class.java)
.withQuery(
@ -36,9 +64,9 @@ interface HistoryQueries : DbProvider {
* Inserts history object if not yet in database
* @param history history object
*/
fun upsertHistoryLastRead(history: History) = db.put()
fun updateHistoryLastRead(history: History) = db.put()
.`object`(history)
.withPutResolver(HistoryUpsertResolver())
.withPutResolver(HistoryLastReadPutResolver())
.prepare()
/**
@ -46,12 +74,12 @@ interface HistoryQueries : DbProvider {
* Inserts history object if not yet in database
* @param historyList history object list
*/
fun upsertHistoryLastRead(historyList: List<History>) = db.put()
fun updateHistoryLastRead(historyList: List<History>) = db.put()
.objects(historyList)
.withPutResolver(HistoryUpsertResolver())
.withPutResolver(HistoryLastReadPutResolver())
.prepare()
fun dropHistoryTable() = db.delete()
fun deleteHistory() = db.delete()
.byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE)

View file

@ -70,8 +70,7 @@ fun getRecentMangasQuery(search: String = "") =
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
) AS max_last_read
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}

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.tables.HistoryTable
class HistoryUpsertResolver : HistoryPutResolver() {
class HistoryLastReadPutResolver : HistoryPutResolver() {
/**
* Updates last_read time of chapter

View file

@ -11,4 +11,13 @@ object CategoryTable {
const val COL_ORDER = "sort"
const val COL_FLAGS = "flags"
val createTableQuery: String
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_NAME TEXT NOT NULL,
$COL_ORDER INTEGER NOT NULL,
$COL_FLAGS INTEGER NOT NULL
)"""
}

View file

@ -27,4 +27,42 @@ object ChapterTable {
const val COL_CHAPTER_NUMBER = "chapter_number"
const val COL_SOURCE_ORDER = "source_order"
val createTableQuery: String
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_URL TEXT NOT NULL,
$COL_NAME TEXT NOT NULL,
$COL_SCANLATOR TEXT,
$COL_READ BOOLEAN NOT NULL,
$COL_BOOKMARK BOOLEAN NOT NULL,
$COL_LAST_PAGE_READ INT NOT NULL,
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
$COL_SOURCE_ORDER INTEGER NOT NULL,
$COL_DATE_FETCH LONG NOT NULL,
$COL_DATE_UPLOAD LONG NOT NULL,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE
)"""
val createMangaIdIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)"
val createUnreadChaptersIndexQuery: String
get() = "CREATE INDEX ${TABLE}_unread_by_manga_index ON $TABLE($COL_MANGA_ID, $COL_READ) " +
"WHERE $COL_READ = 0"
val sourceOrderUpdateQuery: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
val bookmarkUpdateQuery: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
val addScanlator: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
val fixDateUploadIfNeeded: String
get() = "UPDATE $TABLE SET $COL_DATE_UPLOAD = $COL_DATE_FETCH WHERE $COL_DATE_UPLOAD = 0"
}

View file

@ -26,4 +26,24 @@ object HistoryTable {
* Time read column name
*/
const val COL_TIME_READ = "${TABLE}_time_read"
/**
* query to create history table
*/
val createTableQuery: String
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
$COL_LAST_READ LONG,
$COL_TIME_READ LONG,
FOREIGN KEY($COL_CHAPTER_ID) REFERENCES ${ChapterTable.TABLE} (${ChapterTable.COL_ID})
ON DELETE CASCADE
)"""
/**
* query to index history chapter id
*/
val createChapterIdIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_CHAPTER_ID}_index ON $TABLE($COL_CHAPTER_ID)"
}

View file

@ -9,4 +9,16 @@ object MangaCategoryTable {
const val COL_MANGA_ID = "manga_id"
const val COL_CATEGORY_ID = "category_id"
val createTableQuery: String
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_CATEGORY_ID INTEGER NOT NULL,
FOREIGN KEY($COL_CATEGORY_ID) REFERENCES ${CategoryTable.TABLE} (${CategoryTable.COL_ID})
ON DELETE CASCADE,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE
)"""
}

View file

@ -47,4 +47,53 @@ object MangaTable {
const val COMPUTED_COL_UNREAD_COUNT = "unread_count"
const val COMPUTED_COL_READ_COUNT = "read_count"
val createTableQuery: String
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_SOURCE INTEGER NOT NULL,
$COL_URL TEXT NOT NULL,
$COL_ARTIST TEXT,
$COL_AUTHOR TEXT,
$COL_DESCRIPTION TEXT,
$COL_GENRE TEXT,
$COL_TITLE TEXT NOT NULL,
$COL_STATUS INTEGER NOT NULL,
$COL_THUMBNAIL_URL TEXT,
$COL_FAVORITE INTEGER NOT NULL,
$COL_LAST_UPDATE LONG,
$COL_NEXT_UPDATE LONG,
$COL_INITIALIZED BOOLEAN NOT NULL,
$COL_VIEWER INTEGER NOT NULL,
$COL_CHAPTER_FLAGS INTEGER NOT NULL,
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
$COL_DATE_ADDED LONG NOT NULL
)"""
val createUrlIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_URL}_index ON $TABLE($COL_URL)"
val createLibraryIndexQuery: String
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
"WHERE $COL_FAVORITE = 1"
val addCoverLastModified: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0"
val addDateAdded: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG NOT NULL DEFAULT 0"
/**
* Used with addDateAdded to populate it with the oldest chapter fetch date.
*/
val backfillDateAdded: String
get() = "UPDATE $TABLE SET $COL_DATE_ADDED = " +
"(SELECT MIN(${ChapterTable.COL_DATE_FETCH}) " +
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
"GROUP BY $TABLE.$COL_ID)"
val addNextUpdateCol: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_NEXT_UPDATE LONG DEFAULT 0"
}

View file

@ -30,6 +30,43 @@ object TrackTable {
const val COL_FINISH_DATE = "finish_date"
val createTableQuery: String
get() =
"""CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_SYNC_ID INTEGER NOT NULL,
$COL_MEDIA_ID INTEGER NOT NULL,
$COL_LIBRARY_ID INTEGER,
$COL_TITLE TEXT NOT NULL,
$COL_LAST_CHAPTER_READ REAL NOT NULL,
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
$COL_STATUS INTEGER NOT NULL,
$COL_SCORE FLOAT NOT NULL,
$COL_TRACKING_URL TEXT NOT NULL,
$COL_START_DATE LONG NOT NULL,
$COL_FINISH_DATE LONG NOT NULL,
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE
)"""
val addTrackingUrl: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_TRACKING_URL TEXT DEFAULT ''"
val addLibraryId: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_LIBRARY_ID INTEGER NULL"
val addStartDate: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_START_DATE LONG NOT NULL DEFAULT 0"
val addFinishDate: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_FINISH_DATE LONG NOT NULL DEFAULT 0"
val renameTableToTemp: String
get() =
"ALTER TABLE $TABLE RENAME TO ${TABLE}_tmp"
val insertFromTempTable: String
get() =
"""
@ -37,4 +74,7 @@ object TrackTable {
|SELECT $COL_ID,$COL_MANGA_ID,$COL_SYNC_ID,$COL_MEDIA_ID,$COL_LIBRARY_ID,$COL_TITLE,$COL_LAST_CHAPTER_READ,$COL_TOTAL_CHAPTERS,$COL_STATUS,$COL_SCORE,$COL_TRACKING_URL,$COL_START_DATE,$COL_FINISH_DATE
|FROM ${TABLE}_tmp
""".trimMargin()
val dropTempTable: String
get() = "DROP TABLE ${TABLE}_tmp"
}

View file

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

View file

@ -7,7 +7,6 @@ import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import com.fredporciuncula.flow.preferences.FlowSharedPreferences
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.TrackService
@ -19,6 +18,7 @@ import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import java.io.File
import java.text.DateFormat
@ -26,7 +26,6 @@ import java.text.SimpleDateFormat
import java.util.Locale
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
import eu.kanade.tachiyomi.util.system.isDevFlavor
class PreferencesHelper(val context: Context) {

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

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

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

@ -97,19 +97,14 @@ import kotlin.math.max
class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
companion object {
fun newIntent(context: Context, mangaId: Long?, chapterId: Long?): Intent {
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
return Intent(context, ReaderActivity::class.java).apply {
putExtra("manga", mangaId)
putExtra("chapter", chapterId)
putExtra("manga", manga.id)
putExtra("chapter", chapter.id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
return newIntent(context, manga.id, chapter.id)
}
private const val ENABLED_BUTTON_IMAGE_ALPHA = 255
private const val DISABLED_BUTTON_IMAGE_ALPHA = 64

View file

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

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
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.widget.SearchView
import androidx.compose.runtime.Composable
import eu.kanade.domain.chapter.model.Chapter
import eu.kanade.presentation.history.HistoryScreen
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.ComposeController
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.HistoryControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.ProgressItem
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import logcat.LogPriority
import reactivecircus.flowbinding.appcompat.queryTextChanges
import uy.kohesive.injekt.injectLazy
class HistoryController : ComposeController<HistoryPresenter>(), RootController {
/**
* Fragment that shows recently read manga.
*/
class HistoryController :
NucleusController<HistoryControllerBinding, HistoryPresenter>(),
RootController,
FlexibleAdapter.OnUpdateListener,
FlexibleAdapter.EndlessScrollListener,
HistoryAdapter.OnRemoveClickListener,
HistoryAdapter.OnResumeClickListener,
HistoryAdapter.OnItemClickListener,
RemoveHistoryDialog.Listener {
private val db: DatabaseHelper by injectLazy()
/**
* Adapter containing the recent manga.
*/
var adapter: HistoryAdapter? = null
private set
/**
* Endless loading item.
*/
private var progressItem: ProgressItem? = null
/**
* Search query.
*/
private var query = ""
override fun getTitle() = resources?.getString(R.string.label_recent_manga)
override fun getTitle(): String? {
return resources?.getString(R.string.label_recent_manga)
}
override fun createPresenter() = HistoryPresenter()
override fun createPresenter(): HistoryPresenter {
return HistoryPresenter()
}
@Composable
override fun ComposeContent() {
HistoryScreen(
composeView = binding.root,
presenter = presenter,
onClickItem = { history ->
router.pushController(MangaController(history).withFadeTransaction())
},
onClickResume = { history ->
presenter.getNextChapterForManga(history.mangaId, history.chapterId)
},
onClickDelete = { history, all ->
override fun createBinding(inflater: LayoutInflater) = HistoryControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
// Initialize adapter
binding.recycler.layoutManager = LinearLayoutManager(view.context)
adapter = HistoryAdapter(this@HistoryController)
binding.recycler.setHasFixedSize(true)
binding.recycler.adapter = adapter
adapter?.fastScroller = binding.fastScroller
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
/**
* Populate adapter with chapters
*
* @param mangaHistory list of manga history
*/
fun onNextManga(mangaHistory: List<HistoryItem>, cleanBatch: Boolean = false) {
if (adapter?.itemCount ?: 0 == 0) {
resetProgressItem()
}
if (cleanBatch) {
adapter?.updateDataSet(mangaHistory)
} else {
adapter?.onLoadMoreComplete(mangaHistory)
}
binding.recycler.onAnimationsFinished {
(activity as? MainActivity)?.ready = true
}
}
/**
* Safely error if next page load fails
*/
fun onAddPageError(error: Throwable) {
adapter?.onLoadMoreComplete(null)
adapter?.endlessTargetCount = 1
logcat(LogPriority.ERROR, error)
}
override fun onUpdateEmptyView(size: Int) {
if (size > 0) {
binding.emptyView.hide()
} else {
binding.emptyView.show(R.string.information_no_recent_manga)
}
}
/**
* Sets a new progress item and reenables the scroll listener.
*/
private fun resetProgressItem() {
progressItem = ProgressItem()
adapter?.endlessTargetCount = 0
adapter?.setEndlessScrollListener(this, progressItem!!)
}
override fun onLoadMore(lastPosition: Int, currentPage: Int) {
val view = view ?: return
if (BackupRestoreService.isRunning(view.context.applicationContext)) {
onAddPageError(Throwable())
return
}
val adapter = adapter ?: return
presenter.requestNext(adapter.itemCount - adapter.headerItems.size, query)
}
override fun noMoreLoad(newItemsSize: Int) {}
override fun onResumeClick(position: Int) {
val activity = activity ?: return
val (manga, chapter, _) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return
val nextChapter = presenter.getNextChapter(chapter, manga)
if (nextChapter != null) {
val intent = ReaderActivity.newIntent(activity, manga, nextChapter)
startActivity(intent)
} else {
activity.toast(R.string.no_next_chapter)
}
}
override fun onRemoveClick(position: Int) {
val (manga, _, history) = (adapter?.getItem(position) as? HistoryItem)?.mch ?: return
RemoveHistoryDialog(this, manga, history).showDialog(router)
}
override fun onItemClick(position: Int) {
val manga = (adapter?.getItem(position) as? HistoryItem)?.mch?.manga ?: return
router.pushController(MangaController(manga).withFadeTransaction())
}
override fun removeHistory(manga: Manga, history: History, all: Boolean) {
if (all) {
// Reset last read of chapter to 0L
presenter.removeAllFromHistory(history.mangaId)
presenter.removeAllFromHistory(manga.id!!)
} else {
// Remove all chapters belonging to manga from library
presenter.removeFromHistory(history)
}
},
)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
@ -61,33 +201,46 @@ class HistoryController : ComposeController<HistoryPresenter>(), RootController
searchView.clearFocus()
}
searchView.queryTextChanges()
.drop(1) // Drop first event after subscribed
.filter { router.backstack.lastOrNull()?.controller == this }
.onEach {
query = it.toString()
presenter.search(query)
presenter.updateList(query)
}
.launchIn(viewScope)
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.fixExpand(
onExpand = { invalidateMenuOnExpand() },
)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
return when (item.itemId) {
when (item.itemId) {
R.id.action_clear_history -> {
val dialog = ClearHistoryDialogController()
dialog.targetController = this@HistoryController
dialog.showDialog(router)
true
}
else -> super.onOptionsItemSelected(item)
val ctrl = ClearHistoryDialogController()
ctrl.targetController = this@HistoryController
ctrl.showDialog(router)
}
}
fun openChapter(chapter: Chapter?) {
val activity = activity ?: return
if (chapter != null) {
val intent = ReaderActivity.newIntent(activity, chapter.mangaId, chapter.id)
startActivity(intent)
} else {
activity.toast(R.string.no_next_chapter)
return super.onOptionsItemSelected(item)
}
class ClearHistoryDialogController : DialogController() {
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialAlertDialogBuilder(activity!!)
.setMessage(R.string.clear_history_confirmation)
.setPositiveButton(android.R.string.ok) { _, _ ->
(targetController as? HistoryController)?.clearHistory()
}
.setNegativeButton(android.R.string.cancel, null)
.create()
}
}
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
import android.os.Bundle
import androidx.paging.PagingData
import androidx.paging.cachedIn
import androidx.paging.insertSeparators
import androidx.paging.map
import eu.kanade.domain.history.interactor.DeleteHistoryTable
import eu.kanade.domain.history.interactor.GetHistory
import eu.kanade.domain.history.interactor.GetNextChapterForManga
import eu.kanade.domain.history.interactor.RemoveHistoryById
import eu.kanade.domain.history.interactor.RemoveHistoryByMangaId
import eu.kanade.domain.history.model.HistoryWithRelations
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.ui.recent.DateSectionItem
import eu.kanade.tachiyomi.util.lang.toDateKey
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.update
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.injectLazy
import java.text.DateFormat
import java.util.Calendar
import java.util.Date
import java.util.TreeMap
/**
* Presenter of HistoryFragment.
* Contains information and data for fragment.
* Observable updates should be called from here.
*/
class HistoryPresenter(
private val getHistory: GetHistory = Injekt.get(),
private val getNextChapterForManga: GetNextChapterForManga = Injekt.get(),
private val deleteHistoryTable: DeleteHistoryTable = Injekt.get(),
private val removeHistoryById: RemoveHistoryById = Injekt.get(),
private val removeHistoryByMangaId: RemoveHistoryByMangaId = Injekt.get(),
) : BasePresenter<HistoryController>() {
class HistoryPresenter : BasePresenter<HistoryController>() {
private var _query: MutableStateFlow<String> = MutableStateFlow("")
private var _state: MutableStateFlow<HistoryState> = MutableStateFlow(HistoryState.EMPTY)
val state: StateFlow<HistoryState> = _state
private val db: DatabaseHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
private val relativeTime: Int = preferences.relativeTime().get()
private val dateFormat: DateFormat = preferences.dateFormat()
private var recentMangaSubscription: Subscription? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
presenterScope.launchIO {
_state.update { state ->
state.copy(
list = _query.flatMapLatest { query ->
getHistory.subscribe(query)
.map { pagingData ->
pagingData
.map {
UiModel.Item(it)
// Used to get a list of recently read manga
updateList()
}
.insertSeparators { before, after ->
val beforeDate = before?.item?.readAt?.time?.toDateKey() ?: Date(0)
val afterDate = after?.item?.readAt?.time?.toDateKey() ?: Date(0)
when {
beforeDate.time != afterDate.time && afterDate.time != 0L -> UiModel.Header(afterDate)
// Return null to avoid adding a separator between two items.
else -> null
}
}
}
}
.cachedIn(presenterScope),
fun requestNext(offset: Int, search: String = "") {
getRecentMangaObservable(offset = offset, search = search)
.subscribeLatestCache(
{ view, mangas ->
view.onNextManga(mangas)
},
HistoryController::onAddPageError,
)
}
}
/**
* Get recent manga observable
* @return list of history
*/
private fun getRecentMangaObservable(limit: Int = 25, offset: Int = 0, search: String = ""): Observable<List<HistoryItem>> {
// Set date limit for recent manga
val cal = Calendar.getInstance().apply {
time = Date()
add(Calendar.YEAR, -50)
}
fun search(query: String) {
presenterScope.launchIO {
_query.emit(query)
return db.getRecentManga(cal.time, limit, offset, search).asRxObservable()
.map { recents ->
val map = TreeMap<Date, MutableList<MangaChapterHistory>> { d1, d2 -> d2.compareTo(d1) }
val byDay = recents
.groupByTo(map) { it.history.last_read.toDateKey() }
byDay.flatMap { entry ->
val dateItem = DateSectionItem(entry.key, relativeTime, dateFormat)
entry.value.map { HistoryItem(it, dateItem) }
}
}
.observeOn(AndroidSchedulers.mainThread())
}
fun removeFromHistory(history: HistoryWithRelations) {
presenterScope.launchIO {
removeHistoryById.await(history)
}
/**
* Reset last read of chapter to 0L
* @param history history belonging to chapter
*/
fun removeFromHistory(history: History) {
history.last_read = 0L
db.updateHistoryLastRead(history).asRxObservable()
.subscribe()
}
/**
* Pull a list of history from the db
* @param search a search query to use for filtering
*/
fun updateList(search: String = "") {
recentMangaSubscription?.unsubscribe()
recentMangaSubscription = getRecentMangaObservable(search = search)
.subscribeLatestCache(
{ view, mangas ->
view.onNextManga(mangas, true)
},
HistoryController::onAddPageError,
)
}
/**
* Removes all chapters belonging to manga from history.
* @param mangaId id of manga
*/
fun removeAllFromHistory(mangaId: Long) {
presenterScope.launchIO {
removeHistoryByMangaId.await(mangaId)
db.getHistoryByMangaId(mangaId).asRxSingle()
.map { list ->
list.forEach { it.last_read = 0L }
db.updateHistoryLastRead(list).executeAsBlocking()
}
.subscribe()
}
fun getNextChapterForManga(mangaId: Long, chapterId: Long) {
presenterScope.launchIO {
val chapter = getNextChapterForManga.await(mangaId, chapterId)
launchUI {
view?.openChapter(chapter)
}
}
/**
* Retrieves the next chapter of the given one.
*
* @param chapter the chapter of the history object.
* @param manga the manga of the chapter.
*/
fun getNextChapter(chapter: Chapter, manga: Manga): Chapter? {
if (!chapter.read) {
return chapter
}
fun deleteAllHistory() {
presenterScope.launchIO {
val result = deleteHistoryTable.await()
if (!result) return@launchIO
launchUI {
view?.activity?.toast(R.string.clear_history_completed)
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
Manga.CHAPTER_SORTING_SOURCE -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
Manga.CHAPTER_SORTING_NUMBER -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
Manga.CHAPTER_SORTING_UPLOAD_DATE -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
else -> throw NotImplementedError("Unknown sorting method")
}
val chapters = db.getChapters(manga).executeAsBlocking()
.sortedWith { c1, c2 -> sortFunction(c1, c2) }
val currChapterIndex = chapters.indexOfFirst { chapter.id == it.id }
return when (manga.sorting) {
Manga.CHAPTER_SORTING_SOURCE -> chapters.getOrNull(currChapterIndex + 1)
Manga.CHAPTER_SORTING_NUMBER -> {
val chapterNumber = chapter.chapter_number
((currChapterIndex + 1) until chapters.size)
.map { chapters[it] }
.firstOrNull {
it.chapter_number > chapterNumber &&
it.chapter_number <= chapterNumber + 1
}
}
Manga.CHAPTER_SORTING_UPLOAD_DATE -> {
chapters.drop(currChapterIndex + 1)
.firstOrNull { it.date_upload >= chapter.date_upload }
}
else -> throw NotImplementedError("Unknown sorting method")
}
}
}
sealed class UiModel {
data class Item(val item: HistoryWithRelations) : UiModel()
data class Header(val date: Date) : UiModel()
}
data class HistoryState(
val list: Flow<PagingData<UiModel>>? = null,
) {
companion object {
val EMPTY = HistoryState(null)
}
}

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

@ -9,7 +9,6 @@ import android.webkit.WebView
import androidx.core.net.toUri
import androidx.preference.PreferenceScreen
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -40,6 +39,7 @@ import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.powerManager
@ -49,7 +49,6 @@ import logcat.LogPriority
import rikka.sui.Sui
import uy.kohesive.injekt.injectLazy
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.util.system.isDevFlavor
class SettingsAdvancedController : SettingsController() {

View file

@ -9,6 +9,7 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.core.view.forEach
import androidx.core.view.get
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
@ -36,6 +37,7 @@ class ClearDatabaseController :
private var menu: Menu? = null
private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
init {
setHasOptionsMenu(true)
@ -141,6 +143,7 @@ class ClearDatabaseController :
override fun cleanupFab(fab: ExtendedFloatingActionButton) {
actionFab?.setOnClickListener(null)
actionFabScrollListener?.let { recycler?.removeOnScrollListener(it) }
actionFab = null
}

View file

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

View file

@ -5,10 +5,8 @@ import android.os.Parcel
import android.os.Parcelable
import android.util.AttributeSet
import android.view.View
import androidx.compose.ui.platform.ComposeView
import androidx.coordinatorlayout.R
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.view.doOnLayout
import androidx.core.view.isVisible
import androidx.customview.view.AbsSavedState
@ -65,18 +63,9 @@ class TachiyomiCoordinatorLayout @JvmOverloads constructor(
super.onNestedScroll(target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed, type, consumed)
// Disable elevation overlay when tabs are visible
if (canLiftAppBarOnScroll) {
if (target is ComposeView) {
val scrollCondition = if (type == ViewCompat.TYPE_NON_TOUCH) {
dyUnconsumed >= 0
} else {
dyConsumed != 0 || dyUnconsumed >= 0
}
appBarLayout?.isLifted = scrollCondition && tabLayout?.isVisible == false
} else {
appBarLayout?.isLifted = (dyConsumed != 0 || dyUnconsumed >= 0) && tabLayout?.isVisible == false
}
}
}
override fun onAttachedToWindow() {
super.onAttachedToWindow()

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,6 +0,0 @@
CREATE TABLE categories(
_id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
sort INTEGER NOT NULL,
flags INTEGER 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,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,31 +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
);
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,6 +0,0 @@
ALTER TABLE chapters
ADD COLUMN source_order INTEGER DEFAULT 0;
UPDATE mangas
SET thumbnail_url = replace(thumbnail_url, '93.174.95.110', 'kissmanga.com')
WHERE source = 4;

View file

@ -1,11 +0,0 @@
ALTER TABLE mangas
ADD COLUMN date_added INTEGER NOT NULL DEFAULT 0;
UPDATE mangas
SET date_added = (
SELECT MIN(date_fetch)
FROM mangas M
INNER JOIN chapters C
ON M._id = C.manga_id
GROUP BY M._id
);

View file

@ -1,2 +0,0 @@
ALTER TABLE mangas
ADD COLUMN next_update INTEGER DEFAULT 0;

View file

@ -1,9 +0,0 @@
ALTER TABLE manga_sync
RENAME TO manga_sync_tmp;
INSERT INTO manga_sync(_id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date)
SELECT _id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date
FROM manga_sync_tmp;
DROP TABLE manga_sync_tmp;

View file

@ -1,3 +0,0 @@
UPDATE chapters
SET date_upload = date_fetch
WHERE date_upload = 0;

View file

@ -1,149 +0,0 @@
DROP INDEX IF EXISTS chapters_manga_id_index;
DROP INDEX IF EXISTS chapters_unread_by_manga_index;
DROP INDEX IF EXISTS history_history_chapter_id_index;
DROP INDEX IF EXISTS library_favorite_index;
DROP INDEX IF EXISTS mangas_url_index;
ALTER TABLE mangas RENAME TO manga_temp;
CREATE TABLE mangas(
_id INTEGER NOT NULL PRIMARY KEY,
source INTEGER NOT NULL,
url TEXT NOT NULL,
artist TEXT,
author TEXT,
description TEXT,
genre TEXT,
title TEXT NOT NULL,
status INTEGER NOT NULL,
thumbnail_url TEXT,
favorite INTEGER NOT NULL,
last_update INTEGER AS Long,
next_update INTEGER AS Long,
initialized INTEGER AS Boolean NOT NULL,
viewer INTEGER NOT NULL,
chapter_flags INTEGER NOT NULL,
cover_last_modified INTEGER AS Long NOT NULL,
date_added INTEGER AS Long NOT NULL
);
INSERT INTO mangas
SELECT _id,source,url,artist,author,description,genre,title,status,thumbnail_url,favorite,last_update,next_update,initialized,viewer,chapter_flags,cover_last_modified,date_added
FROM manga_temp;
ALTER TABLE categories RENAME TO categories_temp;
CREATE TABLE categories(
_id INTEGER NOT NULL PRIMARY KEY,
name TEXT NOT NULL,
sort INTEGER NOT NULL,
flags INTEGER NOT NULL
);
INSERT INTO categories
SELECT _id,name,sort,flags
FROM categories_temp;
ALTER TABLE chapters RENAME TO chapters_temp;
CREATE TABLE chapters(
_id INTEGER NOT NULL PRIMARY KEY,
manga_id INTEGER NOT NULL,
url TEXT NOT NULL,
name TEXT NOT NULL,
scanlator TEXT,
read INTEGER AS Boolean NOT NULL,
bookmark INTEGER AS Boolean NOT NULL,
last_page_read INTEGER NOT NULL,
chapter_number REAL AS Float NOT NULL,
source_order INTEGER NOT NULL,
date_fetch INTEGER AS Long NOT NULL,
date_upload INTEGER AS Long NOT NULL,
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
ON DELETE CASCADE
);
INSERT INTO chapters
SELECT _id,manga_id,url,name,scanlator,read,bookmark,last_page_read,chapter_number,source_order,date_fetch,date_upload
FROM chapters_temp;
ALTER TABLE history RENAME TO history_temp;
CREATE TABLE history(
history_id INTEGER NOT NULL PRIMARY KEY,
history_chapter_id INTEGER NOT NULL UNIQUE,
history_last_read INTEGER AS Long,
history_time_read INTEGER AS Long,
FOREIGN KEY(history_chapter_id) REFERENCES chapters (_id)
ON DELETE CASCADE
);
INSERT INTO history
SELECT history_id, history_chapter_id, history_last_read, history_time_read
FROM history_temp;
ALTER TABLE mangas_categories RENAME TO mangas_categories_temp;
CREATE TABLE mangas_categories(
_id INTEGER NOT NULL PRIMARY KEY,
manga_id INTEGER NOT NULL,
category_id INTEGER NOT NULL,
FOREIGN KEY(category_id) REFERENCES categories (_id)
ON DELETE CASCADE,
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
ON DELETE CASCADE
);
INSERT INTO mangas_categories
SELECT _id, manga_id, category_id
FROM mangas_categories_temp;
ALTER TABLE manga_sync RENAME TO manga_sync_temp;
CREATE TABLE manga_sync(
_id INTEGER NOT NULL PRIMARY KEY,
manga_id INTEGER NOT NULL,
sync_id INTEGER NOT NULL,
remote_id INTEGER NOT NULL,
library_id INTEGER,
title TEXT NOT NULL,
last_chapter_read REAL NOT NULL,
total_chapters INTEGER NOT NULL,
status INTEGER NOT NULL,
score REAL AS Float NOT NULL,
remote_url TEXT NOT NULL,
start_date INTEGER AS Long NOT NULL,
finish_date INTEGER AS Long NOT NULL,
UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE,
FOREIGN KEY(manga_id) REFERENCES mangas (_id)
ON DELETE CASCADE
);
INSERT INTO manga_sync
SELECT _id, manga_id, sync_id, remote_id, library_id, title, last_chapter_read, total_chapters, status, score, remote_url, start_date, finish_date
FROM manga_sync_temp;
CREATE INDEX chapters_manga_id_index ON chapters(manga_id);
CREATE INDEX chapters_unread_by_manga_index ON chapters(manga_id, read) WHERE read = 0;
CREATE INDEX history_history_chapter_id_index ON history(history_chapter_id);
CREATE INDEX library_favorite_index ON mangas(favorite) WHERE favorite = 1;
CREATE INDEX mangas_url_index ON mangas(url);
CREATE VIEW IF NOT EXISTS historyView AS
SELECT
history.history_id AS id,
mangas._id AS mangaId,
chapters._id AS chapterId,
mangas.title,
mangas.thumbnail_url AS thumnailUrl,
chapters.chapter_number AS chapterNumber,
history.history_last_read AS readAt,
max_last_read.history_last_read AS maxReadAt,
max_last_read.history_chapter_id AS maxReadAtChapterId
FROM mangas
JOIN chapters
ON mangas._id = chapters.manga_id
JOIN history
ON chapters._id = history.history_chapter_id
JOIN (
SELECT chapters.manga_id,chapters._id AS history_chapter_id, MAX(history.history_last_read) AS history_last_read
FROM chapters JOIN history
ON chapters._id = history.history_chapter_id
GROUP BY chapters.manga_id
) AS max_last_read
ON chapters.manga_id = max_last_read.manga_id;
DROP TABLE IF EXISTS manga_sync_temp;
DROP TABLE IF EXISTS mangas_categories_temp;
DROP TABLE IF EXISTS history_temp;
DROP TABLE IF EXISTS chapters_temp;
DROP TABLE IF EXISTS categories_temp;
DROP TABLE IF EXISTS manga_temp;

View file

@ -1,10 +0,0 @@
CREATE TABLE history(
history_id INTEGER NOT NULL PRIMARY KEY,
history_chapter_id INTEGER NOT NULL UNIQUE,
history_last_read INTEGER,
history_time_read INTEGER,
FOREIGN KEY(history_chapter_id) REFERENCES chapters (_id)
ON DELETE CASCADE
);
CREATE INDEX history_history_chapter_id_index ON history(history_chapter_id);

View file

@ -1,2 +0,0 @@
ALTER TABLE chapters
ADD COLUMN bookmark INTEGER DEFAULT 0;

View file

@ -1,2 +0,0 @@
ALTER TABLE chapters
ADD COLUMN scanlator TEXT DEFAULT NULL;

View file

@ -1,2 +0,0 @@
ALTER TABLE manga_sync
ADD COLUMN remote_url TEXT DEFAULT '';

View file

@ -1,2 +0,0 @@
ALTER TABLE manga_sync
ADD COLUMN library_id INTEGER;

View file

@ -1,9 +0,0 @@
DROP INDEX IF EXISTS mangas_favorite_index;
CREATE INDEX library_favorite_index
ON mangas(favorite)
WHERE favorite = 1;
CREATE INDEX chapters_unread_by_manga_index
ON chapters(manga_id, read)
WHERE read = 0;

View file

@ -1,5 +0,0 @@
ALTER TABLE manga_sync
ADD COLUMN start_date INTEGER NOT NULL DEFAULT 0;
ALTER TABLE manga_sync
ADD COLUMN finish_date INTEGER NOT NULL DEFAULT 0;

View file

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

View file

@ -1,46 +0,0 @@
CREATE VIEW historyView AS
SELECT
history.history_id AS id,
mangas._id AS mangaId,
chapters._id AS chapterId,
mangas.title,
mangas.thumbnail_url AS thumnailUrl,
chapters.chapter_number AS chapterNumber,
history.history_last_read AS readAt,
max_last_read.history_last_read AS maxReadAt,
max_last_read.history_chapter_id AS maxReadAtChapterId
FROM mangas
JOIN chapters
ON mangas._id = chapters.manga_id
JOIN history
ON chapters._id = history.history_chapter_id
JOIN (
SELECT chapters.manga_id,chapters._id AS history_chapter_id, MAX(history.history_last_read) AS history_last_read
FROM chapters JOIN history
ON chapters._id = history.history_chapter_id
GROUP BY chapters.manga_id
) AS max_last_read
ON chapters.manga_id = max_last_read.manga_id;
countHistory:
SELECT count(*)
FROM historyView
WHERE historyView.readAt > 0
AND maxReadAtChapterId = historyView.chapterId
AND lower(historyView.title) LIKE ('%' || :query || '%');
history:
SELECT
id,
mangaId,
chapterId,
title,
thumnailUrl,
chapterNumber,
readAt
FROM historyView
WHERE historyView.readAt > 0
AND maxReadAtChapterId = historyView.chapterId
AND lower(historyView.title) LIKE ('%' || :query || '%')
ORDER BY readAt DESC
LIMIT :limit OFFSET :offset;

View file

@ -344,7 +344,7 @@ class BackupTest {
private fun clearDatabase() {
db.deleteMangas().executeAsBlocking()
db.dropHistoryTable().executeAsBlocking()
db.deleteHistory().executeAsBlocking()
}
private fun getSingleHistory(chapter: Chapter): DHistory {

View file

@ -4,7 +4,6 @@ buildscript {
classpath(libs.google.services.gradle)
classpath(libs.aboutlibraries.gradle)
classpath(kotlinx.serialization.gradle)
classpath("com.squareup.sqldelight:gradle-plugin:1.5.3")
}
}

View file

@ -21,9 +21,6 @@ lifecycle-runtimektx = { module = "androidx.lifecycle:lifecycle-runtime-ktx", ve
work-runtime = "androidx.work:work-runtime-ktx:2.6.0"
guava = "com.google.guava:guava:31.1-android"
paging-runtime = "androidx.paging:paging-runtime:3.1.1"
paging-compose = "androidx.paging:paging-compose:1.0.0-alpha14"
[bundles]
lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
workmanager = ["work-runtime", "guava"]

View file

@ -1,9 +0,0 @@
[versions]
compose = "1.2.0-alpha07"
[libraries]
foundation = { module = "androidx.compose.foundation:foundation", version.ref="compose" }
material3-core = "androidx.compose.material3:material3:1.0.0-alpha09"
material3-adapter = "com.google.android.material:compose-theme-adapter-3:1.0.6"
animation = { module = "androidx.compose.animation:animation", version.ref="compose" }
ui-tooling = { module = "androidx.compose.ui:ui-tooling", version.ref="compose" }

View file

@ -1,5 +1,5 @@
[versions]
kotlin_version = "1.6.10"
kotlin_version = "1.6.20"
coroutines_version = "1.6.1"
serialization_version = "1.3.2"

View file

@ -7,7 +7,6 @@ conductor_version = "3.1.2"
flowbinding_version = "1.2.0"
shizuku_version = "12.1.0"
robolectric_version = "3.1.4"
sqldelight = "1.5.3"
[libraries]
android-shortcut-gradle = "com.github.zellius:android-shortcut-gradle-plugin:0.1.2"
@ -50,7 +49,6 @@ injekt-core = "com.github.inorichi.injekt:injekt-core:65b0440"
coil-core = { module = "io.coil-kt:coil", version.ref = "coil_version" }
coil-gif = { module = "io.coil-kt:coil-gif", version.ref = "coil_version" }
coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil_version" }
subsamplingscaleimageview = "com.github.tachiyomiorg:subsampling-scale-image-view:846abe0"
image-decoder = "com.github.tachiyomiorg:image-decoder:7481a4a"
@ -80,7 +78,7 @@ flowbinding-viewpager = { module = "io.github.reactivecircus.flowbinding:flowbin
logcat = "com.squareup.logcat:logcat:0.1"
acra-http = "ch.acra:acra-http:5.9.1"
acra-http = "ch.acra:acra-http:5.9.3"
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:20.0.2"
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlib_version" }
@ -98,22 +96,18 @@ robolectric-playservices = { module = "org.robolectric:shadows-play-services", v
leakcanary-android = "com.squareup.leakcanary:leakcanary-android:2.7"
sqldelight-android-driver = { module = "com.squareup.sqldelight:android-driver", version.ref ="sqldelight" }
sqldelight-coroutines = { module = "com.squareup.sqldelight:coroutines-extensions-jvm", version.ref ="sqldelight" }
sqldelight-android-paging = { module = "com.squareup.sqldelight:android-paging3-extensions", version.ref ="sqldelight" }
[bundles]
reactivex = ["rxandroid","rxjava","rxrelay"]
okhttp = ["okhttp-core","okhttp-logging","okhttp-dnsoverhttps"]
js-engine = ["quickjs-android", "duktape-android"]
sqlite = ["sqlitektx", "sqlite-android"]
nucleus = ["nucleus-core","nucleus-supportv7"]
coil = ["coil-core","coil-gif","coil-compose"]
coil = ["coil-core","coil-gif",]
flowbinding = ["flowbinding-android","flowbinding-appcompat","flowbinding-recyclerview","flowbinding-swiperefreshlayout","flowbinding-viewpager"]
conductor = ["conductor-core","conductor-viewpager","conductor-support-preference"]
shizuku = ["shizuku-api","shizuku-provider"]
robolectric = ["robolectric-core","robolectric-playservices"]
[plugins]
kotlinter = { id = "org.jmailen.kotlinter", version = "3.6.0"}
kotlinter = { id = "org.jmailen.kotlinter", version = "3.10.0"}
versionsx = { id = "com.github.ben-manes.versions", version = "0.42.0"}

View file

@ -22,9 +22,6 @@ dependencyResolutionManagement {
create("androidx") {
from(files("gradle/androidx.versions.toml"))
}
create("compose") {
from(files("gradle/compose.versions.toml"))
}
}
repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS)
repositories {