update libraries + lint + compile to sdk 35

update theme to dynamiccolors
This commit is contained in:
Jays2Kings
2025-04-29 15:40:25 -07:00
parent b71d16681a
commit 60634065ca
619 changed files with 28919 additions and 22990 deletions

View File

@@ -9,6 +9,7 @@ plugins {
id("com.google.android.gms.oss-licenses-plugin")
id(Plugins.googleServices) apply false
id("com.google.firebase.crashlytics")
id("org.jetbrains.kotlin.plugin.compose") version AndroidVersions.kotlin // this version matches your Kotlin version
}
if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
@@ -100,8 +101,8 @@ android {
buildConfigField("Boolean", "INCLUDE_UPDATER", "true")
}
create("dev") {
resourceConfigurations.clear()
resourceConfigurations.add("en")
androidResources.localeFilters.clear()
androidResources.localeFilters.add("en")
}
}
@@ -127,18 +128,18 @@ android {
dependencies {
// Compose
implementation("androidx.activity:activity-compose:1.7.2")
implementation("androidx.compose.foundation:foundation:1.5.1")
implementation("androidx.compose.animation:animation:1.5.1")
implementation("androidx.compose.ui:ui:1.5.1")
implementation("androidx.compose.material:material:1.5.1")
implementation("androidx.compose.material3:material3:1.1.2")
implementation("androidx.activity:activity-compose:1.10.1")
implementation("androidx.compose.foundation:foundation:1.8.0")
implementation("androidx.compose.animation:animation:1.8.0")
implementation("androidx.compose.ui:ui:1.8.0")
implementation("androidx.compose.material:material:1.8.0")
implementation("androidx.compose.material3:material3:1.3.2")
implementation("com.google.android.material:compose-theme-adapter-3:1.1.1")
implementation("androidx.compose.material:material-icons-extended:1.5.1")
implementation("androidx.compose.ui:ui-tooling-preview:1.5.1")
debugImplementation("androidx.compose.ui:ui-tooling:1.5.1")
implementation("androidx.compose.material:material-icons-extended:1.7.8")
implementation("androidx.compose.ui:ui-tooling-preview:1.8.0")
debugImplementation("androidx.compose.ui:ui-tooling:1.8.0")
implementation("com.google.accompanist:accompanist-webview:0.30.1")
implementation("androidx.glance:glance-appwidget:1.0.0")
implementation("androidx.glance:glance-appwidget:1.1.1")
// Modified dependencies
implementation("com.github.jays2kings:subsampling-scale-image-view:756849e") {
@@ -147,32 +148,32 @@ dependencies {
implementation("com.github.tachiyomiorg:image-decoder:7879b45")
// Android X libraries
implementation("androidx.appcompat:appcompat:1.6.1")
implementation("androidx.appcompat:appcompat:1.7.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("com.google.android.material:material:1.10.0")
implementation("androidx.webkit:webkit:1.8.0")
implementation("androidx.recyclerview:recyclerview:1.3.1")
implementation("com.google.android.material:material:1.12.0")
implementation("androidx.webkit:webkit:1.13.0")
implementation("androidx.recyclerview:recyclerview:1.4.0")
implementation("androidx.preference:preference:1.2.1")
implementation("androidx.annotation:annotation:1.7.0")
implementation("androidx.browser:browser:1.6.0")
implementation("androidx.annotation:annotation:1.9.1")
implementation("androidx.browser:browser:1.8.0")
implementation("androidx.biometric:biometric:1.1.0")
implementation("androidx.palette:palette:1.0.0")
implementation("androidx.activity:activity-ktx:1.8.0")
implementation("androidx.core:core-ktx:1.12.0")
implementation("androidx.activity:activity-ktx:1.10.1")
implementation("androidx.core:core-ktx:1.16.0")
implementation("com.google.android.flexbox:flexbox:3.0.0")
implementation("androidx.window:window:1.1.0")
implementation("androidx.window:window:1.3.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.1.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.4")
implementation("androidx.constraintlayout:constraintlayout:2.2.1")
implementation("androidx.multidex:multidex:2.0.1")
implementation(platform("com.google.firebase:firebase-bom:31.2.3"))
implementation(platform("com.google.firebase:firebase-bom:33.13.0"))
implementation("com.google.firebase:firebase-analytics-ktx")
implementation("com.google.firebase:firebase-crashlytics-ktx")
val lifecycleVersion = "2.6.2"
val lifecycleVersion = "2.8.7"
implementation("androidx.lifecycle:lifecycle-viewmodel-ktx:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-livedata-ktx:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-common:$lifecycleVersion")
@@ -188,23 +189,23 @@ dependencies {
implementation("com.fredporciuncula:flow-preferences:1.6.0")
// Network client
val okhttpVersion = "5.0.0-alpha.11"
val okhttpVersion = "5.0.0-alpha.14"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-brotli:$okhttpVersion")
implementation("com.squareup.okio:okio:3.4.0")
implementation("com.squareup.okio:okio:3.11.0")
// Chucker
val chuckerVersion = "3.5.2"
debugImplementation("com.github.ChuckerTeam.Chucker:library:$chuckerVersion")
releaseImplementation("com.github.ChuckerTeam.Chucker:library-no-op:$chuckerVersion")
add("betaImplementation", "com.github.ChuckerTeam.Chucker:library-no-op:$chuckerVersion")
// val chuckerVersion = "3.5.2"
// debugImplementation("com.github.ChuckerTeam.Chucker:library:$chuckerVersion")
// releaseImplementation("com.github.ChuckerTeam.Chucker:library-no-op:$chuckerVersion")
// add("betaImplementation", "com.github.ChuckerTeam.Chucker:library-no-op:$chuckerVersion")
implementation(kotlin("reflect", version = AndroidVersions.kotlin))
// JSON
val kotlinSerialization = "1.6.0"
val kotlinSerialization = "1.8.1"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:${kotlinSerialization}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:${kotlinSerialization}")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json-okio:${kotlinSerialization}")
@@ -218,17 +219,17 @@ dependencies {
implementation("com.github.junrar:junrar:7.5.5")
// HTML parser
implementation("org.jsoup:jsoup:1.16.1")
implementation("org.jsoup:jsoup:1.19.1")
// Job scheduling
implementation("androidx.work:work-runtime-ktx:2.8.1")
implementation("com.google.guava:guava:31.1-android")
implementation("androidx.work:work-runtime-ktx:2.10.1")
implementation("com.google.guava:guava:32.0.1-jre")
implementation("com.google.android.gms:play-services-gcm:17.0.0")
// Database
implementation("androidx.sqlite:sqlite-ktx:2.3.1")
implementation("com.github.requery:sqlite-android:3.39.2")
implementation("androidx.sqlite:sqlite-ktx:2.5.0")
implementation("com.github.requery:sqlite-android:3.45.0")
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
@@ -238,7 +239,7 @@ dependencies {
implementation("info.android15.nucleus:nucleus-support-v7:$nucleusVersion")
// Dependency injection
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
implementation("com.github.mihonapp:injekt:91edab2317")
// Image library
val coilVersion = "2.4.0"
@@ -253,8 +254,7 @@ dependencies {
implementation("com.github.gpanther:java-nat-sort:natural-comparator-1.1")
// UI
implementation("com.dmitrymalkovich.android:material-design-dimens:1.4")
implementation("br.com.simplepass:loading-button-android:2.2.0")
implementation("io.writeopia:loading-button:3.0.0")
val fastAdapterVersion = "5.6.0"
implementation("com.mikepenz:fastadapter:$fastAdapterVersion")
implementation("com.mikepenz:fastadapter-extensions-binding:$fastAdapterVersion")
@@ -266,7 +266,7 @@ dependencies {
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
implementation("com.github.florent37:viewtooltip:1.2.2")
implementation("com.github.florent37:ViewTooltip:f79a895")
implementation("com.getkeepsafe.taptargetview:taptargetview:1.13.3")
// Conductor
@@ -281,17 +281,17 @@ dependencies {
implementation(kotlin("stdlib", org.jetbrains.kotlin.config.KotlinCompilerVersion.VERSION))
val coroutines = "1.7.3"
val coroutines = "1.10.2"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines")
// Text distance
implementation("info.debatty:java-string-similarity:2.0.0")
implementation("com.google.android.gms:play-services-oss-licenses:17.0.1")
implementation("com.google.android.gms:play-services-oss-licenses:17.1.0")
// TLS 1.3 support for Android < 10
implementation("org.conscrypt:conscrypt-android:2.5.2")
implementation("org.conscrypt:conscrypt-android:2.5.3")
// Android Chart
implementation("com.github.PhilJay:MPAndroidChart:v3.1.0")
@@ -300,7 +300,7 @@ dependencies {
tasks {
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<org.jetbrains.kotlin.gradle.tasks.KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf(
compilerOptions.freeCompilerArgs.addAll(
"-Xcontext-receivers",
"-opt-in=kotlin.Experimental",
"-opt-in=kotlin.RequiresOptIn",
@@ -322,18 +322,18 @@ tasks {
"-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
)
if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
project.buildDir.absolutePath + "/compose_metrics",
)
kotlinOptions.freeCompilerArgs += listOf(
"-P",
"plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
project.buildDir.absolutePath + "/compose_metrics",
)
}
// if (project.findProperty("tachiyomi.enableComposeCompilerMetrics") == "true") {
// compilerOptions.freeCompilerArgs.addAll(
// "-P",
// "plugin:androidx.compose.compiler.plugins.kotlin:reportsDestination=" +
// project.layout.buildDirectory + "/compose_metrics",
// )
// compilerOptions.freeCompilerArgs.addAll(
// "-P",
// "plugin:androidx.compose.compiler.plugins.kotlin:metricsDestination=" +
// project.layout.buildDirectory + "/compose_metrics",
// )
// }
}
// Duplicating Hebrew string assets due to some locale code issues on different devices

View File

@@ -42,8 +42,9 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.injectLazy
import java.security.Security
open class App : Application(), DefaultLifecycleObserver {
open class App :
Application(),
DefaultLifecycleObserver {
val preferences: PreferencesHelper by injectLazy()
private val disableIncognitoReceiver = DisableIncognitoReceiver()
@@ -52,7 +53,6 @@ open class App : Application(), DefaultLifecycleObserver {
override fun onCreate() {
super<Application>.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
// TLS 1.3 support for Android 10 and below
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
@@ -72,7 +72,8 @@ open class App : Application(), DefaultLifecycleObserver {
ProcessLifecycleOwner.get().lifecycle.addObserver(this)
MangaCoverMetadata.load()
preferences.nightMode()
preferences
.nightMode()
.asImmediateFlow { AppCompatDelegate.setDefaultNightMode(it) }
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
@@ -81,27 +82,31 @@ open class App : Application(), DefaultLifecycleObserver {
}
// Show notification to disable Incognito Mode when it's enabled
preferences.incognitoMode().asFlow()
preferences
.incognitoMode()
.asFlow()
.onEach { enabled ->
val notificationManager = NotificationManagerCompat.from(this)
if (enabled) {
disableIncognitoReceiver.register()
val nContext = localeContext
val notification = nContext.notification(Notifications.CHANNEL_INCOGNITO_MODE) {
val incogText = nContext.getString(R.string.incognito_mode)
setContentTitle(incogText)
setContentText(nContext.getString(R.string.turn_off_, incogText))
setSmallIcon(R.drawable.ic_incognito_24dp)
setOngoing(true)
val notification =
nContext.notification(Notifications.CHANNEL_INCOGNITO_MODE) {
val incogText = nContext.getString(R.string.incognito_mode)
setContentTitle(incogText)
setContentText(nContext.getString(R.string.turn_off_, incogText))
setSmallIcon(R.drawable.ic_incognito_24dp)
setOngoing(true)
val pendingIntent = PendingIntent.getBroadcast(
this@App,
0,
Intent(ACTION_DISABLE_INCOGNITO_MODE),
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
setContentIntent(pendingIntent)
}
val pendingIntent =
PendingIntent.getBroadcast(
this@App,
0,
Intent(ACTION_DISABLE_INCOGNITO_MODE),
PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE,
)
setContentIntent(pendingIntent)
}
if (ActivityCompat.checkSelfPermission(
this,
Manifest.permission.POST_NOTIFICATIONS,
@@ -114,8 +119,7 @@ open class App : Application(), DefaultLifecycleObserver {
disableIncognitoReceiver.unregister()
notificationManager.cancel(Notifications.ID_INCOGNITO_MODE)
}
}
.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
}.launchIn(ProcessLifecycleOwner.get().lifecycleScope)
}
override fun onPause(owner: LifecycleOwner) {
@@ -143,7 +147,10 @@ open class App : Application(), DefaultLifecycleObserver {
private inner class DisableIncognitoReceiver : BroadcastReceiver() {
private var registered = false
override fun onReceive(context: Context, intent: Intent) {
override fun onReceive(
context: Context,
intent: Intent,
) {
preferences.incognitoMode().set(false)
}

View File

@@ -26,8 +26,9 @@ import uy.kohesive.injekt.api.addSingleton
import uy.kohesive.injekt.api.addSingletonFactory
import uy.kohesive.injekt.api.get
class AppModule(val app: Application) : InjektModule {
class AppModule(
val app: Application,
) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingleton(app)

View File

@@ -29,7 +29,6 @@ import java.io.File
import kotlin.math.max
object Migrations {
/**
* Performs a migration when the application is updated.
*
@@ -163,14 +162,15 @@ object Migrations {
}
if (oldVersion < 77) {
// Migrate Rotation and Viewer values to default values for viewer_flags
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> OrientationType.FREE.flagValue
2 -> OrientationType.PORTRAIT.flagValue
3 -> OrientationType.LANDSCAPE.flagValue
4 -> OrientationType.LOCKED_PORTRAIT.flagValue
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue
else -> OrientationType.FREE.flagValue
}
val newOrientation =
when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> OrientationType.FREE.flagValue
2 -> OrientationType.PORTRAIT.flagValue
3 -> OrientationType.LANDSCAPE.flagValue
4 -> OrientationType.LOCKED_PORTRAIT.flagValue
5 -> OrientationType.LOCKED_LANDSCAPE.flagValue
else -> OrientationType.FREE.flagValue
}
// Reading mode flag and prefValue is the same value
val newReadingMode = prefs.getInt("pref_default_viewer_key", 1)
@@ -228,7 +228,8 @@ object Migrations {
LibraryUpdateJob.setupTask(context)
}
if (oldVersion < 108) {
preferenceStore.getAll()
preferenceStore
.getAll()
.filter { it.key.startsWith("pref_mangasync_") || it.key.startsWith("track_token_") }
.forEach { (key, value) ->
if (value is String) {

View File

@@ -4,7 +4,6 @@ import android.content.Context
import androidx.glance.appwidget.GlanceAppWidgetManager
class TachiyomiWidgetManager {
suspend fun Context.init() {
val manager = GlanceAppWidgetManager(this)
if (manager.getGlanceIds(UpdatesGridGlanceWidget::class.java).isNotEmpty()) {

View File

@@ -50,7 +50,11 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
private var data: List<Pair<Long, Bitmap?>>? = null
override val sizeMode = SizeMode.Exact
override suspend fun provideGlance(context: Context, id: GlanceId) {
override suspend fun provideGlance(
context: Context,
id: GlanceId,
) {
provideContent {
// If app lock enabled, don't do anything
if (preferences.useBiometrics().get()) {
@@ -73,10 +77,11 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
val ids = manager.getGlanceIds(this@UpdatesGridGlanceWidget::class.java)
if (ids.isEmpty()) return@launchIO
val (rowCount, columnCount) = ids
.flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value }
.calculateRowAndColumnCount()
val (rowCount, columnCount) =
ids
.flatMap { manager.getAppWidgetSizes(it) }
.maxBy { it.height.value * it.width.value }
.calculateRowAndColumnCount()
val processList = list ?: RecentsPresenter.getRecentManga(customAmount = min(50, rowCount * columnCount))
data = prepareList(processList, rowCount * columnCount)
@@ -84,7 +89,10 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
}
}
private fun prepareList(processList: List<Pair<Manga, Long>>, take: Int): List<Pair<Long, Bitmap?>> {
private fun prepareList(
processList: List<Pair<Manga, Long>>,
take: Int,
): List<Pair<Long, Bitmap?>> {
// Resize to cover size
val widthPx = CoverWidth.value.toInt().dpToPx
val heightPx = CoverHeight.value.toInt().dpToPx
@@ -95,35 +103,44 @@ class UpdatesGridGlanceWidget : GlanceAppWidget() {
.take(take)
.map { it.first }
.map { updatesView ->
val request = ImageRequest.Builder(app)
.data(updatesView)
.memoryCachePolicy(CachePolicy.DISABLED)
.precision(Precision.EXACT)
.size(widthPx, heightPx)
.scale(Scale.FILL)
.let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
it.transformations(RoundedCornersTransformation(roundPx))
} else {
it // Handled by system
}
}
.build()
Pair(updatesView.id!!, app.imageLoader.executeBlocking(request).drawable?.toBitmap())
val request =
ImageRequest
.Builder(app)
.data(updatesView)
.memoryCachePolicy(CachePolicy.DISABLED)
.precision(Precision.EXACT)
.size(widthPx, heightPx)
.scale(Scale.FILL)
.let {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
it.transformations(RoundedCornersTransformation(roundPx))
} else {
it // Handled by system
}
}.build()
Pair(
updatesView.id!!,
app.imageLoader
.executeBlocking(request)
.drawable
?.toBitmap(),
)
}
}
companion object {
val DateLimit: Calendar
get() = Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
get() =
Calendar.getInstance().apply {
time = Date()
add(Calendar.MONTH, -3)
}
}
}
val ContainerModifier = GlanceModifier
.fillMaxSize()
.background(ImageProvider(R.drawable.appwidget_background))
.appWidgetBackground()
.appWidgetBackgroundRadius()
val ContainerModifier =
GlanceModifier
.fillMaxSize()
.background(ImageProvider(R.drawable.appwidget_background))
.appWidgetBackground()
.appWidgetBackgroundRadius()

View File

@@ -22,23 +22,26 @@ import eu.kanade.tachiyomi.ui.main.MainActivity
@Composable
fun LockedWidget() {
val intent = Intent(LocalContext.current, Class.forName(MainActivity.MAIN_ACTIVITY)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
val intent =
Intent(LocalContext.current, Class.forName(MainActivity.MAIN_ACTIVITY)).apply {
addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
}
Box(
modifier = GlanceModifier
.clickable(actionStartActivity(intent))
.then(ContainerModifier)
.padding(8.dp),
modifier =
GlanceModifier
.clickable(actionStartActivity(intent))
.then(ContainerModifier)
.padding(8.dp),
contentAlignment = Alignment.Center,
) {
Text(
text = stringResource(R.string.appwidget_unavailable_locked),
style = TextStyle(
color = ColorProvider(R.color.appwidget_on_secondary_container),
fontSize = 12.sp,
textAlign = TextAlign.Center,
),
style =
TextStyle(
color = ColorProvider(R.color.appwidget_on_secondary_container),
fontSize = 12.sp,
textAlign = TextAlign.Center,
),
)
}
}

View File

@@ -22,17 +22,19 @@ fun UpdatesMangaCover(
cover: Bitmap?,
) {
Box(
modifier = modifier
.size(width = CoverWidth, height = CoverHeight)
.appWidgetInnerRadius(),
modifier =
modifier
.size(width = CoverWidth, height = CoverHeight)
.appWidgetInnerRadius(),
) {
if (cover != null) {
Image(
provider = ImageProvider(cover),
contentDescription = null,
modifier = GlanceModifier
.fillMaxSize()
.appWidgetInnerRadius(),
modifier =
GlanceModifier
.fillMaxSize()
.appWidgetInnerRadius(),
contentScale = ContentScale.Crop,
)
} else {

View File

@@ -27,8 +27,10 @@ import eu.kanade.tachiyomi.ui.main.SearchActivity
@Composable
fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) {
val (rowCount, columnCount) = LocalSize.current.calculateRowAndColumnCount()
val mainIntent = Intent(LocalContext.current, MainActivity::class.java).setAction(MainActivity.SHORTCUT_RECENTS)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
val mainIntent =
Intent(LocalContext.current, MainActivity::class.java)
.setAction(MainActivity.SHORTCUT_RECENTS)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
Column(
modifier = ContainerModifier.clickable(actionStartActivity(mainIntent)),
verticalAlignment = Alignment.CenterVertically,
@@ -40,27 +42,32 @@ fun UpdatesWidget(data: List<Pair<Long, Bitmap?>>?) {
Text(text = stringResource(R.string.no_recent_read_updated_manga))
} else {
(0 until rowCount).forEach { i ->
val coverRow = (0 until columnCount).mapNotNull { j ->
data.getOrNull(j + (i * columnCount))
}
val coverRow =
(0 until columnCount).mapNotNull { j ->
data.getOrNull(j + (i * columnCount))
}
if (coverRow.isNotEmpty()) {
Row(
modifier = GlanceModifier
.padding(vertical = 4.dp)
.fillMaxWidth(),
modifier =
GlanceModifier
.padding(vertical = 4.dp)
.fillMaxWidth(),
horizontalAlignment = Alignment.CenterHorizontally,
verticalAlignment = Alignment.CenterVertically,
) {
coverRow.forEach { (mangaId, cover) ->
Box(
modifier = GlanceModifier
.padding(horizontal = 3.dp),
modifier =
GlanceModifier
.padding(horizontal = 3.dp),
contentAlignment = Alignment.Center,
) {
val intent = SearchActivity.openMangaIntent(LocalContext.current, mangaId, true)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
// https://issuetracker.google.com/issues/238793260
.addCategory(mangaId.toString())
val intent =
SearchActivity
.openMangaIntent(LocalContext.current, mangaId, true)
.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP)
// https://issuetracker.google.com/issues/238793260
.addCategory(mangaId.toString())
UpdatesMangaCover(
modifier = GlanceModifier.clickable(actionStartActivity(intent)),
cover = cover,

View File

@@ -8,18 +8,14 @@ import androidx.glance.LocalContext
import androidx.glance.appwidget.cornerRadius
import eu.kanade.tachiyomi.R
fun GlanceModifier.appWidgetBackgroundRadius(): GlanceModifier {
return this.cornerRadius(R.dimen.appwidget_background_radius)
}
fun GlanceModifier.appWidgetBackgroundRadius(): GlanceModifier = this.cornerRadius(R.dimen.appwidget_background_radius)
fun GlanceModifier.appWidgetInnerRadius(): GlanceModifier {
return this.cornerRadius(R.dimen.appwidget_inner_radius)
}
fun GlanceModifier.appWidgetInnerRadius(): GlanceModifier = this.cornerRadius(R.dimen.appwidget_inner_radius)
@Composable
fun stringResource(@StringRes id: Int): String {
return LocalContext.current.getString(id)
}
fun stringResource(
@StringRes id: Int,
): String = LocalContext.current.getString(id)
/**
* Calculates row-column count.

View File

@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.backup
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
object BackupConst {
private const val NAME = "BackupRestorer"
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"

View File

@@ -56,8 +56,9 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.FileOutputStream
class BackupCreator(val context: Context) {
class BackupCreator(
val context: Context,
) {
private val preferenceStore: PreferenceStore = Injekt.get()
val parser = ProtoBuf
private val db: DatabaseHelper = Injekt.get()
@@ -71,26 +72,32 @@ class BackupCreator(val context: Context) {
* @param uri path of Uri
* @param isAutoBackup backup called from scheduled backup job
*/
fun createBackup(uri: Uri, flags: Int, isAutoBackup: Boolean): String {
fun createBackup(
uri: Uri,
flags: Int,
isAutoBackup: Boolean,
): String {
// Create root object
var backup: Backup? = null
db.inTransaction {
val databaseManga = db.getFavoriteMangas().executeAsBlocking() +
if (flags and BACKUP_READ_MANGA_MASK == BACKUP_READ_MANGA) {
db.getReadNotInLibraryMangas().executeAsBlocking()
} else {
emptyList()
}
val databaseManga =
db.getFavoriteMangas().executeAsBlocking() +
if (flags and BACKUP_READ_MANGA_MASK == BACKUP_READ_MANGA) {
db.getReadNotInLibraryMangas().executeAsBlocking()
} else {
emptyList()
}
backup = Backup(
backupMangas(databaseManga, flags),
backupCategories(),
emptyList(),
backupExtensionInfo(databaseManga),
backupAppPreferences(flags),
backupSourcePreferences(flags),
)
backup =
Backup(
backupMangas(databaseManga, flags),
backupCategories(),
emptyList(),
backupExtensionInfo(databaseManga),
backupAppPreferences(flags),
backupSourcePreferences(flags),
)
}
var file: UniFile? = null
@@ -103,7 +110,8 @@ class BackupCreator(val context: Context) {
// Delete older backups
val numberOfBackups = preferences.numberOfBackups().get()
dir.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
dir
.listFiles { _, filename -> Backup.filenameRegex.matches(filename) }
.orEmpty()
.sortedByDescending { it.name }
.drop(numberOfBackups - 1)
@@ -114,7 +122,7 @@ class BackupCreator(val context: Context) {
} else {
UniFile.fromUri(context, uri)
}
)
)
?: throw Exception("Couldn't create backup file")
if (!file.isFile) {
@@ -126,10 +134,15 @@ class BackupCreator(val context: Context) {
throw IllegalStateException(context.getString(R.string.empty_backup_error))
}
file.openOutputStream().also {
// Force overwrite old file
(it as? FileOutputStream)?.channel?.truncate(0)
}.sink().gzip().buffer().use { it.write(byteArray) }
file
.openOutputStream()
.also {
// Force overwrite old file
(it as? FileOutputStream)?.channel?.truncate(0)
}.sink()
.gzip()
.buffer()
.use { it.write(byteArray) }
val fileUri = file.uri
// Make sure it's a valid backup file
@@ -143,32 +156,33 @@ class BackupCreator(val context: Context) {
}
}
private fun backupMangas(mangas: List<Manga>, flags: Int): List<BackupManga> {
return mangas.map {
private fun backupMangas(
mangas: List<Manga>,
flags: Int,
): List<BackupManga> =
mangas.map {
backupManga(it, flags)
}
}
private fun backupExtensionInfo(mangas: List<Manga>): List<BackupSource> {
return mangas
private fun backupExtensionInfo(mangas: List<Manga>): List<BackupSource> =
mangas
.asSequence()
.map { it.source }
.distinct()
.map { sourceManager.getOrStub(it) }
.map { BackupSource.copyFrom(it) }
.toList()
}
/**
* Backup the categories of library
*
* @return list of [BackupCategory] to be backed up
*/
private fun backupCategories(): List<BackupCategory> {
return db.getCategories()
private fun backupCategories(): List<BackupCategory> =
db
.getCategories()
.executeAsBlocking()
.map { BackupCategory.copyFrom(it) }
}
/**
* Convert a manga to Json
@@ -177,9 +191,22 @@ class BackupCreator(val context: Context) {
* @param options options for the backup
* @return [BackupManga] containing manga in a serializable form
*/
private fun backupManga(manga: Manga, options: Int): BackupManga {
private fun backupManga(
manga: Manga,
options: Int,
): BackupManga {
// Entry for this manga
val mangaObject = BackupManga.copyFrom(manga, if (options and BACKUP_CUSTOM_INFO_MASK == BACKUP_CUSTOM_INFO) customMangaManager else null)
val mangaObject =
BackupManga.copyFrom(
manga,
if (options and BACKUP_CUSTOM_INFO_MASK ==
BACKUP_CUSTOM_INFO
) {
customMangaManager
} else {
null
},
)
// Check if user wants chapter information in backup
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
@@ -211,10 +238,11 @@ class BackupCreator(val context: Context) {
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
val historyForManga = db.getHistoryByMangaId(manga.id!!).executeAsBlocking()
if (historyForManga.isNotEmpty()) {
val history = historyForManga.mapNotNull { history ->
val url = db.getChapter(history.chapter_id).executeAsBlocking()?.url
url?.let { BackupHistory(url, history.last_read, history.time_read) }
}
val history =
historyForManga.mapNotNull { history ->
val url = db.getChapter(history.chapter_id).executeAsBlocking()?.url
url?.let { BackupHistory(url, history.last_read, history.time_read) }
}
if (history.isNotEmpty()) {
mangaObject.history = history
}
@@ -231,7 +259,8 @@ class BackupCreator(val context: Context) {
private fun backupSourcePreferences(flags: Int): List<BackupSourcePreferences> {
if (flags and BACKUP_SOURCE_PREFS_MASK != BACKUP_SOURCE_PREFS) return emptyList()
return sourceManager.getOnlineSources()
return sourceManager
.getOnlineSources()
.filterIsInstance<ConfigurableSource>()
.map {
BackupSourcePreferences(
@@ -243,7 +272,8 @@ class BackupCreator(val context: Context) {
@Suppress("UNCHECKED_CAST")
private fun Map<String, *>.toBackupPreferences(): List<BackupPreference> {
return this.filterKeys { !Preference.isPrivate(it) }
return this
.filterKeys { !Preference.isPrivate(it) }
.mapNotNull { (key, value) ->
// j2k fork differences
if (key == "library_sorting_mode" && value is Int) {
@@ -257,9 +287,10 @@ class BackupCreator(val context: Context) {
is Float -> BackupPreference(key, FloatPreferenceValue(value))
is String -> BackupPreference(key, StringPreferenceValue(value))
is Boolean -> BackupPreference(key, BooleanPreferenceValue(value))
is Set<*> -> (value as? Set<String>)?.let {
BackupPreference(key, StringSetPreferenceValue(it))
}
is Set<*> ->
(value as? Set<String>)?.let {
BackupPreference(key, StringSetPreferenceValue(it))
}
else -> null
}
}

View File

@@ -22,14 +22,16 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
class BackupCreatorJob(
private val context: Context,
workerParams: WorkerParameters,
) : Worker(context, workerParams) {
override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>()
val notifier = BackupNotifier(context.localeContext)
val uri = inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }
?: preferences.backupsDirectory().get().toUri()
val uri =
inputData.getString(LOCATION_URI_KEY)?.let { Uri.parse(it) }
?: preferences.backupsDirectory().get().toUri()
val flags = inputData.getInt(BACKUP_FLAGS_KEY, BackupConst.BACKUP_ALL)
val isAutoBackup = inputData.getBoolean(IS_AUTO_BACKUP_KEY, true)
@@ -53,20 +55,23 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
return list.find { it.state == WorkInfo.State.RUNNING } != null
}
fun setupTask(context: Context, prefInterval: Int? = null) {
fun setupTask(
context: Context,
prefInterval: Int? = null,
) {
val preferences = Injekt.get<PreferencesHelper>()
val interval = prefInterval ?: preferences.backupInterval().get()
val workManager = WorkManager.getInstance(context)
if (interval > 0) {
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
interval.toLong(),
TimeUnit.HOURS,
10,
TimeUnit.MINUTES,
)
.addTag(TAG_AUTO)
.setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true))
.build()
val request =
PeriodicWorkRequestBuilder<BackupCreatorJob>(
interval.toLong(),
TimeUnit.HOURS,
10,
TimeUnit.MINUTES,
).addTag(TAG_AUTO)
.setInputData(workDataOf(IS_AUTO_BACKUP_KEY to true))
.build()
workManager.enqueueUniquePeriodicWork(TAG_AUTO, ExistingPeriodicWorkPolicy.UPDATE, request)
} else {
@@ -74,16 +79,22 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
}
}
fun startNow(context: Context, uri: Uri, flags: Int) {
val inputData = workDataOf(
IS_AUTO_BACKUP_KEY to false,
LOCATION_URI_KEY to uri.toString(),
BACKUP_FLAGS_KEY to flags,
)
val request = OneTimeWorkRequestBuilder<BackupCreatorJob>()
.addTag(TAG_MANUAL)
.setInputData(inputData)
.build()
fun startNow(
context: Context,
uri: Uri,
flags: Int,
) {
val inputData =
workDataOf(
IS_AUTO_BACKUP_KEY to false,
LOCATION_URI_KEY to uri.toString(),
BACKUP_FLAGS_KEY to flags,
)
val request =
OneTimeWorkRequestBuilder<BackupCreatorJob>()
.addTag(TAG_MANUAL)
.setInputData(inputData)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(TAG_MANUAL, ExistingWorkPolicy.KEEP, request)
}
}

View File

@@ -13,42 +13,51 @@ class BackupFileValidator(
private val sourceManager: SourceManager = Injekt.get(),
private val trackManager: TrackManager = Injekt.get(),
) {
/**
* Checks for critical backup file data.
*
* @throws Exception if manga cannot be found.
* @return List of missing sources or missing trackers.
*/
fun validate(context: Context, uri: Uri): Results {
val backup = try {
BackupUtil.decodeBackup(context, uri)
} catch (e: Exception) {
throw IllegalStateException(e)
}
fun validate(
context: Context,
uri: Uri,
): Results {
val backup =
try {
BackupUtil.decodeBackup(context, uri)
} catch (e: Exception) {
throw IllegalStateException(e)
}
if (backup.backupManga.isEmpty()) {
throw IllegalStateException(context.getString(R.string.backup_has_no_manga))
}
val sources = backup.backupSources.map { it.sourceId to it.name }.toMap()
val missingSources = sources
.filter { sourceManager.get(it.key) == null }
.map { sourceManager.getOrStub(it.key).name }
.sorted()
val missingSources =
sources
.filter { sourceManager.get(it.key) == null }
.map { sourceManager.getOrStub(it.key).name }
.sorted()
val trackers = backup.backupManga
.flatMap { it.tracking }
.map { it.syncId }
.distinct()
val missingTrackers = trackers
.mapNotNull { trackManager.getService(it) }
.filter { !it.isLogged }
.map { context.getString(it.nameRes()) }
.sorted()
val trackers =
backup.backupManga
.flatMap { it.tracking }
.map { it.syncId }
.distinct()
val missingTrackers =
trackers
.mapNotNull { trackManager.getService(it) }
.filter { !it.isLogged }
.map { context.getString(it.nameRes()) }
.sorted()
return Results(missingSources, missingTrackers)
}
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
data class Results(
val missingSources: List<String>,
val missingTrackers: List<String>,
)
}

View File

@@ -15,34 +15,38 @@ import uy.kohesive.injekt.injectLazy
import java.io.File
import java.util.concurrent.TimeUnit
class BackupNotifier(private val context: Context) {
class BackupNotifier(
private val context: Context,
) {
private val preferences: PreferencesHelper by injectLazy()
private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setSmallIcon(R.drawable.ic_tachij2k_notification)
setAutoCancel(false)
setOngoing(true)
}
private val progressNotificationBuilder =
context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setSmallIcon(R.drawable.ic_tachij2k_notification)
setAutoCancel(false)
setOngoing(true)
}
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setSmallIcon(R.drawable.ic_tachij2k_notification)
setAutoCancel(false)
}
private val completeNotificationBuilder =
context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setSmallIcon(R.drawable.ic_tachij2k_notification)
setAutoCancel(false)
}
private fun NotificationCompat.Builder.show(id: Int) {
context.notificationManager.notify(id, build())
}
fun showBackupProgress() {
val builder = with(progressNotificationBuilder) {
setContentTitle(context.getString(R.string.creating_backup))
val builder =
with(progressNotificationBuilder) {
setContentTitle(context.getString(R.string.creating_backup))
setProgress(0, 0, true)
setOnlyAlertOnce(true)
}
setProgress(0, 0, true)
setOnlyAlertOnce(true)
}
builder.show(Notifications.ID_BACKUP_PROGRESS)
}
@@ -78,27 +82,32 @@ class BackupNotifier(private val context: Context) {
}
}
fun showRestoreProgress(content: String = "", progress: Int = 0, maxAmount: Int = 100): NotificationCompat.Builder {
val builder = with(progressNotificationBuilder) {
setContentTitle(context.getString(R.string.restoring_backup))
fun showRestoreProgress(
content: String = "",
progress: Int = 0,
maxAmount: Int = 100,
): NotificationCompat.Builder {
val builder =
with(progressNotificationBuilder) {
setContentTitle(context.getString(R.string.restoring_backup))
if (!preferences.hideNotificationContent()) {
setContentText(content)
if (!preferences.hideNotificationContent()) {
setContentText(content)
}
setProgress(maxAmount, progress, progress == -1)
setOnlyAlertOnce(true)
// Clear old actions if they exist
clearActions()
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.stop),
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS),
)
}
setProgress(maxAmount, progress, progress == -1)
setOnlyAlertOnce(true)
// Clear old actions if they exist
clearActions()
addAction(
R.drawable.ic_close_24dp,
context.getString(R.string.stop),
NotificationReceiver.cancelRestorePendingBroadcast(context, Notifications.ID_RESTORE_PROGRESS),
)
}
if (progress != -1) {
builder.show(Notifications.ID_RESTORE_PROGRESS)
}
@@ -117,16 +126,23 @@ class BackupNotifier(private val context: Context) {
}
}
fun showRestoreComplete(time: Long, errorCount: Int, path: String?, file: String?) {
fun showRestoreComplete(
time: Long,
errorCount: Int,
path: String?,
file: String?,
) {
context.notificationManager.cancel(Notifications.ID_RESTORE_PROGRESS)
val timeString = context.getString(
R.string.restore_duration,
TimeUnit.MILLISECONDS.toMinutes(time),
TimeUnit.MILLISECONDS.toSeconds(time) - TimeUnit.MINUTES.toSeconds(
val timeString =
context.getString(
R.string.restore_duration,
TimeUnit.MILLISECONDS.toMinutes(time),
),
)
TimeUnit.MILLISECONDS.toSeconds(time) -
TimeUnit.MINUTES.toSeconds(
TimeUnit.MILLISECONDS.toMinutes(time),
),
)
with(completeNotificationBuilder) {
setContentTitle(context.getString(R.string.restore_completed))

View File

@@ -20,8 +20,10 @@ import eu.kanade.tachiyomi.util.system.tryToSetForeground
import eu.kanade.tachiyomi.util.system.withIOContext
import kotlinx.coroutines.CancellationException
class BackupRestoreJob(val context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
class BackupRestoreJob(
val context: Context,
workerParams: WorkerParameters,
) : CoroutineWorker(context, workerParams) {
private val notifier = BackupNotifier(context.localeContext)
private val restorer = BackupRestorer(context, notifier)
@@ -62,13 +64,18 @@ class BackupRestoreJob(val context: Context, workerParams: WorkerParameters) : C
companion object {
private const val TAG = "BackupRestorer"
fun start(context: Context, uri: Uri) {
val request = OneTimeWorkRequestBuilder<BackupRestoreJob>()
.addTag(TAG)
.setInputData(workDataOf(BackupConst.EXTRA_URI to uri.toString()))
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager.getInstance(context)
fun start(
context: Context,
uri: Uri,
) {
val request =
OneTimeWorkRequestBuilder<BackupRestoreJob>()
.addTag(TAG)
.setInputData(workDataOf(BackupConst.EXTRA_URI to uri.toString()))
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.build()
WorkManager
.getInstance(context)
.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
}

View File

@@ -44,8 +44,10 @@ import java.util.Date
import java.util.Locale
import kotlin.math.max
class BackupRestorer(val context: Context, val notifier: BackupNotifier) {
class BackupRestorer(
val context: Context,
val notifier: BackupNotifier,
) {
private val db: DatabaseHelper by injectLazy()
private val customMangaManager: CustomMangaManager by injectLazy()
private val preferenceStore: PreferenceStore = Injekt.get()
@@ -143,7 +145,10 @@ class BackupRestorer(val context: Context, val notifier: BackupNotifier) {
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
}
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
private fun restoreManga(
backupManga: BackupManga,
backupCategories: List<BackupCategory>,
) {
val manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl()
val categories = backupManga.categories
@@ -192,10 +197,11 @@ class BackupRestorer(val context: Context, val notifier: BackupNotifier) {
backupCategories: List<BackupCategory>,
customManga: CustomMangaManager.MangaJson?,
) {
val fetchedManga = manga.also {
it.initialized = it.description != null
it.id = db.insertManga(it).executeAsBlocking().insertedId()
}
val fetchedManga =
manga.also {
it.initialized = it.description != null
it.id = db.insertManga(it).executeAsBlocking().insertedId()
}
fetchedManga.id ?: return
restoreChapters(fetchedManga, chapters)
@@ -215,7 +221,10 @@ class BackupRestorer(val context: Context, val notifier: BackupNotifier) {
restoreExtras(backupManga, categories, history, tracks, backupCategories, customManga)
}
private fun restoreChapters(manga: Manga, chapters: List<Chapter>) {
private fun restoreChapters(
manga: Manga,
chapters: List<Chapter>,
) {
val dbChapters = db.getChapters(manga).executeAsBlocking()
chapters.forEach { chapter ->
@@ -263,19 +272,25 @@ class BackupRestorer(val context: Context, val notifier: BackupNotifier) {
* @param manga the manga whose categories have to be restored.
* @param categories the categories to restore.
*/
private fun restoreCategories(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
private fun restoreCategories(
manga: Manga,
categories: List<Int>,
backupCategories: List<BackupCategory>,
) {
val dbCategories = db.getCategories().executeAsBlocking()
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
categories.forEach { backupCategoryOrder ->
backupCategories.firstOrNull {
it.order == backupCategoryOrder
}?.let { backupCategory ->
dbCategories.firstOrNull { dbCategory ->
dbCategory.name == backupCategory.name
}?.let { dbCategory ->
mangaCategoriesToUpdate += MangaCategory.create(manga, dbCategory)
backupCategories
.firstOrNull {
it.order == backupCategoryOrder
}?.let { backupCategory ->
dbCategories
.firstOrNull { dbCategory ->
dbCategory.name == backupCategory.name
}?.let { dbCategory ->
mangaCategoriesToUpdate += MangaCategory.create(manga, dbCategory)
}
}
}
}
// Update database
@@ -305,10 +320,11 @@ class BackupRestorer(val context: Context, val notifier: BackupNotifier) {
} else {
// If not in database create
db.getChapter(url).executeAsBlocking()?.let {
val historyToAdd = History.create(it).apply {
last_read = lastRead
time_read = readDuration
}
val historyToAdd =
History.create(it).apply {
last_read = lastRead
time_read = readDuration
}
historyToBeUpdated.add(historyToAdd)
}
}
@@ -322,7 +338,10 @@ class BackupRestorer(val context: Context, val notifier: BackupNotifier) {
* @param manga the manga whose sync have to be restored.
* @param tracks the track list to restore.
*/
private fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
private fun restoreTrackForManga(
manga: Manga,
tracks: List<Track>,
) {
// Fix foreign keys with the current manga id
tracks.map { it.manga_id = manga.id!! }
@@ -387,7 +406,8 @@ class BackupRestorer(val context: Context, val notifier: BackupNotifier) {
val prefs = preferenceStore.getAll()
toRestore.forEach { (key, value) ->
// j2k fork differences
if (key == "library_sorting_mode" && value is StringPreferenceValue &&
if (key == "library_sorting_mode" &&
value is StringPreferenceValue &&
prefs[key] is Int?
) {
val intValue = LibrarySort.deserialize(value.value)

View File

@@ -16,7 +16,6 @@ data class Backup(
@ProtoNumber(104) var backupPreferences: List<BackupPreference> = emptyList(),
@ProtoNumber(105) var backupSourcePreferences: List<BackupSourcePreferences> = emptyList(),
) {
companion object {
val filenameRegex = """(${BuildConfig.APPLICATION_ID}|tachiyomi)?_\d+-\d+-\d+_\d+-\d+\.(tachibk|proto\.gz)""".toRegex()

View File

@@ -12,27 +12,24 @@ class BackupCategory(
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var flags: Int = 0,
// J2K Specific values
@ProtoNumber(800) var mangaSort: Char? = null,
) {
fun getCategoryImpl(): CategoryImpl {
return CategoryImpl().apply {
fun getCategoryImpl(): CategoryImpl =
CategoryImpl().apply {
name = this@BackupCategory.name
flags = this@BackupCategory.flags
order = this@BackupCategory.order
mangaSort = this@BackupCategory.mangaSort
}
}
companion object {
fun copyFrom(category: Category): BackupCategory {
return BackupCategory(
fun copyFrom(category: Category): BackupCategory =
BackupCategory(
name = category.name,
order = category.order,
flags = category.flags,
mangaSort = category.mangaSort,
)
}
}
}

View File

@@ -21,12 +21,11 @@ data class BackupChapter(
// chapterNumber is called number is 1.x
@ProtoNumber(9) var chapterNumber: Float = 0F,
@ProtoNumber(10) var sourceOrder: Int = 0,
// J2K specific values
@ProtoNumber(800) var pagesLeft: Int = 0,
) {
fun toChapterImpl(): ChapterImpl {
return ChapterImpl().apply {
fun toChapterImpl(): ChapterImpl =
ChapterImpl().apply {
url = this@BackupChapter.url
name = this@BackupChapter.name
chapter_number = this@BackupChapter.chapterNumber
@@ -39,11 +38,10 @@ data class BackupChapter(
source_order = this@BackupChapter.sourceOrder
pages_left = this@BackupChapter.pagesLeft
}
}
companion object {
fun copyFrom(chapter: Chapter): BackupChapter {
return BackupChapter(
fun copyFrom(chapter: Chapter): BackupChapter =
BackupChapter(
url = chapter.url,
name = chapter.name,
chapterNumber = chapter.chapter_number,
@@ -56,6 +54,5 @@ data class BackupChapter(
sourceOrder = chapter.source_order,
pagesLeft = chapter.pages_left,
)
}
}
}

View File

@@ -40,10 +40,8 @@ data class BackupManga(
@ProtoNumber(103) var viewer_flags: Int? = null,
@ProtoNumber(104) var history: List<BackupHistory> = emptyList(),
@ProtoNumber(105) var updateStrategy: UpdateStrategy = UpdateStrategy.ALWAYS_UPDATE,
// SY specific values
@ProtoNumber(602) var customStatus: Int = 0,
// J2K specific values
@ProtoNumber(800) var customTitle: String? = null,
@ProtoNumber(801) var customArtist: String? = null,
@@ -52,8 +50,8 @@ data class BackupManga(
@ProtoNumber(804) var customDescription: String? = null,
@ProtoNumber(805) var customGenre: List<String>? = null,
) {
fun getMangaImpl(): MangaImpl {
return MangaImpl().apply {
fun getMangaImpl(): MangaImpl =
MangaImpl().apply {
url = this@BackupManga.url
title = this@BackupManga.title
artist = this@BackupManga.artist
@@ -68,18 +66,16 @@ data class BackupManga(
viewer_flags = (
this@BackupManga.viewer_flags
?: this@BackupManga.viewer
).takeIf { it != 0 }
).takeIf { it != 0 }
?: -1
chapter_flags = this@BackupManga.chapterFlags
update_strategy = this@BackupManga.updateStrategy
}
}
fun getChaptersImpl(): List<ChapterImpl> {
return chapters.map {
fun getChaptersImpl(): List<ChapterImpl> =
chapters.map {
it.toChapterImpl()
}
}
fun getCustomMangaInfo(): CustomMangaManager.MangaJson? {
if (customTitle != null ||
@@ -102,15 +98,17 @@ data class BackupManga(
return null
}
fun getTrackingImpl(): List<TrackImpl> {
return tracking.map {
fun getTrackingImpl(): List<TrackImpl> =
tracking.map {
it.getTrackingImpl()
}
}
companion object {
fun copyFrom(manga: Manga, customMangaManager: CustomMangaManager?): BackupManga {
return BackupManga(
fun copyFrom(
manga: Manga,
customMangaManager: CustomMangaManager?,
): BackupManga =
BackupManga(
url = manga.url,
title = manga.originalTitle,
artist = manga.originalArtist,
@@ -136,6 +134,5 @@ data class BackupManga(
backupManga.customStatus = it.status
}
}
}
}
}

View File

@@ -19,19 +19,31 @@ data class BackupSourcePreferences(
sealed class PreferenceValue
@Serializable
data class IntPreferenceValue(val value: Int) : PreferenceValue()
data class IntPreferenceValue(
val value: Int,
) : PreferenceValue()
@Serializable
data class LongPreferenceValue(val value: Long) : PreferenceValue()
data class LongPreferenceValue(
val value: Long,
) : PreferenceValue()
@Serializable
data class FloatPreferenceValue(val value: Float) : PreferenceValue()
data class FloatPreferenceValue(
val value: Float,
) : PreferenceValue()
@Serializable
data class StringPreferenceValue(val value: String) : PreferenceValue()
data class StringPreferenceValue(
val value: String,
) : PreferenceValue()
@Serializable
data class BooleanPreferenceValue(val value: Boolean) : PreferenceValue()
data class BooleanPreferenceValue(
val value: Boolean,
) : PreferenceValue()
@Serializable
data class StringSetPreferenceValue(val value: Set<String>) : PreferenceValue()
data class StringSetPreferenceValue(
val value: Set<String>,
) : PreferenceValue()

View File

@@ -16,11 +16,10 @@ data class BackupSource(
@ProtoNumber(2) var sourceId: Long,
) {
companion object {
fun copyFrom(source: Source): BackupSource {
return BackupSource(
fun copyFrom(source: Source): BackupSource =
BackupSource(
name = source.name,
sourceId = source.id,
)
}
}
}

View File

@@ -29,15 +29,15 @@ data class BackupTracking(
@ProtoNumber(11) var finishedReadingDate: Long = 0,
@ProtoNumber(100) var mediaId: Long = 0,
) {
fun getTrackingImpl(): TrackImpl {
return TrackImpl().apply {
fun getTrackingImpl(): TrackImpl =
TrackImpl().apply {
sync_id = this@BackupTracking.syncId
media_id = if (this@BackupTracking.mediaIdInt != 0) {
this@BackupTracking.mediaIdInt.toLong()
} else {
this@BackupTracking.mediaId
}
media_id =
if (this@BackupTracking.mediaIdInt != 0) {
this@BackupTracking.mediaIdInt.toLong()
} else {
this@BackupTracking.mediaId
}
library_id = this@BackupTracking.libraryId
title = this@BackupTracking.title
last_chapter_read = this@BackupTracking.lastChapterRead
@@ -48,11 +48,10 @@ data class BackupTracking(
finished_reading_date = this@BackupTracking.finishedReadingDate
tracking_url = this@BackupTracking.trackingUrl
}
}
companion object {
fun copyFrom(track: Track): BackupTracking {
return BackupTracking(
fun copyFrom(track: Track): BackupTracking =
BackupTracking(
syncId = track.sync_id,
mediaId = track.media_id,
// forced not null so its compatible with 1.x backup system
@@ -66,6 +65,5 @@ data class BackupTracking(
finishedReadingDate = track.finished_reading_date,
trackingUrl = track.tracking_url,
)
}
}
}

View File

@@ -35,8 +35,9 @@ import kotlin.math.roundToLong
* @param context the application context.
* @constructor creates an instance of the chapter cache.
*/
class ChapterCache(private val context: Context) {
class ChapterCache(
private val context: Context,
) {
companion object {
/** Name of cache directory. */
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
@@ -80,26 +81,26 @@ class ChapterCache(private val context: Context) {
get() = Formatter.formatFileSize(context, realSize)
init {
preferences.preloadSize().asFlow()
preferences
.preloadSize()
.asFlow()
.drop(1)
.onEach {
// Save old cache for destruction later
val oldCache = diskCache
diskCache = setupDiskCache(it)
oldCache.close()
}
.launchIn(scope)
}.launchIn(scope)
}
private fun setupDiskCache(cacheSize: Int): DiskLruCache {
return DiskLruCache.open(
private fun setupDiskCache(cacheSize: Int): DiskLruCache =
DiskLruCache.open(
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT,
// 4 pages = 115MB, 6 = ~150MB, 10 = ~200MB, 20 = ~300MB
(PARAMETER_CACHE_SIZE * cacheSize.toFloat().pow(0.6f)).roundToLong(),
)
}
/**
* Remove file from cache.
@@ -145,7 +146,10 @@ class ChapterCache(private val context: Context) {
* @param chapter the chapter.
* @param pages list of pages.
*/
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
fun putPageListToCache(
chapter: Chapter,
pages: List<Page>,
) {
// Convert list of pages to json string.
val cachedValue = json.encodeToString(pages)
@@ -179,13 +183,12 @@ class ChapterCache(private val context: Context) {
* @param imageUrl url of image.
* @return true if in cache otherwise false.
*/
fun isImageInCache(imageUrl: String): Boolean {
return try {
fun isImageInCache(imageUrl: String): Boolean =
try {
diskCache.get(DiskUtil.hashKeyForDisk(imageUrl)).use { it != null }
} catch (e: IOException) {
false
}
}
/**
* Get image file from url.
@@ -207,7 +210,10 @@ class ChapterCache(private val context: Context) {
* @throws IOException image error.
*/
@Throws(IOException::class)
fun putImageToCache(imageUrl: String, response: Response) {
fun putImageToCache(
imageUrl: String,
response: Response,
) {
// Initialize editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null
@@ -227,7 +233,5 @@ class ChapterCache(private val context: Context) {
}
}
private fun getKey(chapter: Chapter): String {
return "${chapter.manga_id}${chapter.url}"
}
private fun getKey(chapter: Chapter): String = "${chapter.manga_id}${chapter.url}"
}

View File

@@ -31,8 +31,9 @@ import java.util.concurrent.TimeUnit
* @param context the application context.
* @constructor creates an instance of the cover cache.
*/
class CoverCache(val context: Context) {
class CoverCache(
val context: Context,
) {
companion object {
private const val COVERS_DIR = "covers"
private const val CUSTOM_COVERS_DIR = "covers/custom"
@@ -58,21 +59,18 @@ class CoverCache(val context: Context) {
*/
private val renewInterval = TimeUnit.HOURS.toMillis(1)
fun getChapterCacheSize(): String {
return Formatter.formatFileSize(context, DiskUtil.getDirectorySize(cacheDir))
}
fun getChapterCacheSize(): String = Formatter.formatFileSize(context, DiskUtil.getDirectorySize(cacheDir))
fun getOnlineCoverCacheSize(): String {
return Formatter.formatFileSize(context, DiskUtil.getDirectorySize(onlineCoverDirectory))
}
fun getOnlineCoverCacheSize(): String = Formatter.formatFileSize(context, DiskUtil.getDirectorySize(onlineCoverDirectory))
suspend fun deleteOldCovers() {
val db = Injekt.get<DatabaseHelper>()
var deletedSize = 0L
val urls = db.getFavoriteMangas().executeOnIO().mapNotNull {
it.thumbnail_url?.let { url -> return@mapNotNull DiskUtil.hashKeyForDisk(url) }
null
}
val urls =
db.getFavoriteMangas().executeOnIO().mapNotNull {
it.thumbnail_url?.let { url -> return@mapNotNull DiskUtil.hashKeyForDisk(url) }
null
}
val files = cacheDir.listFiles()?.iterator() ?: return
while (files.hasNext()) {
val file = files.next()
@@ -130,8 +128,9 @@ class CoverCache(val context: Context) {
return@withIOContext
}
var deletedSize = 0L
val files = directory.listFiles()?.sortedBy { it.lastModified() }?.iterator()
?: return@withIOContext
val files =
directory.listFiles()?.sortedBy { it.lastModified() }?.iterator()
?: return@withIOContext
while (files.hasNext()) {
val file = files.next()
deletedSize += file.length()
@@ -154,9 +153,7 @@ class CoverCache(val context: Context) {
* @param manga the manga.
* @return cover image.
*/
fun getCustomCoverFile(manga: Manga): File {
return File(customCoverCacheDir, DiskUtil.hashKeyForDisk(manga.id.toString()))
}
fun getCustomCoverFile(manga: Manga): File = File(customCoverCacheDir, DiskUtil.hashKeyForDisk(manga.id.toString()))
/**
* Saves the given stream as the manga's custom cover to cache.
@@ -166,7 +163,10 @@ class CoverCache(val context: Context) {
* @throws IOException if there's any error.
*/
@Throws(IOException::class)
fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) {
fun setCustomCoverToCache(
manga: Manga,
inputStream: InputStream,
) {
getCustomCoverFile(manga).outputStream().use {
inputStream.copyTo(it)
context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key()))
@@ -180,9 +180,10 @@ class CoverCache(val context: Context) {
* @return whether the cover was deleted.
*/
fun deleteCustomCover(manga: Manga): Boolean {
val result = getCustomCoverFile(manga).let {
it.exists() && it.delete()
}
val result =
getCustomCoverFile(manga).let {
it.exists() && it.delete()
}
context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key()))
return result
}
@@ -231,8 +232,7 @@ class CoverCache(val context: Context) {
}
}
private fun getCacheDir(dir: String): File {
return context.getExternalFilesDir(dir)
private fun getCacheDir(dir: String): File =
context.getExternalFilesDir(dir)
?: File(context.filesDir, dir).also { it.mkdirs() }
}
}

View File

@@ -3,30 +3,34 @@ package eu.kanade.tachiyomi.data.database
import eu.kanade.tachiyomi.source.model.UpdateStrategy
import java.util.Date
val dateAdapter = object : ColumnAdapter<Date, Long> {
override fun decode(databaseValue: Long): Date = Date(databaseValue)
override fun encode(value: Date): Long = value.time
}
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()) {
emptyList()
} else {
databaseValue.split(listOfStringsSeparator)
}
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsSeparator)
}
val listOfStringsAdapter =
object : ColumnAdapter<List<String>, String> {
override fun decode(databaseValue: String) =
if (databaseValue.isEmpty()) {
emptyList()
} else {
databaseValue.split(listOfStringsSeparator)
}
val updateStrategyAdapter = object : ColumnAdapter<UpdateStrategy, Int> {
private val enumValues by lazy { UpdateStrategy.entries }
override fun encode(value: List<String>) = value.joinToString(separator = listOfStringsSeparator)
}
override fun decode(databaseValue: Int): UpdateStrategy =
enumValues.getOrElse(databaseValue) { UpdateStrategy.ALWAYS_UPDATE }
val updateStrategyAdapter =
object : ColumnAdapter<UpdateStrategy, Int> {
private val enumValues by lazy { UpdateStrategy.entries }
override fun encode(value: UpdateStrategy): Int = value.ordinal
}
override fun decode(databaseValue: Int): UpdateStrategy = enumValues.getOrElse(databaseValue) { UpdateStrategy.ALWAYS_UPDATE }
override fun encode(value: UpdateStrategy): Int = value.ordinal
}
interface ColumnAdapter<T : Any, S> {
/**

View File

@@ -29,30 +29,34 @@ import io.requery.android.database.sqlite.RequerySQLiteOpenHelperFactory
/**
* This class provides operations to manage the database through its interfaces.
*/
open class DatabaseHelper(context: Context) :
MangaQueries,
open class DatabaseHelper(
context: Context,
) : MangaQueries,
ChapterQueries,
TrackQueries,
CategoryQueries,
MangaCategoryQueries,
HistoryQueries,
SearchMetadataQueries {
private val configuration =
SupportSQLiteOpenHelper.Configuration
.builder(context)
.name(DbOpenCallback.DATABASE_NAME)
.callback(DbOpenCallback())
.build()
private val configuration = SupportSQLiteOpenHelper.Configuration.builder(context)
.name(DbOpenCallback.DATABASE_NAME)
.callback(DbOpenCallback())
.build()
override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration))
.addTypeMapping(Manga::class.java, MangaTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
.addTypeMapping(Track::class.java, TrackTypeMapping())
.addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping())
.build()
override val db =
DefaultStorIOSQLite
.builder()
.sqliteOpenHelper(RequerySQLiteOpenHelperFactory().create(configuration))
.addTypeMapping(Manga::class.java, MangaTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
.addTypeMapping(Track::class.java, TrackTypeMapping())
.addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
.addTypeMapping(SearchMetadata::class.java, SearchMetadataTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping())
.build()
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)

View File

@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.data.database.tables.TrackTable
class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
companion object {
/**
* Name of the database file.
@@ -30,29 +29,37 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
setPragma(db, "synchronous = NORMAL")
}
private fun setPragma(db: SupportSQLiteDatabase, pragma: String) {
private fun setPragma(
db: SupportSQLiteDatabase,
pragma: String,
) {
val cursor = db.query("PRAGMA $pragma")
cursor.moveToFirst()
cursor.close()
}
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)
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)
}
// 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) {
override fun onUpgrade(
db: SupportSQLiteDatabase,
oldVersion: Int,
newVersion: Int,
) {
if (oldVersion < 2) {
db.execSQL(ChapterTable.sourceOrderUpdateQuery)

View File

@@ -3,6 +3,5 @@ package eu.kanade.tachiyomi.data.database
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
interface DbProvider {
val db: DefaultStorIOSQLite
}

View File

@@ -18,66 +18,72 @@ import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
CategoryPutResolver(),
CategoryGetResolver(),
CategoryDeleteResolver(),
)
class CategoryTypeMapping :
SQLiteTypeMapping<Category>(
CategoryPutResolver(),
CategoryGetResolver(),
CategoryDeleteResolver(),
)
class CategoryPutResolver : DefaultPutResolver<Category>() {
override fun mapToInsertQuery(obj: Category) =
InsertQuery
.builder()
.table(TABLE)
.build()
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: Category) =
UpdateQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
put(COL_ID, obj.id)
put(COL_NAME, obj.name)
put(COL_ORDER, obj.order)
put(COL_FLAGS, obj.flags)
if (obj.mangaSort != null) {
put(COL_MANGA_ORDER, obj.mangaSort.toString())
} else {
val orderString = obj.mangaOrder.joinToString("/")
put(COL_MANGA_ORDER, orderString)
override fun mapToContentValues(obj: Category) =
ContentValues(4).apply {
put(COL_ID, obj.id)
put(COL_NAME, obj.name)
put(COL_ORDER, obj.order)
put(COL_FLAGS, obj.flags)
if (obj.mangaSort != null) {
put(COL_MANGA_ORDER, obj.mangaSort.toString())
} else {
val orderString = obj.mangaOrder.joinToString("/")
put(COL_MANGA_ORDER, orderString)
}
}
}
}
class CategoryGetResolver : DefaultGetResolver<Category>() {
override fun mapFromCursor(cursor: Cursor): Category =
CategoryImpl().apply {
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
val orderString = cursor.getString(cursor.getColumnIndex(COL_MANGA_ORDER))
when {
orderString.isNullOrBlank() -> {
mangaSort = 'a'
mangaOrder = emptyList()
val orderString = cursor.getString(cursor.getColumnIndex(COL_MANGA_ORDER))
when {
orderString.isNullOrBlank() -> {
mangaSort = 'a'
mangaOrder = emptyList()
}
orderString.firstOrNull()?.isLetter() == true -> {
mangaSort = orderString.first()
mangaOrder = emptyList()
}
else -> mangaOrder = orderString.split("/").mapNotNull { it.toLongOrNull() }
}
orderString.firstOrNull()?.isLetter() == true -> {
mangaSort = orderString.first()
mangaOrder = emptyList()
}
else -> mangaOrder = orderString.split("/").mapNotNull { it.toLongOrNull() }
}
}
}
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToDeleteQuery(obj: Category) =
DeleteQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@@ -26,65 +26,71 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
ChapterPutResolver(),
ChapterGetResolver(),
ChapterDeleteResolver(),
)
class ChapterTypeMapping :
SQLiteTypeMapping<Chapter>(
ChapterPutResolver(),
ChapterGetResolver(),
ChapterDeleteResolver(),
)
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
override fun mapToInsertQuery(obj: Chapter) =
InsertQuery
.builder()
.table(TABLE)
.build()
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: Chapter) =
UpdateQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Chapter) = ContentValues(11).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_URL, obj.url)
put(COL_NAME, obj.name)
put(COL_READ, obj.read)
put(COL_SCANLATOR, obj.scanlator)
put(COL_BOOKMARK, obj.bookmark)
put(COL_DATE_FETCH, obj.date_fetch)
put(COL_DATE_UPLOAD, obj.date_upload)
put(COL_LAST_PAGE_READ, obj.last_page_read)
put(COL_PAGES_LEFT, obj.pages_left)
put(COL_CHAPTER_NUMBER, obj.chapter_number)
put(COL_SOURCE_ORDER, obj.source_order)
}
override fun mapToContentValues(obj: Chapter) =
ContentValues(11).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_URL, obj.url)
put(COL_NAME, obj.name)
put(COL_READ, obj.read)
put(COL_SCANLATOR, obj.scanlator)
put(COL_BOOKMARK, obj.bookmark)
put(COL_DATE_FETCH, obj.date_fetch)
put(COL_DATE_UPLOAD, obj.date_upload)
put(COL_LAST_PAGE_READ, obj.last_page_read)
put(COL_PAGES_LEFT, obj.pages_left)
put(COL_CHAPTER_NUMBER, obj.chapter_number)
put(COL_SOURCE_ORDER, obj.source_order)
}
}
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
pages_left = cursor.getInt(cursor.getColumnIndex(COL_PAGES_LEFT))
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
}
override fun mapFromCursor(cursor: Cursor): Chapter =
ChapterImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
pages_left = cursor.getInt(cursor.getColumnIndex(COL_PAGES_LEFT))
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
}
}
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToDeleteQuery(obj: Chapter) =
DeleteQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@@ -17,47 +17,53 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_LAST_READ
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
class HistoryTypeMapping : SQLiteTypeMapping<History>(
HistoryPutResolver(),
HistoryGetResolver(),
HistoryDeleteResolver(),
)
class HistoryTypeMapping :
SQLiteTypeMapping<History>(
HistoryPutResolver(),
HistoryGetResolver(),
HistoryDeleteResolver(),
)
open class HistoryPutResolver : DefaultPutResolver<History>() {
override fun mapToInsertQuery(obj: History) =
InsertQuery
.builder()
.table(TABLE)
.build()
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: History) =
UpdateQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
put(COL_ID, obj.id)
put(COL_CHAPTER_ID, obj.chapter_id)
put(COL_LAST_READ, obj.last_read)
put(COL_TIME_READ, obj.time_read)
}
override fun mapToContentValues(obj: History) =
ContentValues(4).apply {
put(COL_ID, obj.id)
put(COL_CHAPTER_ID, obj.chapter_id)
put(COL_LAST_READ, obj.last_read)
put(COL_TIME_READ, obj.time_read)
}
}
class HistoryGetResolver : DefaultGetResolver<History>() {
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
}
override fun mapFromCursor(cursor: Cursor): History =
HistoryImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
}
}
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToDeleteQuery(obj: History) =
DeleteQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@@ -15,45 +15,51 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
MangaCategoryPutResolver(),
MangaCategoryGetResolver(),
MangaCategoryDeleteResolver(),
)
class MangaCategoryTypeMapping :
SQLiteTypeMapping<MangaCategory>(
MangaCategoryPutResolver(),
MangaCategoryGetResolver(),
MangaCategoryDeleteResolver(),
)
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
override fun mapToInsertQuery(obj: MangaCategory) =
InsertQuery
.builder()
.table(TABLE)
.build()
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: MangaCategory) =
UpdateQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_CATEGORY_ID, obj.category_id)
}
override fun mapToContentValues(obj: MangaCategory) =
ContentValues(3).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_CATEGORY_ID, obj.category_id)
}
}
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
}
override fun mapFromCursor(cursor: Cursor): MangaCategory =
MangaCategory().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
}
}
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToDeleteQuery(obj: MangaCategory) =
DeleteQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@@ -33,49 +33,57 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
import eu.kanade.tachiyomi.data.database.updateStrategyAdapter
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
MangaPutResolver(),
MangaGetResolver(),
MangaDeleteResolver(),
)
class MangaTypeMapping :
SQLiteTypeMapping<Manga>(
MangaPutResolver(),
MangaGetResolver(),
MangaDeleteResolver(),
)
class MangaPutResolver : DefaultPutResolver<Manga>() {
override fun mapToInsertQuery(obj: Manga) =
InsertQuery
.builder()
.table(TABLE)
.build()
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: Manga) =
UpdateQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
put(COL_ID, obj.id)
put(COL_SOURCE, obj.source)
put(COL_URL, obj.url)
put(COL_ARTIST, obj.originalArtist)
put(COL_AUTHOR, obj.originalAuthor)
put(COL_DESCRIPTION, obj.originalDescription)
put(COL_GENRE, obj.originalGenre)
put(COL_TITLE, obj.originalTitle)
put(COL_STATUS, obj.originalStatus)
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
put(COL_FAVORITE, obj.favorite)
put(COL_LAST_UPDATE, obj.last_update)
put(COL_INITIALIZED, obj.initialized)
put(COL_VIEWER, obj.viewer_flags)
put(COL_HIDE_TITLE, obj.hide_title)
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
put(COL_DATE_ADDED, obj.date_added)
put(COL_FILTERED_SCANLATORS, obj.filtered_scanlators)
put(COL_UPDATE_STRATEGY, obj.update_strategy.let(updateStrategyAdapter::encode))
}
override fun mapToContentValues(obj: Manga) =
ContentValues(15).apply {
put(COL_ID, obj.id)
put(COL_SOURCE, obj.source)
put(COL_URL, obj.url)
put(COL_ARTIST, obj.originalArtist)
put(COL_AUTHOR, obj.originalAuthor)
put(COL_DESCRIPTION, obj.originalDescription)
put(COL_GENRE, obj.originalGenre)
put(COL_TITLE, obj.originalTitle)
put(COL_STATUS, obj.originalStatus)
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
put(COL_FAVORITE, obj.favorite)
put(COL_LAST_UPDATE, obj.last_update)
put(COL_INITIALIZED, obj.initialized)
put(COL_VIEWER, obj.viewer_flags)
put(COL_HIDE_TITLE, obj.hide_title)
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
put(COL_DATE_ADDED, obj.date_added)
put(COL_FILTERED_SCANLATORS, obj.filtered_scanlators)
put(COL_UPDATE_STRATEGY, obj.update_strategy.let(updateStrategyAdapter::encode))
}
}
interface BaseMangaGetResolver {
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
fun mapBaseFromCursor(
manga: Manga,
cursor: Cursor,
) = manga.apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
@@ -94,24 +102,25 @@ interface BaseMangaGetResolver {
hide_title = cursor.getInt(cursor.getColumnIndex(COL_HIDE_TITLE)) == 1
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
filtered_scanlators = cursor.getString(cursor.getColumnIndex(COL_FILTERED_SCANLATORS))
update_strategy = cursor.getInt(cursor.getColumnIndex(COL_UPDATE_STRATEGY)).let(
updateStrategyAdapter::decode,
)
update_strategy =
cursor.getInt(cursor.getColumnIndex(COL_UPDATE_STRATEGY)).let(
updateStrategyAdapter::decode,
)
}
}
open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver {
override fun mapFromCursor(cursor: Cursor): Manga {
return mapBaseFromCursor(MangaImpl(), cursor)
}
open class MangaGetResolver :
DefaultGetResolver<Manga>(),
BaseMangaGetResolver {
override fun mapFromCursor(cursor: Cursor): Manga = mapBaseFromCursor(MangaImpl(), cursor)
}
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToDeleteQuery(obj: Manga) =
DeleteQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@@ -17,49 +17,55 @@ import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.COL_UPLOADER
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable.TABLE
class SearchMetadataTypeMapping : SQLiteTypeMapping<SearchMetadata>(
SearchMetadataPutResolver(),
SearchMetadataGetResolver(),
SearchMetadataDeleteResolver(),
)
class SearchMetadataTypeMapping :
SQLiteTypeMapping<SearchMetadata>(
SearchMetadataPutResolver(),
SearchMetadataGetResolver(),
SearchMetadataDeleteResolver(),
)
class SearchMetadataPutResolver : DefaultPutResolver<SearchMetadata>() {
override fun mapToInsertQuery(obj: SearchMetadata) =
InsertQuery
.builder()
.table(TABLE)
.build()
override fun mapToInsertQuery(obj: SearchMetadata) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: SearchMetadata) =
UpdateQuery
.builder()
.table(TABLE)
.where("$COL_MANGA_ID = ?")
.whereArgs(obj.mangaId)
.build()
override fun mapToUpdateQuery(obj: SearchMetadata) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_MANGA_ID = ?")
.whereArgs(obj.mangaId)
.build()
override fun mapToContentValues(obj: SearchMetadata) = ContentValues(5).apply {
put(COL_MANGA_ID, obj.mangaId)
put(COL_UPLOADER, obj.uploader)
put(COL_EXTRA, obj.extra)
put(COL_INDEXED_EXTRA, obj.indexedExtra)
put(COL_EXTRA_VERSION, obj.extraVersion)
}
override fun mapToContentValues(obj: SearchMetadata) =
ContentValues(5).apply {
put(COL_MANGA_ID, obj.mangaId)
put(COL_UPLOADER, obj.uploader)
put(COL_EXTRA, obj.extra)
put(COL_INDEXED_EXTRA, obj.indexedExtra)
put(COL_EXTRA_VERSION, obj.extraVersion)
}
}
class SearchMetadataGetResolver : DefaultGetResolver<SearchMetadata>() {
override fun mapFromCursor(cursor: Cursor): SearchMetadata = SearchMetadata(
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
uploader = cursor.getString(cursor.getColumnIndex(COL_UPLOADER)),
extra = cursor.getString(cursor.getColumnIndex(COL_EXTRA)),
indexedExtra = cursor.getString(cursor.getColumnIndex(COL_INDEXED_EXTRA)),
extraVersion = cursor.getInt(cursor.getColumnIndex(COL_EXTRA_VERSION)),
)
override fun mapFromCursor(cursor: Cursor): SearchMetadata =
SearchMetadata(
mangaId = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID)),
uploader = cursor.getString(cursor.getColumnIndex(COL_UPLOADER)),
extra = cursor.getString(cursor.getColumnIndex(COL_EXTRA)),
indexedExtra = cursor.getString(cursor.getColumnIndex(COL_INDEXED_EXTRA)),
extraVersion = cursor.getInt(cursor.getColumnIndex(COL_EXTRA_VERSION)),
)
}
class SearchMetadataDeleteResolver : DefaultDeleteResolver<SearchMetadata>() {
override fun mapToDeleteQuery(obj: SearchMetadata) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_MANGA_ID = ?")
.whereArgs(obj.mangaId)
.build()
override fun mapToDeleteQuery(obj: SearchMetadata) =
DeleteQuery
.builder()
.table(TABLE)
.where("$COL_MANGA_ID = ?")
.whereArgs(obj.mangaId)
.build()
}

View File

@@ -26,65 +26,71 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
class TrackTypeMapping : SQLiteTypeMapping<Track>(
TrackPutResolver(),
TrackGetResolver(),
TrackDeleteResolver(),
)
class TrackTypeMapping :
SQLiteTypeMapping<Track>(
TrackPutResolver(),
TrackGetResolver(),
TrackDeleteResolver(),
)
class TrackPutResolver : DefaultPutResolver<Track>() {
override fun mapToInsertQuery(obj: Track) =
InsertQuery
.builder()
.table(TABLE)
.build()
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: Track) =
UpdateQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Track) = contentValuesOf(
COL_ID to obj.id,
COL_MANGA_ID to obj.manga_id,
COL_SYNC_ID to obj.sync_id,
COL_MEDIA_ID to obj.media_id,
COL_LIBRARY_ID to obj.library_id,
COL_TITLE to obj.title,
COL_LAST_CHAPTER_READ to obj.last_chapter_read,
COL_TOTAL_CHAPTERS to obj.total_chapters,
COL_STATUS to obj.status,
COL_TRACKING_URL to obj.tracking_url,
COL_SCORE to obj.score,
COL_START_DATE to obj.started_reading_date,
COL_FINISH_DATE to obj.finished_reading_date,
)
override fun mapToContentValues(obj: Track) =
contentValuesOf(
COL_ID to obj.id,
COL_MANGA_ID to obj.manga_id,
COL_SYNC_ID to obj.sync_id,
COL_MEDIA_ID to obj.media_id,
COL_LIBRARY_ID to obj.library_id,
COL_TITLE to obj.title,
COL_LAST_CHAPTER_READ to obj.last_chapter_read,
COL_TOTAL_CHAPTERS to obj.total_chapters,
COL_STATUS to obj.status,
COL_TRACKING_URL to obj.tracking_url,
COL_SCORE to obj.score,
COL_START_DATE to obj.started_reading_date,
COL_FINISH_DATE to obj.finished_reading_date,
)
}
class TrackGetResolver : DefaultGetResolver<Track>() {
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID))
media_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID))
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
last_chapter_read = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_CHAPTER_READ))
total_chapters = cursor.getInt(cursor.getColumnIndexOrThrow(COL_TOTAL_CHAPTERS))
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_SCORE))
tracking_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_TRACKING_URL))
started_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_START_DATE))
finished_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_FINISH_DATE))
}
override fun mapFromCursor(cursor: Cursor): Track =
TrackImpl().apply {
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID))
media_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID))
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
last_chapter_read = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_CHAPTER_READ))
total_chapters = cursor.getInt(cursor.getColumnIndexOrThrow(COL_TOTAL_CHAPTERS))
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_SCORE))
tracking_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_TRACKING_URL))
started_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_START_DATE))
finished_reading_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_FINISH_DATE))
}
}
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToDeleteQuery(obj: Track) =
DeleteQuery
.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.ui.library.LibrarySort
import java.io.Serializable
interface Category : Serializable {
var id: Int?
var name: String
@@ -30,22 +29,22 @@ interface Category : Serializable {
var langId: String?
fun isAscending(): Boolean {
return ((mangaSort?.minus('a') ?: 0) % 2) != 1
}
fun isAscending(): Boolean = ((mangaSort?.minus('a') ?: 0) % 2) != 1
fun sortingMode(nullAsDND: Boolean = false): LibrarySort? = LibrarySort.valueOf(mangaSort)
?: if (nullAsDND && !isDynamic) LibrarySort.DragAndDrop else null
fun sortingMode(nullAsDND: Boolean = false): LibrarySort? =
LibrarySort.valueOf(mangaSort)
?: if (nullAsDND && !isDynamic) LibrarySort.DragAndDrop else null
val isDragAndDrop
get() = (
mangaSort == null ||
mangaSort == LibrarySort.DragAndDrop.categoryValue
) && !isDynamic
get() =
(
mangaSort == null ||
mangaSort == LibrarySort.DragAndDrop.categoryValue
) &&
!isDynamic
@StringRes
fun sortRes(): Int =
(LibrarySort.valueOf(mangaSort) ?: LibrarySort.DragAndDrop).stringRes(isDynamic)
fun sortRes(): Int = (LibrarySort.valueOf(mangaSort) ?: LibrarySort.DragAndDrop).stringRes(isDynamic)
fun changeSortTo(sort: Int) {
mangaSort = (LibrarySort.valueOf(sort) ?: LibrarySort.Title).categoryValue
@@ -54,16 +53,21 @@ interface Category : Serializable {
companion object {
var lastCategoriesAddedTo = emptySet<Int>()
fun create(name: String): Category = CategoryImpl().apply {
this.name = name
}
fun create(name: String): Category =
CategoryImpl().apply {
this.name = name
}
fun createDefault(context: Context): Category =
create(context.getString(R.string.default_value)).apply {
id = 0
}
fun createCustom(name: String, libSort: Int, ascending: Boolean): Category =
fun createCustom(
name: String,
libSort: Int,
ascending: Boolean,
): Category =
create(name).apply {
val librarySort = LibrarySort.valueOf(libSort) ?: LibrarySort.DragAndDrop
changeSortTo(librarySort.mainValue)
@@ -73,7 +77,11 @@ interface Category : Serializable {
isDynamic = true
}
fun createAll(context: Context, libSort: Int, ascending: Boolean): Category =
fun createAll(
context: Context,
libSort: Int,
ascending: Boolean,
): Category =
createCustom(context.getString(R.string.all), libSort, ascending).apply {
id = -1
order = -1

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.database.models
class CategoryImpl : Category {
override var id: Int? = null
override lateinit var name: String
@@ -33,7 +32,5 @@ class CategoryImpl : Category {
return name == category.name
}
override fun hashCode(): Int {
return name.hashCode()
}
override fun hashCode(): Int = name.hashCode()
}

View File

@@ -3,8 +3,9 @@ package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.source.model.SChapter
import java.io.Serializable
interface Chapter : SChapter, Serializable {
interface Chapter :
SChapter,
Serializable {
var id: Long?
var manga_id: Long?
@@ -25,18 +26,17 @@ interface Chapter : SChapter, Serializable {
get() = chapter_number >= 0f
companion object {
fun create(): Chapter =
ChapterImpl().apply {
chapter_number = -1f
}
fun create(): Chapter = ChapterImpl().apply {
chapter_number = -1f
}
fun List<Chapter>.copy(): List<Chapter> {
return map {
fun List<Chapter>.copy(): List<Chapter> =
map {
ChapterImpl().apply {
copyFrom(it)
}
}
}
}
fun copyFrom(other: Chapter) {

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.database.models
class ChapterImpl : Chapter {
override var id: Long? = null
override var manga_id: Long? = null
@@ -36,7 +35,5 @@ class ChapterImpl : Chapter {
return url == chapter.url
}
override fun hashCode(): Int {
return url.hashCode()
}
override fun hashCode(): Int = url.hashCode()
}

View File

@@ -6,7 +6,6 @@ import java.io.Serializable
* Object containing the history statistics of a chapter
*/
interface History : Serializable {
/**
* Id of history object.
*/
@@ -28,15 +27,15 @@ interface History : Serializable {
var time_read: Long
companion object {
/**
* History constructor
*
* @param chapter chapter object
* @return history object
*/
fun create(chapter: Chapter): History = HistoryImpl().apply {
this.chapter_id = chapter.id!!
}
fun create(chapter: Chapter): History =
HistoryImpl().apply {
this.chapter_id = chapter.id!!
}
}
}

View File

@@ -4,7 +4,6 @@ package eu.kanade.tachiyomi.data.database.models
* Object containing the history statistics of a chapter
*/
class HistoryImpl : History {
/**
* Id of history object.
*/

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.database.models
class LibraryManga : MangaImpl() {
var unread: Int = 0
var read: Int = 0
@@ -16,13 +15,18 @@ class LibraryManga : MangaImpl() {
get() = read > 0
companion object {
fun createBlank(categoryId: Int): LibraryManga = LibraryManga().apply {
title = ""
id = Long.MIN_VALUE
category = categoryId
}
fun createBlank(categoryId: Int): LibraryManga =
LibraryManga().apply {
title = ""
id = Long.MIN_VALUE
category = categoryId
}
fun createHide(categoryId: Int, title: String, hideCount: Int): LibraryManga =
fun createHide(
categoryId: Int,
title: String,
hideCount: Int,
): LibraryManga =
createBlank(categoryId).apply {
this.title = title
status = -1

View File

@@ -14,7 +14,6 @@ import uy.kohesive.injekt.api.get
import java.util.Locale
interface Manga : SManga {
var id: Long?
var source: Long
@@ -34,9 +33,13 @@ interface Manga : SManga {
var filtered_scanlators: String?
fun isBlank() = id == Long.MIN_VALUE
fun isHidden() = status == -1
fun setChapterOrder(sorting: Int, order: Int) {
fun setChapterOrder(
sorting: Int,
order: Int,
) {
setChapterFlags(sorting, CHAPTER_SORTING_MASK)
setChapterFlags(order, CHAPTER_SORT_MASK)
setChapterFlags(CHAPTER_SORT_LOCAL, CHAPTER_SORT_LOCAL_MASK)
@@ -45,13 +48,20 @@ interface Manga : SManga {
fun setSortToGlobal() = setChapterFlags(CHAPTER_SORT_FILTER_GLOBAL, CHAPTER_SORT_LOCAL_MASK)
fun setFilterToGlobal() = setChapterFlags(CHAPTER_SORT_FILTER_GLOBAL, CHAPTER_FILTER_LOCAL_MASK)
fun setFilterToLocal() = setChapterFlags(CHAPTER_FILTER_LOCAL, CHAPTER_FILTER_LOCAL_MASK)
private fun setChapterFlags(flag: Int, mask: Int) {
private fun setChapterFlags(
flag: Int,
mask: Int,
) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
private fun setViewerFlags(flag: Int, mask: Int) {
private fun setViewerFlags(
flag: Int,
mask: Int,
) {
viewer_flags = viewer_flags and mask.inv() or (flag and mask)
}
@@ -70,11 +80,9 @@ interface Manga : SManga {
fun sortDescending(preferences: PreferencesHelper): Boolean =
if (usesLocalSort) sortDescending else preferences.chaptersDescAsDefault().get()
fun chapterOrder(preferences: PreferencesHelper): Int =
if (usesLocalSort) sorting else preferences.sortChapterOrder().get()
fun chapterOrder(preferences: PreferencesHelper): Int = if (usesLocalSort) sorting else preferences.sortChapterOrder().get()
fun readFilter(preferences: PreferencesHelper): Int =
if (usesLocalFilter) readFilter else preferences.filterChapterByRead().get()
fun readFilter(preferences: PreferencesHelper): Int = if (usesLocalFilter) readFilter else preferences.filterChapterByRead().get()
fun downloadedFilter(preferences: PreferencesHelper): Int =
if (usesLocalFilter) downloadedFilter else preferences.filterChapterByDownloaded().get()
@@ -87,32 +95,39 @@ interface Manga : SManga {
fun showChapterTitle(defaultShow: Boolean): Boolean = chapter_flags and CHAPTER_DISPLAY_MASK == CHAPTER_DISPLAY_NUMBER
fun seriesType(context: Context, sourceManager: SourceManager? = null): String {
return context.getString(
when (seriesType(sourceManager = sourceManager)) {
TYPE_WEBTOON -> R.string.webtoon
TYPE_MANHWA -> R.string.manhwa
TYPE_MANHUA -> R.string.manhua
TYPE_COMIC -> R.string.comic
else -> R.string.manga
},
).lowercase(Locale.getDefault())
}
fun seriesType(
context: Context,
sourceManager: SourceManager? = null,
): String =
context
.getString(
when (seriesType(sourceManager = sourceManager)) {
TYPE_WEBTOON -> R.string.webtoon
TYPE_MANHWA -> R.string.manhwa
TYPE_MANHUA -> R.string.manhua
TYPE_COMIC -> R.string.comic
else -> R.string.manga
},
).lowercase(Locale.getDefault())
fun getGenres(): List<String>? {
return genre?.split(",")
fun getGenres(): List<String>? =
genre
?.split(",")
?.mapNotNull { tag -> tag.trim().takeUnless { it.isBlank() } }
}
fun getOriginalGenres(): List<String>? {
return (originalGenre ?: genre)?.split(",")
fun getOriginalGenres(): List<String>? =
(originalGenre ?: genre)
?.split(",")
?.mapNotNull { tag -> tag.trim().takeUnless { it.isBlank() } }
}
/**
* The type of comic the manga is (ie. manga, manhwa, manhua)
*/
fun seriesType(useOriginalTags: Boolean = false, customTags: String? = null, sourceManager: SourceManager? = null): Int {
fun seriesType(
useOriginalTags: Boolean = false,
customTags: String? = null,
sourceManager: SourceManager? = null,
): Int {
val sourceName by lazy { (sourceManager ?: Injekt.get()).getOrStub(source).name }
val tags = customTags ?: if (useOriginalTags) originalGenre else genre
val currentTags = tags?.split(",")?.map { it.trim().lowercase(Locale.US) } ?: emptyList()
@@ -127,10 +142,11 @@ interface Manga : SManga {
sourceName.contains("webtoon", true) &&
currentTags.none { tag -> isManhuaTag(tag) } &&
currentTags.none { tag -> isManhwaTag(tag) }
)
)
) {
TYPE_WEBTOON
} else if (currentTags.any { tag -> isManhuaTag(tag) } || sourceName.contains(
} else if (currentTags.any { tag -> isManhuaTag(tag) } ||
sourceName.contains(
"manhua",
true,
)
@@ -151,23 +167,28 @@ interface Manga : SManga {
val sourceName = Injekt.get<SourceManager>().getOrStub(source).name
val currentTags = genre?.split(",")?.map { it.trim().lowercase(Locale.US) } ?: emptyList()
return if (currentTags.any
{ tag ->
isManhwaTag(tag) || tag.contains("webtoon")
} || (
{ tag ->
isManhwaTag(tag) || tag.contains("webtoon")
} ||
(
isWebtoonSource(sourceName) &&
currentTags.none { tag -> isManhuaTag(tag) } &&
currentTags.none { tag -> isComicTag(tag) }
)
)
) {
ReadingModeType.WEBTOON.flagValue
} else if (currentTags.any
{ tag ->
tag == "chinese" || tag == "manhua" ||
tag.startsWith("english") || tag == "comic"
} || (
isComicSource(sourceName) && !sourceName.contains("tapas", true) &&
{ tag ->
tag == "chinese" ||
tag == "manhua" ||
tag.startsWith("english") ||
tag == "comic"
} ||
(
isComicSource(sourceName) &&
!sourceName.contains("tapas", true) &&
currentTags.none { tag -> isMangaTag(tag) }
) ||
) ||
(sourceName.contains("manhua", true) && currentTags.none { tag -> isMangaTag(tag) })
) {
ReadingModeType.LEFT_TO_RIGHT.flagValue
@@ -178,17 +199,17 @@ interface Manga : SManga {
fun isSeriesTag(tag: String): Boolean {
val tagLower = tag.lowercase(Locale.ROOT)
return isMangaTag(tagLower) || isManhuaTag(tagLower) ||
isManhwaTag(tagLower) || isComicTag(tagLower) || isWebtoonTag(tagLower)
return isMangaTag(tagLower) ||
isManhuaTag(tagLower) ||
isManhwaTag(tagLower) ||
isComicTag(tagLower) ||
isWebtoonTag(tagLower)
}
fun isMangaTag(tag: String): Boolean {
return tag in listOf("manga", "манга", "jp") || tag.startsWith("japanese")
}
fun isMangaTag(tag: String): Boolean = tag in listOf("manga", "манга", "jp") || tag.startsWith("japanese")
fun isManhuaTag(tag: String): Boolean {
return tag in listOf("manhua", "маньхуа", "cn", "hk", "zh-Hans", "zh-Hant") || tag.startsWith("chinese")
}
fun isManhuaTag(tag: String): Boolean =
tag in listOf("manhua", "маньхуа", "cn", "hk", "zh-Hans", "zh-Hant") || tag.startsWith("chinese")
fun isLongStrip(): Boolean {
val currentTags =
@@ -196,26 +217,19 @@ interface Manga : SManga {
return currentTags.any { it == "long strip" }
}
fun isManhwaTag(tag: String): Boolean {
return tag in listOf("long strip", "manhwa", "манхва", "kr") || tag.startsWith("korean")
}
fun isManhwaTag(tag: String): Boolean = tag in listOf("long strip", "manhwa", "манхва", "kr") || tag.startsWith("korean")
fun isComicTag(tag: String): Boolean {
return tag in listOf("comic", "комикс", "en", "gb") || tag.startsWith("english")
}
fun isComicTag(tag: String): Boolean = tag in listOf("comic", "комикс", "en", "gb") || tag.startsWith("english")
fun isWebtoonTag(tag: String): Boolean {
return tag.startsWith("webtoon")
}
fun isWebtoonTag(tag: String): Boolean = tag.startsWith("webtoon")
fun isWebtoonSource(sourceName: String): Boolean {
return sourceName.contains("webtoon", true) ||
fun isWebtoonSource(sourceName: String): Boolean =
sourceName.contains("webtoon", true) ||
sourceName.contains("manhwa", true) ||
sourceName.contains("toonily", true)
}
fun isComicSource(sourceName: String): Boolean {
return sourceName.contains("gunnerkrigg", true) ||
fun isComicSource(sourceName: String): Boolean =
sourceName.contains("gunnerkrigg", true) ||
sourceName.contains("dilbert", true) ||
sourceName.contains("cyanide", true) ||
sourceName.contains("xkcd", true) ||
@@ -223,25 +237,23 @@ interface Manga : SManga {
sourceName.contains("ComicExtra", true) ||
sourceName.contains("Read Comics Online", true) ||
sourceName.contains("ReadComicOnline", true)
}
fun isOneShotOrCompleted(db: DatabaseHelper): Boolean {
val tags by lazy { genre?.split(",")?.map { it.trim().lowercase(Locale.US) } }
val chapters by lazy { db.getChapters(this).executeAsBlocking() }
val firstChapterName by lazy { chapters.firstOrNull()?.name?.lowercase() ?: "" }
return status == SManga.COMPLETED || tags?.contains("oneshot") == true ||
return status == SManga.COMPLETED ||
tags?.contains("oneshot") == true ||
(
chapters.size == 1 &&
(
Regex("one.?shot").containsMatchIn(firstChapterName) ||
firstChapterName.contains("oneshot")
)
)
)
)
}
fun key(): String {
return "manga-id-$id"
}
fun key(): String = "manga-id-$id"
// Used to display the chapter's title one way or another
var displayMode: Int
@@ -286,7 +298,6 @@ interface Manga : SManga {
}
companion object {
// Generic filter that does not filter anything
const val SHOW_ALL = 0x00000000
@@ -329,14 +340,20 @@ interface Manga : SManga {
private val vibrantCoverColorMap: HashMap<Long, Int?> = hashMapOf()
fun create(source: Long): Manga = MangaImpl().apply {
this.source = source
}
fun create(source: Long): Manga =
MangaImpl().apply {
this.source = source
}
fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
url = pathUrl
this.title = title
this.source = source
}
fun create(
pathUrl: String,
title: String,
source: Long = 0,
): Manga =
MangaImpl().apply {
url = pathUrl
this.title = title
this.source = source
}
}
}

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.database.models
class MangaCategory {
var id: Long? = null
var manga_id: Long = 0
@@ -9,8 +8,10 @@ class MangaCategory {
var category_id: Int = 0
companion object {
fun create(manga: Manga, category: Category): MangaCategory {
fun create(
manga: Manga,
category: Category,
): MangaCategory {
val mc = MangaCategory()
mc.manga_id = manga.id!!
mc.category_id = category.id!!

View File

@@ -1,3 +1,6 @@
package eu.kanade.tachiyomi.data.database.models
class MangaChapter(val manga: Manga, val chapter: Chapter)
class MangaChapter(
val manga: Manga,
val chapter: Chapter,
)

View File

@@ -7,11 +7,18 @@ package eu.kanade.tachiyomi.data.database.models
* @param chapter object containing chater
* @param history object containing history
*/
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History, var extraChapters: List<ChapterHistory> = emptyList()) {
data class MangaChapterHistory(
val manga: Manga,
val chapter: Chapter,
val history: History,
var extraChapters: List<ChapterHistory> = emptyList(),
) {
companion object {
fun createBlank() = MangaChapterHistory(MangaImpl(), ChapterImpl(), HistoryImpl())
}
}
data class ChapterHistory(val chapter: Chapter, var history: History? = null) : Chapter by chapter
data class ChapterHistory(
val chapter: Chapter,
var history: History? = null,
) : Chapter by chapter

View File

@@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.source.model.UpdateStrategy
import uy.kohesive.injekt.injectLazy
open class MangaImpl : Manga {
override var id: Long? = null
override var source: Long = -1
@@ -18,40 +17,52 @@ open class MangaImpl : Manga {
private val customMangaManager: CustomMangaManager by injectLazy()
override var title: String
get() = if (favorite) {
val customTitle = customMangaManager.getManga(this)?.title
if (customTitle.isNullOrBlank()) ogTitle else customTitle
} else {
ogTitle
}
get() =
if (favorite) {
val customTitle = customMangaManager.getManga(this)?.title
if (customTitle.isNullOrBlank()) ogTitle else customTitle
} else {
ogTitle
}
set(value) {
ogTitle = value
}
override var author: String?
get() = if (favorite) customMangaManager.getManga(this)?.author ?: ogAuthor else ogAuthor
set(value) { ogAuthor = value }
set(value) {
ogAuthor = value
}
override var artist: String?
get() = if (favorite) customMangaManager.getManga(this)?.artist ?: ogArtist else ogArtist
set(value) { ogArtist = value }
set(value) {
ogArtist = value
}
override var description: String?
get() = if (favorite) customMangaManager.getManga(this)?.description ?: ogDesc else ogDesc
set(value) { ogDesc = value }
set(value) {
ogDesc = value
}
override var genre: String?
get() = if (favorite) customMangaManager.getManga(this)?.genre ?: ogGenre else ogGenre
set(value) { ogGenre = value }
set(value) {
ogGenre = value
}
override var status: Int
get() = if (favorite) {
customMangaManager.getManga(this)?.status.takeIf { it != -1 }
?: ogStatus
} else {
ogStatus
get() =
if (favorite) {
customMangaManager.getManga(this)?.status.takeIf { it != -1 }
?: ogStatus
} else {
ogStatus
}
set(value) {
ogStatus = value
}
set(value) { ogStatus = value }
override var thumbnail_url: String? = null
@@ -87,8 +98,10 @@ open class MangaImpl : Manga {
private set
override fun copyFrom(other: SManga) {
if (other is MangaImpl && other::ogTitle.isInitialized &&
other.title.isNotBlank() && other.ogTitle != ogTitle
if (other is MangaImpl &&
other::ogTitle.isInitialized &&
other.title.isNotBlank() &&
other.ogTitle != ogTitle
) {
val oldTitle = ogTitle
title = other.ogTitle
@@ -108,11 +121,10 @@ open class MangaImpl : Manga {
return url == manga.url && source == manga.source
}
override fun hashCode(): Int {
return if (::url.isInitialized) {
override fun hashCode(): Int =
if (::url.isInitialized) {
url.hashCode()
} else {
(id ?: 0L).hashCode()
}
}
}

View File

@@ -3,16 +3,12 @@ package eu.kanade.tachiyomi.data.database.models
data class SearchMetadata(
// Manga ID this gallery is linked to
val mangaId: Long,
// Gallery uploader
val uploader: String?,
// Extra data attached to this metadata, in JSON format
val extra: String,
// Indexed extra data attached to this metadata
val indexedExtra: String?,
// The version of this metadata's extra. Used to track changes to the 'extra' field's schema
val extraVersion: Int,
) {

View File

@@ -1,3 +1,6 @@
package eu.kanade.tachiyomi.data.database.models
data class SourceIdMangaCount(val source: Long, val count: Int)
data class SourceIdMangaCount(
val source: Long,
val count: Int,
)

View File

@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
interface Track : Serializable {
var id: Long?
var manga_id: Long
@@ -39,8 +38,9 @@ interface Track : Serializable {
}
companion object {
fun create(serviceId: Int): Track = TrackImpl().apply {
sync_id = serviceId
}
fun create(serviceId: Int): Track =
TrackImpl().apply {
sync_id = serviceId
}
}
}

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.database.models
class TrackImpl : Track {
override var id: Long? = null
override var manga_id: Long = 0

View File

@@ -8,26 +8,29 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
interface CategoryQueries : DbProvider {
fun getCategories() =
db
.get()
.listOfObjects(Category::class.java)
.withQuery(
Query
.builder()
.table(CategoryTable.TABLE)
.orderBy(CategoryTable.COL_ORDER)
.build(),
).prepare()
fun getCategories() = db.get()
.listOfObjects(Category::class.java)
.withQuery(
Query.builder()
.table(CategoryTable.TABLE)
.orderBy(CategoryTable.COL_ORDER)
.build(),
)
.prepare()
fun getCategoriesForManga(manga: Manga) = db.get()
.listOfObjects(Category::class.java)
.withQuery(
RawQuery.builder()
.query(getCategoriesForMangaQuery())
.args(manga.id)
.build(),
)
.prepare()
fun getCategoriesForManga(manga: Manga) =
db
.get()
.listOfObjects(Category::class.java)
.withQuery(
RawQuery
.builder()
.query(getCategoriesForMangaQuery())
.args(manga.id)
.build(),
).prepare()
fun insertCategory(category: Category) = db.put().`object`(category).prepare()

View File

@@ -15,74 +15,90 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.util.lang.sqLite
interface ChapterQueries : DbProvider {
fun getChapters(manga: Manga) = getChapters(manga.id)
fun getChapters(mangaId: Long?) = db.get()
.listOfObjects(Chapter::class.java)
.withQuery(
Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId)
.build(),
)
.prepare()
fun getChapters(mangaId: Long?) =
db
.get()
.listOfObjects(Chapter::class.java)
.withQuery(
Query
.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId)
.build(),
).prepare()
fun getRecentChapters(search: String = "", offset: Int, isResuming: Boolean) = db.get()
fun getRecentChapters(
search: String = "",
offset: Int,
isResuming: Boolean,
) = db
.get()
.listOfObjects(MangaChapter::class.java)
.withQuery(
RawQuery.builder()
RawQuery
.builder()
.query(getRecentsQuery(search.sqLite, offset, isResuming))
.observesTables(ChapterTable.TABLE)
.build(),
)
.withGetResolver(MangaChapterGetResolver.INSTANCE)
).withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare()
fun getChapter(id: Long) = db.get()
fun getChapter(id: Long) =
db
.get()
.`object`(Chapter::class.java)
.withQuery(
Query
.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(id)
.build(),
).prepare()
fun getChapter(url: String) =
db
.get()
.`object`(Chapter::class.java)
.withQuery(
Query
.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?")
.whereArgs(url)
.build(),
).prepare()
fun getChapters(url: String) =
db
.get()
.listOfObjects(Chapter::class.java)
.withQuery(
Query
.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?")
.whereArgs(url)
.build(),
).prepare()
fun getChapter(
url: String,
mangaId: Long,
) = db
.get()
.`object`(Chapter::class.java)
.withQuery(
Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(id)
.build(),
)
.prepare()
fun getChapter(url: String) = db.get()
.`object`(Chapter::class.java)
.withQuery(
Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?")
.whereArgs(url)
.build(),
)
.prepare()
fun getChapters(url: String) = db.get()
.listOfObjects(Chapter::class.java)
.withQuery(
Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?")
.whereArgs(url)
.build(),
)
.prepare()
fun getChapter(url: String, mangaId: Long) = db.get()
.`object`(Chapter::class.java)
.withQuery(
Query.builder()
Query
.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(url, mangaId)
.build(),
)
.prepare()
).prepare()
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
@@ -92,28 +108,38 @@ interface ChapterQueries : DbProvider {
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterBackupPutResolver())
.prepare()
fun updateChaptersBackup(chapters: List<Chapter>) =
db
.put()
.objects(chapters)
.withPutResolver(ChapterBackupPutResolver())
.prepare()
fun updateKnownChaptersBackup(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterKnownBackupPutResolver())
.prepare()
fun updateKnownChaptersBackup(chapters: List<Chapter>) =
db
.put()
.objects(chapters)
.withPutResolver(ChapterKnownBackupPutResolver())
.prepare()
fun updateChapterProgress(chapter: Chapter) = db.put()
.`object`(chapter)
.withPutResolver(ChapterProgressPutResolver())
.prepare()
fun updateChapterProgress(chapter: Chapter) =
db
.put()
.`object`(chapter)
.withPutResolver(ChapterProgressPutResolver())
.prepare()
fun updateChaptersProgress(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterProgressPutResolver())
.prepare()
fun updateChaptersProgress(chapters: List<Chapter>) =
db
.put()
.objects(chapters)
.withPutResolver(ChapterProgressPutResolver())
.prepare()
fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterSourceOrderPutResolver())
.prepare()
fun fixChaptersSourceOrder(chapters: List<Chapter>) =
db
.put()
.objects(chapters)
.withPutResolver(ChapterSourceOrderPutResolver())
.prepare()
}

View File

@@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import eu.kanade.tachiyomi.util.lang.sqLite
interface HistoryQueries : DbProvider {
/**
* Insert history into database
* @param history object containing history information
@@ -41,15 +40,20 @@ interface HistoryQueries : DbProvider {
* @param date recent date range
* @offset offset the db by
*/
fun getHistoryUngrouped(search: String = "", offset: Int, isResuming: Boolean) = db.get()
fun getHistoryUngrouped(
search: String = "",
offset: Int,
isResuming: Boolean,
) = db
.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(
RawQuery.builder()
RawQuery
.builder()
.query(getRecentHistoryUngrouped(search.sqLite, offset, isResuming))
.observesTables(HistoryTable.TABLE)
.build(),
)
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
).withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
/**
@@ -57,15 +61,20 @@ interface HistoryQueries : DbProvider {
* @param date recent date range
* @offset offset the db by
*/
fun getRecentMangaLimit(search: String = "", offset: Int, isResuming: Boolean) = db.get()
fun getRecentMangaLimit(
search: String = "",
offset: Int,
isResuming: Boolean,
) = db
.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(
RawQuery.builder()
RawQuery
.builder()
.query(getRecentMangasLimitQuery(search.sqLite, offset, isResuming))
.observesTables(HistoryTable.TABLE)
.build(),
)
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
).withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
/**
@@ -74,15 +83,19 @@ interface HistoryQueries : DbProvider {
* @param endDate end date of the period
* @offset offset the db by
*/
fun getHistoryPerPeriod(startDate: Long, endDate: Long) = db.get()
fun getHistoryPerPeriod(
startDate: Long,
endDate: Long,
) = db
.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(
RawQuery.builder()
RawQuery
.builder()
.query(getHistoryPerPeriodQuery(startDate, endDate))
.observesTables(HistoryTable.TABLE)
.build(),
)
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
).withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
/**
@@ -96,10 +109,12 @@ interface HistoryQueries : DbProvider {
endless: Boolean,
offset: Int,
isResuming: Boolean,
) = db.get()
) = db
.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(
RawQuery.builder()
RawQuery
.builder()
.query(
getAllRecentsType(
search.sqLite,
@@ -112,81 +127,95 @@ interface HistoryQueries : DbProvider {
// .args(date.time, startDate.time)
.observesTables(HistoryTable.TABLE)
.build(),
)
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
).withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
fun getHistoryByMangaId(mangaId: Long) = db.get()
.listOfObjects(History::class.java)
.withQuery(
RawQuery.builder()
.query(getHistoryByMangaId())
.args(mangaId)
.observesTables(HistoryTable.TABLE)
.build(),
)
.prepare()
fun getTotalReadDuration(): Long {
val cursor = db.lowLevel()
.rawQuery(
RawQuery.builder()
.query("SELECT SUM(${HistoryTable.COL_TIME_READ}) FROM ${HistoryTable.TABLE}")
fun getHistoryByMangaId(mangaId: Long) =
db
.get()
.listOfObjects(History::class.java)
.withQuery(
RawQuery
.builder()
.query(getHistoryByMangaId())
.args(mangaId)
.observesTables(HistoryTable.TABLE)
.build(),
)
).prepare()
fun getTotalReadDuration(): Long {
val cursor =
db
.lowLevel()
.rawQuery(
RawQuery
.builder()
.query("SELECT SUM(${HistoryTable.COL_TIME_READ}) FROM ${HistoryTable.TABLE}")
.observesTables(HistoryTable.TABLE)
.build(),
)
cursor.moveToFirst()
return cursor.getLong(0)
}
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
.`object`(History::class.java)
.withQuery(
RawQuery.builder()
.query(getHistoryByChapterUrl())
.args(chapterUrl)
.observesTables(HistoryTable.TABLE)
.build(),
)
.prepare()
fun getHistoryByChapterUrl(chapterUrl: String) =
db
.get()
.`object`(History::class.java)
.withQuery(
RawQuery
.builder()
.query(getHistoryByChapterUrl())
.args(chapterUrl)
.observesTables(HistoryTable.TABLE)
.build(),
).prepare()
/**
* Updates the history last read.
* Inserts history object if not yet in database
* @param history history object
*/
fun upsertHistoryLastRead(history: History) = db.put()
.`object`(history)
.withPutResolver(HistoryUpsertResolver())
.prepare()
fun upsertHistoryLastRead(history: History) =
db
.put()
.`object`(history)
.withPutResolver(HistoryUpsertResolver())
.prepare()
/**
* Updates the history last read.
* Inserts history object if not yet in database
* @param historyList history object list
*/
fun upsertHistoryLastRead(historyList: List<History>) = db.inTransactionReturn {
db.put()
.objects(historyList)
.withPutResolver(HistoryUpsertResolver())
.prepare()
}
fun upsertHistoryLastRead(historyList: List<History>) =
db.inTransactionReturn {
db
.put()
.objects(historyList)
.withPutResolver(HistoryUpsertResolver())
.prepare()
}
fun deleteHistory() = db.delete()
.byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE)
.build(),
)
.prepare()
fun deleteHistory() =
db
.delete()
.byQuery(
DeleteQuery
.builder()
.table(HistoryTable.TABLE)
.build(),
).prepare()
fun deleteHistoryNoLastRead() = db.delete()
.byQuery(
DeleteQuery.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_LAST_READ} = ?")
.whereArgs(0)
.build(),
)
.prepare()
fun deleteHistoryNoLastRead() =
db
.delete()
.byQuery(
DeleteQuery
.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_LAST_READ} = ?")
.whereArgs(0)
.build(),
).prepare()
}

View File

@@ -9,22 +9,26 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
interface MangaCategoryQueries : DbProvider {
fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
.byQuery(
DeleteQuery.builder()
.table(MangaCategoryTable.TABLE)
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.whereArgs(*mangas.map { it.id }.toTypedArray())
.build(),
)
.prepare()
fun deleteOldMangasCategories(mangas: List<Manga>) =
db
.delete()
.byQuery(
DeleteQuery
.builder()
.table(MangaCategoryTable.TABLE)
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.whereArgs(*mangas.map { it.id }.toTypedArray())
.build(),
).prepare()
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
fun setMangaCategories(
mangasCategories: List<MangaCategory>,
mangas: List<Manga>,
) {
db.inTransaction {
deleteOldMangasCategories(mangas).executeAsBlocking()
insertMangasCategories(mangasCategories).executeAsBlocking()

View File

@@ -23,208 +23,264 @@ import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable
interface MangaQueries : DbProvider {
fun getMangas() =
db
.get()
.listOfObjects(Manga::class.java)
.withQuery(
Query
.builder()
.table(MangaTable.TABLE)
.build(),
).prepare()
fun getMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.build(),
)
.prepare()
fun getLibraryMangas() =
db
.get()
.listOfObjects(LibraryManga::class.java)
.withQuery(
RawQuery
.builder()
.query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
.build(),
).withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
fun getLibraryMangas() = db.get()
.listOfObjects(LibraryManga::class.java)
.withQuery(
RawQuery.builder()
.query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
.build(),
)
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
fun getDuplicateLibraryManga(manga: Manga) =
db
.get()
.`object`(Manga::class.java)
.withQuery(
Query
.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = 1 AND LOWER(${MangaTable.COL_TITLE}) = ? AND ${MangaTable.COL_SOURCE} != ?")
.whereArgs(
manga.title.lowercase(),
manga.source,
).limit(1)
.build(),
).prepare()
fun getDuplicateLibraryManga(manga: Manga) = db.get()
fun getFavoriteMangas() =
db
.get()
.listOfObjects(Manga::class.java)
.withQuery(
Query
.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs(1)
.orderBy(MangaTable.COL_TITLE)
.build(),
).prepare()
fun getManga(
url: String,
sourceId: Long,
) = db
.get()
.`object`(Manga::class.java)
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = 1 AND LOWER(${MangaTable.COL_TITLE}) = ? AND ${MangaTable.COL_SOURCE} != ?")
.whereArgs(
manga.title.lowercase(),
manga.source,
)
.limit(1)
.build(),
)
.prepare()
fun getFavoriteMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs(1)
.orderBy(MangaTable.COL_TITLE)
.build(),
)
.prepare()
fun getManga(url: String, sourceId: Long) = db.get()
.`object`(Manga::class.java)
.withQuery(
Query.builder()
Query
.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
.whereArgs(url, sourceId)
.build(),
)
.prepare()
).prepare()
fun getManga(id: Long) = db.get()
.`object`(Manga::class.java)
.withQuery(
Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(id)
.build(),
)
.prepare()
fun getManga(id: Long) =
db
.get()
.`object`(Manga::class.java)
.withQuery(
Query
.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(id)
.build(),
).prepare()
fun getSourceIdsWithNonLibraryManga() = db.get()
.listOfObjects(SourceIdMangaCount::class.java)
.withQuery(
RawQuery.builder()
.query(getSourceIdsWithNonLibraryMangaQuery())
.observesTables(MangaTable.TABLE)
.build(),
)
.withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
.prepare()
fun getSourceIdsWithNonLibraryManga() =
db
.get()
.listOfObjects(SourceIdMangaCount::class.java)
.withQuery(
RawQuery
.builder()
.query(getSourceIdsWithNonLibraryMangaQuery())
.observesTables(MangaTable.TABLE)
.build(),
).withGetResolver(SourceIdMangaCountGetResolver.INSTANCE)
.prepare()
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
fun updateChapterFlags(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
.prepare()
fun updateChapterFlags(manga: Manga) =
db
.put()
.`object`(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags))
.prepare()
fun updateChapterFlags(manga: List<Manga>) = db.put()
.objects(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags, true))
.prepare()
fun updateChapterFlags(manga: List<Manga>) =
db
.put()
.objects(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_CHAPTER_FLAGS, Manga::chapter_flags, true))
.prepare()
fun updateViewerFlags(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
.prepare()
fun updateViewerFlags(manga: Manga) =
db
.put()
.`object`(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
.prepare()
fun updateViewerFlags(manga: List<Manga>) = db.put()
.objects(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
.prepare()
fun updateViewerFlags(manga: List<Manga>) =
db
.put()
.objects(manga)
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags, true))
.prepare()
fun updateLastUpdated(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaLastUpdatedPutResolver())
.prepare()
fun updateLastUpdated(manga: Manga) =
db
.put()
.`object`(manga)
.withPutResolver(MangaLastUpdatedPutResolver())
.prepare()
fun updateMangaFavorite(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaFavoritePutResolver())
.prepare()
fun updateMangaFavorite(manga: Manga) =
db
.put()
.`object`(manga)
.withPutResolver(MangaFavoritePutResolver())
.prepare()
fun updateMangaAdded(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaDateAddedPutResolver())
.prepare()
fun updateMangaAdded(manga: Manga) =
db
.put()
.`object`(manga)
.withPutResolver(MangaDateAddedPutResolver())
.prepare()
fun updateMangaTitle(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaTitlePutResolver())
.prepare()
fun updateMangaTitle(manga: Manga) =
db
.put()
.`object`(manga)
.withPutResolver(MangaTitlePutResolver())
.prepare()
fun updateMangaInfo(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaInfoPutResolver())
.prepare()
fun updateMangaInfo(manga: Manga) =
db
.put()
.`object`(manga)
.withPutResolver(MangaInfoPutResolver())
.prepare()
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) = db.delete()
.byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})")
.whereArgs(0, *sourceIds.toTypedArray())
.build(),
)
.prepare()
fun deleteMangasNotInLibraryBySourceIds(sourceIds: List<Long>) =
db
.delete()
.byQuery(
DeleteQuery
.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)})")
.whereArgs(0, *sourceIds.toTypedArray())
.build(),
).prepare()
fun deleteMangasNotInLibraryAndNotReadBySourceIds(sourceIds: List<Long>) = db.delete()
.byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE)
.where(
"""
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(sourceIds.size)}) AND ${MangaTable.COL_ID} NOT IN (
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
)
""".trimIndent(),
)
.whereArgs(0, *sourceIds.toTypedArray())
.build(),
)
.prepare()
fun deleteMangasNotInLibraryAndNotReadBySourceIds(sourceIds: List<Long>) =
db
.delete()
.byQuery(
DeleteQuery
.builder()
.table(MangaTable.TABLE)
.where(
"""
${MangaTable.COL_FAVORITE} = ? AND ${MangaTable.COL_SOURCE} IN (${Queries.placeholders(
sourceIds.size,
)}) AND ${MangaTable.COL_ID} NOT IN (
SELECT ${ChapterTable.COL_MANGA_ID} FROM ${ChapterTable.TABLE} WHERE ${ChapterTable.COL_READ} = 1 OR ${ChapterTable.COL_LAST_PAGE_READ} != 0
)
""".trimIndent(),
).whereArgs(0, *sourceIds.toTypedArray())
.build(),
).prepare()
fun deleteMangas() = db.delete()
.byQuery(
DeleteQuery.builder()
.table(MangaTable.TABLE)
.build(),
)
.prepare()
fun deleteMangas() =
db
.delete()
.byQuery(
DeleteQuery
.builder()
.table(MangaTable.TABLE)
.build(),
).prepare()
fun getReadNotInLibraryMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getReadMangaNotInLibraryQuery())
.build(),
)
.prepare()
fun getReadNotInLibraryMangas() =
db
.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery
.builder()
.query(getReadMangaNotInLibraryQuery())
.build(),
).prepare()
fun getLastReadManga() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getLastReadMangaQuery())
.observesTables(MangaTable.TABLE)
.build(),
)
.prepare()
fun getLastReadManga() =
db
.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery
.builder()
.query(getLastReadMangaQuery())
.observesTables(MangaTable.TABLE)
.build(),
).prepare()
fun getLastFetchedManga() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getLastFetchedMangaQuery())
.observesTables(MangaTable.TABLE)
.build(),
)
.prepare()
fun getLastFetchedManga() =
db
.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery
.builder()
.query(getLastFetchedMangaQuery())
.observesTables(MangaTable.TABLE)
.build(),
).prepare()
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare()
fun getTotalChapterManga() =
db
.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery
.builder()
.query(getTotalChapterMangaQuery())
.observesTables(MangaTable.TABLE)
.build(),
).prepare()
fun updateMangaFilteredScanlators(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaFilteredScanlatorsPutResolver())
.prepare()
fun updateMangaFilteredScanlators(manga: Manga) =
db
.put()
.`object`(manga)
.withPutResolver(MangaFilteredScanlatorsPutResolver())
.prepare()
}

View File

@@ -50,8 +50,11 @@ val libraryQuery =
/**
* Query to get the recent chapters of manga from the library up to a date.
*/
fun getRecentsQuery(search: String, offset: Int, isResuming: Boolean) =
"""
fun getRecentsQuery(
search: String,
offset: Int,
isResuming: Boolean,
) = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1
@@ -61,13 +64,16 @@ fun getRecentsQuery(search: String, offset: Int, isResuming: Boolean) =
${limitAndOffset(true, isResuming, offset)}
"""
fun limitAndOffset(endless: Boolean, isResuming: Boolean, offset: Int): String {
return when {
fun limitAndOffset(
endless: Boolean,
isResuming: Boolean,
offset: Int,
): String =
when {
isResuming && endless && offset > 0 -> "LIMIT $offset"
endless -> "LIMIT ${RecentsPresenter.ENDLESS_LIMIT}\nOFFSET $offset"
else -> "LIMIT ${RecentsPresenter.SHORT_LIMIT}"
}
}
/**
* Query to get the recently read chapters of manga from the library up to a date.
@@ -79,8 +85,7 @@ fun getRecentHistoryUngrouped(
search: String = "",
offset: Int = 0,
isResuming: Boolean,
) =
"""
) = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
@@ -103,8 +108,7 @@ fun getRecentMangasLimitQuery(
search: String = "",
offset: Int = 0,
isResuming: Boolean,
) =
"""
) = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
@@ -130,8 +134,10 @@ fun getRecentMangasLimitQuery(
* The select statement returns all information of chapters that have the same id as the chapter in max_last_read
* and are read after the given time period
*/
fun getHistoryPerPeriodQuery(startDate: Long, endDate: Long) =
"""
fun getHistoryPerPeriodQuery(
startDate: Long,
endDate: Long,
) = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}

View File

@@ -7,46 +7,54 @@ import eu.kanade.tachiyomi.data.database.models.SearchMetadata
import eu.kanade.tachiyomi.data.database.tables.SearchMetadataTable
interface SearchMetadataQueries : DbProvider {
fun getSearchMetadataForManga(mangaId: Long) =
db
.get()
.`object`(SearchMetadata::class.java)
.withQuery(
Query
.builder()
.table(SearchMetadataTable.TABLE)
.where("${SearchMetadataTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId)
.build(),
).prepare()
fun getSearchMetadataForManga(mangaId: Long) = db.get()
.`object`(SearchMetadata::class.java)
.withQuery(
Query.builder()
.table(SearchMetadataTable.TABLE)
.where("${SearchMetadataTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId)
.build(),
)
.prepare()
fun getSearchMetadata() =
db
.get()
.listOfObjects(SearchMetadata::class.java)
.withQuery(
Query
.builder()
.table(SearchMetadataTable.TABLE)
.build(),
).prepare()
fun getSearchMetadata() = db.get()
.listOfObjects(SearchMetadata::class.java)
.withQuery(
Query.builder()
.table(SearchMetadataTable.TABLE)
.build(),
)
.prepare()
fun getSearchMetadataByIndexedExtra(extra: String) = db.get()
.listOfObjects(SearchMetadata::class.java)
.withQuery(
Query.builder()
.table(SearchMetadataTable.TABLE)
.where("${SearchMetadataTable.COL_INDEXED_EXTRA} = ?")
.whereArgs(extra)
.build(),
)
.prepare()
fun getSearchMetadataByIndexedExtra(extra: String) =
db
.get()
.listOfObjects(SearchMetadata::class.java)
.withQuery(
Query
.builder()
.table(SearchMetadataTable.TABLE)
.where("${SearchMetadataTable.COL_INDEXED_EXTRA} = ?")
.whereArgs(extra)
.build(),
).prepare()
fun insertSearchMetadata(metadata: SearchMetadata) = db.put().`object`(metadata).prepare()
fun deleteSearchMetadata(metadata: SearchMetadata) = db.delete().`object`(metadata).prepare()
fun deleteAllSearchMetadata() = db.delete().byQuery(
DeleteQuery.builder()
.table(SearchMetadataTable.TABLE)
.build(),
)
.prepare()
fun deleteAllSearchMetadata() =
db
.delete()
.byQuery(
DeleteQuery
.builder()
.table(SearchMetadataTable.TABLE)
.build(),
).prepare()
}

View File

@@ -9,31 +9,36 @@ import eu.kanade.tachiyomi.data.database.tables.TrackTable
import eu.kanade.tachiyomi.data.track.TrackService
interface TrackQueries : DbProvider {
fun getTracks(manga: Manga) = getTracks(manga.id)
fun getTracks(mangaId: Long?) = db.get()
.listOfObjects(Track::class.java)
.withQuery(
Query.builder()
.table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId)
.build(),
)
.prepare()
fun getTracks(mangaId: Long?) =
db
.get()
.listOfObjects(Track::class.java)
.withQuery(
Query
.builder()
.table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ?")
.whereArgs(mangaId)
.build(),
).prepare()
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
fun deleteTrackForManga(
manga: Manga,
sync: TrackService,
) = db
.delete()
.byQuery(
DeleteQuery.builder()
DeleteQuery
.builder()
.table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id)
.build(),
)
.prepare()
).prepare()
}

View File

@@ -10,8 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
class ChapterBackupPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
override fun performPut(
db: StorIOSQLite,
chapter: Chapter,
) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
@@ -19,15 +21,18 @@ class ChapterBackupPutResolver : PutResolver<Chapter>() {
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?")
.whereArgs(chapter.url)
.build()
fun mapToUpdateQuery(chapter: Chapter) =
UpdateQuery
.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?")
.whereArgs(chapter.url)
.build()
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
put(ChapterTable.COL_READ, chapter.read)
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
}
fun mapToContentValues(chapter: Chapter) =
ContentValues(3).apply {
put(ChapterTable.COL_READ, chapter.read)
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
}
}

View File

@@ -10,8 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
override fun performPut(
db: StorIOSQLite,
chapter: Chapter,
) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
@@ -19,11 +21,13 @@ class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(chapter.id)
.build()
fun mapToUpdateQuery(chapter: Chapter) =
UpdateQuery
.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(chapter.id)
.build()
fun mapToContentValues(chapter: Chapter) =
contentValuesOf(

View File

@@ -10,8 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
class ChapterProgressPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
override fun performPut(
db: StorIOSQLite,
chapter: Chapter,
) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
@@ -19,16 +21,19 @@ class ChapterProgressPutResolver : PutResolver<Chapter>() {
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(chapter.id)
.build()
fun mapToUpdateQuery(chapter: Chapter) =
UpdateQuery
.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(chapter.id)
.build()
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
put(ChapterTable.COL_READ, chapter.read)
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
put(ChapterTable.COL_PAGES_LEFT, chapter.pages_left)
}
fun mapToContentValues(chapter: Chapter) =
ContentValues(3).apply {
put(ChapterTable.COL_READ, chapter.read)
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
put(ChapterTable.COL_PAGES_LEFT, chapter.pages_left)
}
}

View File

@@ -10,8 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
override fun performPut(
db: StorIOSQLite,
chapter: Chapter,
) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
@@ -19,13 +21,16 @@ class ChapterSourceOrderPutResolver : PutResolver<Chapter>() {
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(chapter.url, chapter.manga_id)
.build()
fun mapToUpdateQuery(chapter: Chapter) =
UpdateQuery
.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(chapter.url, chapter.manga_id)
.build()
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
}
fun mapToContentValues(chapter: Chapter) =
ContentValues(1).apply {
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
}
}

View File

@@ -10,20 +10,24 @@ import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
class HistoryUpsertResolver : HistoryPutResolver() {
/**
* Updates last_read time of chapter
*/
override fun performPut(db: StorIOSQLite, history: History): PutResult {
override fun performPut(
db: StorIOSQLite,
history: History,
): PutResult {
val updateQuery = mapToUpdateQuery(history)
val cursor = db.lowLevel().query(
Query.builder()
.table(updateQuery.table())
.where(updateQuery.where())
.whereArgs(updateQuery.whereArgs())
.build(),
)
val cursor =
db.lowLevel().query(
Query
.builder()
.table(updateQuery.table())
.where(updateQuery.where())
.whereArgs(updateQuery.whereArgs())
.build(),
)
return cursor.use { putCursor ->
if (putCursor.count == 0) {
@@ -37,11 +41,13 @@ class HistoryUpsertResolver : HistoryPutResolver() {
}
}
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
.whereArgs(obj.chapter_id)
.build()
override fun mapToUpdateQuery(obj: History) =
UpdateQuery
.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
.whereArgs(obj.chapter_id)
.build()
private fun mapToUpdateContentValues(history: History) =
contentValuesOf(

View File

@@ -7,8 +7,9 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
import eu.kanade.tachiyomi.util.chapter.ChapterUtil
class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGetResolver {
class LibraryMangaGetResolver :
DefaultGetResolver<LibraryManga>(),
BaseMangaGetResolver {
companion object {
val INSTANCE = LibraryMangaGetResolver()
}
@@ -17,11 +18,15 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
val manga = LibraryManga()
mapBaseFromCursor(manga, cursor)
manga.unread = cursor.getString(cursor.getColumnIndex(MangaTable.COL_UNREAD))
.filterChaptersByScanlators(manga)
manga.unread =
cursor
.getString(cursor.getColumnIndex(MangaTable.COL_UNREAD))
.filterChaptersByScanlators(manga)
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
manga.read = cursor.getString(cursor.getColumnIndex(MangaTable.COL_HAS_READ))
.filterChaptersByScanlators(manga)
manga.read =
cursor
.getString(cursor.getColumnIndex(MangaTable.COL_HAS_READ))
.filterChaptersByScanlators(manga)
manga.bookmarkCount = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_BOOKMARK_COUNT))
return manga

View File

@@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
import eu.kanade.tachiyomi.data.database.models.MangaChapter
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
companion object {
val INSTANCE = MangaChapterGetResolver()
}

View File

@@ -52,11 +52,12 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>()
historyGetResolver.mapFromCursor(cursor)
} else {
HistoryImpl().apply {
last_read = try {
cursor.getLong(cursor.getColumnIndex(HistoryTable.COL_LAST_READ))
} catch (e: Exception) {
0L
}
last_read =
try {
cursor.getLong(cursor.getColumnIndex(HistoryTable.COL_LAST_READ))
} catch (e: Exception) {
0L
}
}
}

View File

@@ -10,8 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaDateAddedPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
override fun performPut(
db: StorIOSQLite,
manga: Manga,
) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
@@ -19,13 +21,16 @@ class MangaDateAddedPutResolver : PutResolver<Manga>() {
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToUpdateQuery(manga: Manga) =
UpdateQuery
.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_DATE_ADDED, manga.date_added)
}
fun mapToContentValues(manga: Manga) =
ContentValues(1).apply {
put(MangaTable.COL_DATE_ADDED, manga.date_added)
}
}

View File

@@ -10,8 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaFavoritePutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
override fun performPut(
db: StorIOSQLite,
manga: Manga,
) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
@@ -19,13 +21,16 @@ class MangaFavoritePutResolver : PutResolver<Manga>() {
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToUpdateQuery(manga: Manga) =
UpdateQuery
.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_FAVORITE, manga.favorite)
}
fun mapToContentValues(manga: Manga) =
ContentValues(1).apply {
put(MangaTable.COL_FAVORITE, manga.favorite)
}
}

View File

@@ -10,8 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaFilteredScanlatorsPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
override fun performPut(
db: StorIOSQLite,
manga: Manga,
) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
@@ -19,13 +21,16 @@ class MangaFilteredScanlatorsPutResolver : PutResolver<Manga>() {
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToUpdateQuery(manga: Manga) =
UpdateQuery
.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = contentValuesOf(
MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators,
)
fun mapToContentValues(manga: Manga) =
contentValuesOf(
MangaTable.COL_FILTERED_SCANLATORS to manga.filtered_scanlators,
)
}

View File

@@ -10,9 +10,15 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
import kotlin.reflect.KProperty1
class MangaFlagsPutResolver(private val colName: String, private val fieldGetter: KProperty1<Manga, Int>, private val updateAll: Boolean = false) : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
class MangaFlagsPutResolver(
private val colName: String,
private val fieldGetter: KProperty1<Manga, Int>,
private val updateAll: Boolean = false,
) : PutResolver<Manga>() {
override fun performPut(
db: StorIOSQLite,
manga: Manga,
) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)

View File

@@ -9,9 +9,11 @@ import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaInfoPutResolver() : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
class MangaInfoPutResolver : PutResolver<Manga>() {
override fun performPut(
db: StorIOSQLite,
manga: Manga,
) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
@@ -19,18 +21,21 @@ class MangaInfoPutResolver() : PutResolver<Manga>() {
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToUpdateQuery(manga: Manga) =
UpdateQuery
.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_TITLE, manga.originalTitle)
put(MangaTable.COL_GENRE, manga.originalGenre)
put(MangaTable.COL_AUTHOR, manga.originalAuthor)
put(MangaTable.COL_ARTIST, manga.originalArtist)
put(MangaTable.COL_DESCRIPTION, manga.originalDescription)
put(MangaTable.COL_STATUS, manga.originalStatus)
}
fun mapToContentValues(manga: Manga) =
ContentValues(1).apply {
put(MangaTable.COL_TITLE, manga.originalTitle)
put(MangaTable.COL_GENRE, manga.originalGenre)
put(MangaTable.COL_AUTHOR, manga.originalAuthor)
put(MangaTable.COL_ARTIST, manga.originalArtist)
put(MangaTable.COL_DESCRIPTION, manga.originalDescription)
put(MangaTable.COL_STATUS, manga.originalStatus)
}
}

View File

@@ -10,8 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
override fun performPut(
db: StorIOSQLite,
manga: Manga,
) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
@@ -19,13 +21,16 @@ class MangaLastUpdatedPutResolver : PutResolver<Manga>() {
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToUpdateQuery(manga: Manga) =
UpdateQuery
.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
}
fun mapToContentValues(manga: Manga) =
ContentValues(1).apply {
put(MangaTable.COL_LAST_UPDATE, manga.last_update)
}
}

View File

@@ -10,8 +10,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaTitlePutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
override fun performPut(
db: StorIOSQLite,
manga: Manga,
) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
@@ -19,13 +21,16 @@ class MangaTitlePutResolver : PutResolver<Manga>() {
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToUpdateQuery(manga: Manga) =
UpdateQuery
.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_TITLE, manga.title)
}
fun mapToContentValues(manga: Manga) =
ContentValues(1).apply {
put(MangaTable.COL_TITLE, manga.title)
}
}

View File

@@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.data.database.models.SourceIdMangaCount
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class SourceIdMangaCountGetResolver : DefaultGetResolver<SourceIdMangaCount>() {
companion object {
val INSTANCE = SourceIdMangaCountGetResolver()
const val COL_COUNT = "manga_count"

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.database.tables
object CategoryTable {
const val TABLE = "categories"
const val COL_ID = "_id"

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.database.tables
object ChapterTable {
const val TABLE = "chapters"
const val COL_ID = "_id"
@@ -54,8 +53,9 @@ object ChapterTable {
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"
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"

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.database.tables
object HistoryTable {
/**
* Table name
*/

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.database.tables
object MangaCategoryTable {
const val TABLE = "mangas_categories"
const val COL_ID = "_id"

View File

@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.database.tables
object MangaTable {
const val TABLE = "mangas"
const val COL_ID = "_id"
@@ -79,8 +78,9 @@ object MangaTable {
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"
get() =
"CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
"WHERE $COL_FAVORITE = 1"
val addHideTitle: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_HIDE_TITLE INTEGER DEFAULT 0"

View File

@@ -3,7 +3,6 @@ package eu.kanade.tachiyomi.data.database.tables
import eu.kanade.tachiyomi.data.track.TrackManager
object TrackTable {
const val TABLE = "manga_sync"
const val COL_ID = "_id"
@@ -83,8 +82,8 @@ object TrackTable {
val updateMangaUpdatesScore: String
get() =
"""
UPDATE $TABLE
SET $COL_SCORE = max($COL_SCORE, 0)
WHERE $COL_SYNC_ID = ${TrackManager.MANGA_UPDATES};
UPDATE $TABLE
SET $COL_SCORE = max($COL_SCORE, 0)
WHERE $COL_SYNC_ID = ${TrackManager.MANGA_UPDATES};
""".trimIndent()
}

View File

@@ -37,7 +37,6 @@ class DownloadCache(
private val sourceManager: SourceManager,
private val preferences: PreferencesHelper = Injekt.get(),
) {
/**
* The interval after which this cache should be invalidated. 1 hour shouldn't cause major
* issues, as the cache is only used for UI feedback.
@@ -54,7 +53,9 @@ class DownloadCache(
val scope = CoroutineScope(Job() + Dispatchers.IO)
init {
preferences.downloadsDirectory().asFlow()
preferences
.downloadsDirectory()
.asFlow()
.drop(1)
.onEach { lastRenew = 0L } // invalidate cache
.launchIn(scope)
@@ -75,7 +76,11 @@ class DownloadCache(
* @param manga the manga of the chapter.
* @param skipCache whether to skip the directory cache and check in the filesystem.
*/
fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean): Boolean {
fun isChapterDownloaded(
chapter: Chapter,
manga: Manga,
skipCache: Boolean,
): Boolean {
if (skipCache) {
val source = sourceManager.get(manga.source) ?: return false
return provider.findChapterDir(chapter, manga, source) != null
@@ -94,7 +99,10 @@ class DownloadCache(
*
* @param manga the manga to check.
*/
fun getDownloadCount(manga: Manga, forceCheckFolder: Boolean = false): Int {
fun getDownloadCount(
manga: Manga,
forceCheckFolder: Boolean = false,
): Int {
checkRenew()
if (forceCheckFolder) {
@@ -137,10 +145,14 @@ class DownloadCache(
private fun renew() {
val onlineSources = sourceManager.getOnlineSources()
val sourceDirs = getDirectoryFromPreference().listFiles().orEmpty()
.associate { it.name to SourceDirectory(it) }.mapNotNullKeys { entry ->
onlineSources.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id
}
val sourceDirs =
getDirectoryFromPreference()
.listFiles()
.orEmpty()
.associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry ->
onlineSources.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id
}
val db: DatabaseHelper by injectLazy()
val mangas = db.getMangas().executeAsBlocking().groupBy { it.source }
@@ -151,17 +163,31 @@ class DownloadCache(
val sourceDir = sourceValue.value
val mangaDirs = sourceDir.dir.listFiles().orEmpty().mapNotNull { mangaDir ->
val name = mangaDir.name ?: return@mapNotNull null
val chapterDirs = mangaDir.listFiles().orEmpty().mapNotNull { chapterFile -> chapterFile.name?.substringBeforeLast(".cbz") }.toHashSet()
name to MangaDirectory(mangaDir, chapterDirs)
}.toMap()
val mangaDirs =
sourceDir.dir
.listFiles()
.orEmpty()
.mapNotNull { mangaDir ->
val name = mangaDir.name ?: return@mapNotNull null
val chapterDirs =
mangaDir
.listFiles()
.orEmpty()
.mapNotNull { chapterFile ->
chapterFile.name?.substringBeforeLast(".cbz")
}.toHashSet()
name to MangaDirectory(mangaDir, chapterDirs)
}.toMap()
val trueMangaDirs = mangaDirs.mapNotNull { mangaDir ->
val manga = findManga(sourceMangaPair.first, mangaDir.key, sourceValue.key) ?: findManga(sourceMangaPair.second, mangaDir.key, sourceValue.key)
val id = manga?.id ?: return@mapNotNull null
id to mangaDir.value.files
}.toMap()
val trueMangaDirs =
mangaDirs
.mapNotNull { mangaDir ->
val manga =
findManga(sourceMangaPair.first, mangaDir.key, sourceValue.key)
?: findManga(sourceMangaPair.second, mangaDir.key, sourceValue.key)
val id = manga?.id ?: return@mapNotNull null
id to mangaDir.value.files
}.toMap()
mangaFiles.putAll(trueMangaDirs)
}
@@ -170,11 +196,14 @@ class DownloadCache(
/**
* Searches a manga list and matches the given mangakey and source key
*/
private fun findManga(mangaList: List<Manga>, mangaKey: String, sourceKey: Long): Manga? {
return mangaList.find {
private fun findManga(
mangaList: List<Manga>,
mangaKey: String,
sourceKey: Long,
): Manga? =
mangaList.find {
DiskUtil.buildValidFilename(it.originalTitle).equals(mangaKey, ignoreCase = true) && it.source == sourceKey
}
}
/**
* Adds a chapter that has just been download to this cache.
@@ -184,7 +213,10 @@ class DownloadCache(
* @param manga the manga of the chapter.
*/
@Synchronized
fun addChapter(chapterDirName: String, manga: Manga) {
fun addChapter(
chapterDirName: String,
manga: Manga,
) {
val id = manga.id ?: return
val files = mangaFiles[id]
if (files == null) {
@@ -201,7 +233,10 @@ class DownloadCache(
* @param manga the manga of the chapter.
*/
@Synchronized
fun removeChapters(chapters: List<Chapter>, manga: Manga) {
fun removeChapters(
chapters: List<Chapter>,
manga: Manga,
) {
val id = manga.id ?: return
for (chapter in chapters) {
val list = provider.getValidChapterDirNames(chapter)
@@ -213,7 +248,10 @@ class DownloadCache(
}
}
fun removeFolders(folders: List<String>, manga: Manga) {
fun removeFolders(
folders: List<String>,
manga: Manga,
) {
val id = manga.id ?: return
for (chapter in folders) {
if (mangaFiles[id] != null && chapter in mangaFiles[id]!!) {

View File

@@ -32,8 +32,10 @@ import uy.kohesive.injekt.api.get
* This worker is used to manage the downloader. The system can decide to stop the worker, in
* which case the downloader is also stopped. It's also stopped while there's no network available.
*/
class DownloadJob(val context: Context, workerParams: WorkerParameters) : CoroutineWorker(context, workerParams) {
class DownloadJob(
val context: Context,
workerParams: WorkerParameters,
) : CoroutineWorker(context, workerParams) {
private val downloadManager: DownloadManager = Injekt.get()
private val preferences: PreferencesHelper = Injekt.get()
@@ -76,8 +78,8 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout
}
}
private fun checkConnectivity(): Boolean {
return with(applicationContext) {
private fun checkConnectivity(): Boolean =
with(applicationContext) {
if (isOnline()) {
val noWifi = preferences.downloadOnlyOverWifi() && !isConnectedToWifi()
if (noWifi) {
@@ -89,28 +91,33 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout
false
}
}
}
companion object {
private const val TAG = "Downloader"
private const val START_EXT_JOB_AFTER = "StartExtJobAfter"
private val downloadChannel = MutableSharedFlow<Boolean>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
private val downloadChannel =
MutableSharedFlow<Boolean>(
extraBufferCapacity = 1,
onBufferOverflow = BufferOverflow.DROP_OLDEST,
)
val downloadFlow = downloadChannel.asSharedFlow()
fun start(context: Context, alsoStartExtJob: Boolean = false) {
val request = OneTimeWorkRequestBuilder<DownloadJob>()
.addTag(TAG)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST).apply {
if (alsoStartExtJob) {
setInputData(workDataOf(START_EXT_JOB_AFTER to true))
}
}
.build()
WorkManager.getInstance(context)
fun start(
context: Context,
alsoStartExtJob: Boolean = false,
) {
val request =
OneTimeWorkRequestBuilder<DownloadJob>()
.addTag(TAG)
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
.apply {
if (alsoStartExtJob) {
setInputData(workDataOf(START_EXT_JOB_AFTER to true))
}
}.build()
WorkManager
.getInstance(context)
.enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, request)
}
@@ -118,16 +125,19 @@ class DownloadJob(val context: Context, workerParams: WorkerParameters) : Corout
WorkManager.getInstance(context).cancelUniqueWork(TAG)
}
fun callListeners(downloading: Boolean? = null, downloadManager: DownloadManager? = null) {
fun callListeners(
downloading: Boolean? = null,
downloadManager: DownloadManager? = null,
) {
val dManager by lazy { downloadManager ?: Injekt.get() }
downloadChannel.tryEmit(downloading ?: !dManager.isPaused())
}
fun isRunning(context: Context): Boolean {
return WorkManager.getInstance(context)
fun isRunning(context: Context): Boolean =
WorkManager
.getInstance(context)
.getWorkInfosForUniqueWork(TAG)
.get()
.let { list -> list.count { it.state == WorkInfo.State.RUNNING } == 1 }
}
}
}

View File

@@ -24,8 +24,9 @@ import uy.kohesive.injekt.injectLazy
*
* @param context the application context.
*/
class DownloadManager(val context: Context) {
class DownloadManager(
val context: Context,
) {
/**
* The sources manager.
*/
@@ -146,7 +147,11 @@ class DownloadManager(val context: Context) {
* @param chapters the list of chapters to enqueue.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/
fun downloadChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean = true) {
fun downloadChapters(
manga: Manga,
chapters: List<Chapter>,
autoStart: Boolean = true,
) {
downloader.queueChapters(manga, chapters, autoStart)
}
@@ -172,16 +177,24 @@ class DownloadManager(val context: Context) {
* @param chapter the downloaded chapter.
* @return the list of pages from the chapter.
*/
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): List<Page> {
fun buildPageList(
source: Source,
manga: Manga,
chapter: Chapter,
): List<Page> {
val chapterDir = provider.findChapterDir(chapter, manga, source)
val files = chapterDir?.listFiles().orEmpty()
.filter { "image" in it.type.orEmpty() }
val files =
chapterDir
?.listFiles()
.orEmpty()
.filter { "image" in it.type.orEmpty() }
if (files.isEmpty()) {
throw Exception(context.getString(R.string.no_pages_found))
}
return files.sortedBy { it.name }
return files
.sortedBy { it.name }
.mapIndexed { i, file ->
Page(i, uri = file.uri).apply { status = Page.State.READY }
}
@@ -194,9 +207,11 @@ class DownloadManager(val context: Context) {
* @param manga the manga of the chapter.
* @param skipCache whether to skip the directory cache and check in the filesystem.
*/
fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean = false): Boolean {
return cache.isChapterDownloaded(chapter, manga, skipCache)
}
fun isChapterDownloaded(
chapter: Chapter,
manga: Manga,
skipCache: Boolean = false,
): Boolean = cache.isChapterDownloaded(chapter, manga, skipCache)
/**
* Returns the download from queue if the chapter is queued for download
@@ -204,19 +219,16 @@ class DownloadManager(val context: Context) {
*
* @param chapter the chapter to check.
*/
fun getChapterDownloadOrNull(chapter: Chapter): Download? {
return downloader.queue
fun getChapterDownloadOrNull(chapter: Chapter): Download? =
downloader.queue
.firstOrNull { it.chapter.id == chapter.id && it.chapter.manga_id == chapter.manga_id }
}
/**
* Returns the amount of downloaded chapters for a manga.
*
* @param manga the manga to check.
*/
fun getDownloadCount(manga: Manga): Int {
return cache.getDownloadCount(manga)
}
fun getDownloadCount(manga: Manga): Int = cache.getDownloadCount(manga)
/*fun renameCache(from: String, to: String, source: Long) {
cache.renameFolder(from, to, source)
@@ -242,7 +254,12 @@ class DownloadManager(val context: Context) {
* @param manga the manga of the chapters.
* @param source the source of the chapters.
*/
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source, force: Boolean = false) {
fun deleteChapters(
chapters: List<Chapter>,
manga: Manga,
source: Source,
force: Boolean = false,
) {
val filteredChapters = if (force) chapters else getChaptersToDelete(chapters, manga)
GlobalScope.launch(Dispatchers.IO) {
val wasPaused = isPaused()
@@ -263,11 +280,12 @@ class DownloadManager(val context: Context) {
}
queue.remove(filteredChapters)
val chapterDirs =
provider.findChapterDirs(filteredChapters, manga, source) + provider.findTempChapterDirs(
filteredChapters,
manga,
source,
)
provider.findChapterDirs(filteredChapters, manga, source) +
provider.findTempChapterDirs(
filteredChapters,
manga,
source,
)
chapterDirs.forEach { it.delete() }
cache.removeChapters(filteredChapters, manga)
if (cache.getDownloadCount(manga, true) == 0) { // Delete manga directory if empty
@@ -280,9 +298,7 @@ class DownloadManager(val context: Context) {
/**
* return the list of all manga folders
*/
fun getMangaFolders(source: Source): List<UniFile> {
return provider.findSourceDir(source)?.listFiles()?.toList() ?: emptyList()
}
fun getMangaFolders(source: Source): List<UniFile> = provider.findSourceDir(source)?.listFiles()?.toList() ?: emptyList()
/**
* Deletes the directories of chapters that were read or have no match
@@ -291,7 +307,13 @@ class DownloadManager(val context: Context) {
* @param manga the manga of the chapters.
* @param source the source of the chapters.
*/
fun cleanupChapters(allChapters: List<Chapter>, manga: Manga, source: Source, removeRead: Boolean, removeNonFavorite: Boolean): Int {
fun cleanupChapters(
allChapters: List<Chapter>,
manga: Manga,
source: Source,
removeRead: Boolean,
removeNonFavorite: Boolean,
): Int {
var cleaned = 0
if (removeNonFavorite && !manga.favorite) {
@@ -334,7 +356,10 @@ class DownloadManager(val context: Context) {
* @param manga the manga to delete.
* @param source the source of the manga.
*/
fun deleteManga(manga: Manga, source: Source) {
fun deleteManga(
manga: Manga,
source: Source,
) {
downloader.clearQueue(manga, true)
queue.remove(manga)
provider.findMangaDir(manga, source)?.delete()
@@ -348,7 +373,10 @@ class DownloadManager(val context: Context) {
* @param chapters the list of chapters to delete.
* @param manga the manga of the chapters.
*/
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
fun enqueueDeleteChapters(
chapters: List<Chapter>,
manga: Manga,
) {
pendingDeleter.addChapters(getChaptersToDelete(chapters, manga), manga)
}
@@ -370,15 +398,22 @@ class DownloadManager(val context: Context) {
* @param oldChapter the existing chapter with the old name.
* @param newChapter the target chapter with the new name.
*/
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
fun renameChapter(
source: Source,
manga: Manga,
oldChapter: Chapter,
newChapter: Chapter,
) {
val oldNames = provider.getValidChapterDirNames(oldChapter).map { listOf(it, "$it.cbz") }.flatten()
var newName = provider.getChapterDirName(newChapter)
val mangaDir = provider.getMangaDir(manga, source)
// Assume there's only 1 version of the chapter name formats present
val oldDownload = oldNames.asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull() ?: return
val oldDownload =
oldNames
.asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull() ?: return
if (oldDownload.isFile && oldDownload.name?.endsWith(".cbz") == true) {
newName += ".cbz"
@@ -400,9 +435,13 @@ class DownloadManager(val context: Context) {
}
fun addListener(listener: DownloadQueue.DownloadListener) = queue.addListener(listener)
fun removeListener(listener: DownloadQueue.DownloadListener) = queue.removeListener(listener)
private fun getChaptersToDelete(chapters: List<Chapter>, manga: Manga): List<Chapter> {
private fun getChaptersToDelete(
chapters: List<Chapter>,
manga: Manga,
): List<Chapter> {
// Retrieve the categories that are set to exclude from being deleted on read
return if (!preferences.removeBookmarkedChapters().get()) {
chapters.filterNot { it.bookmark }

View File

@@ -26,15 +26,17 @@ import java.util.regex.Pattern
*
* @param context context of application
*/
internal class DownloadNotifier(private val context: Context) {
internal class DownloadNotifier(
private val context: Context,
) {
private val preferences: PreferencesHelper by injectLazy()
/**
* Notification builder.
*/
private val notification by lazy {
NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER)
NotificationCompat
.Builder(context, Notifications.CHANNEL_DOWNLOADER)
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
}
@@ -90,11 +92,12 @@ internal class DownloadNotifier(private val context: Context) {
val title = download.manga.title.chop(15)
val quotedTitle = Pattern.quote(title)
val name = download.chapter.preferredChapterName(context, download.manga, preferences)
val chapter = name.replaceFirst(
"$quotedTitle[\\s]*[-]*[\\s]*"
.toRegex(RegexOption.IGNORE_CASE),
"",
)
val chapter =
name.replaceFirst(
"$quotedTitle[\\s]*[-]*[\\s]*"
.toRegex(RegexOption.IGNORE_CASE),
"",
)
setContentTitle("$title - $chapter".chop(30))
setContentText(context.getString(R.string.downloading))
} else {
@@ -134,7 +137,8 @@ internal class DownloadNotifier(private val context: Context) {
}
val downloadingProgressText =
context.localeContext.getString(R.string.downloading_progress)
context.localeContext
.getString(R.string.downloading_progress)
.format(download.downloadedImages, download.pages!!.size)
if (preferences.hideNotificationContent()) {
@@ -143,10 +147,11 @@ internal class DownloadNotifier(private val context: Context) {
val title = download.manga.title.chop(15)
val quotedTitle = Pattern.quote(title)
val name = download.chapter.preferredChapterName(context, download.manga, preferences)
val chapter = name.replaceFirst(
"$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE),
"",
)
val chapter =
name.replaceFirst(
"$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE),
"",
)
setContentTitle("$title - $chapter".chop(30))
setContentText(downloadingProgressText)
}
@@ -221,22 +226,24 @@ internal class DownloadNotifier(private val context: Context) {
*/
fun massDownloadWarning() {
val context = context.localeContext
val notification = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
setContentTitle(context.getString(R.string.warning))
setSmallIcon(R.drawable.ic_warning_white_24dp)
setStyle(
NotificationCompat.BigTextStyle()
.bigText(context.getString(R.string.download_queue_size_warning)),
)
setContentIntent(
NotificationHandler.openUrl(
context,
LibraryUpdateNotifier.HELP_WARNING_URL,
),
)
setTimeoutAfter(30000)
}
.build()
val notification =
context
.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) {
setContentTitle(context.getString(R.string.warning))
setSmallIcon(R.drawable.ic_warning_white_24dp)
setStyle(
NotificationCompat
.BigTextStyle()
.bigText(context.getString(R.string.download_queue_size_warning)),
)
setContentIntent(
NotificationHandler.openUrl(
context,
LibraryUpdateNotifier.HELP_WARNING_URL,
),
)
setTimeoutAfter(30000)
}.build()
context.notificationManager.notify(
Notifications.ID_DOWNLOAD_SIZE_WARNING,

View File

@@ -15,8 +15,9 @@ import uy.kohesive.injekt.injectLazy
*
* @param context the application context.
*/
class DownloadPendingDeleter(context: Context) {
class DownloadPendingDeleter(
context: Context,
) {
private val json: Json by injectLazy()
/**
@@ -36,36 +37,40 @@ class DownloadPendingDeleter(context: Context) {
* @param manga the manga of the chapters.
*/
@Synchronized
fun addChapters(chapters: List<Chapter>, manga: Manga) {
fun addChapters(
chapters: List<Chapter>,
manga: Manga,
) {
val lastEntry = lastAddedEntry
val newEntry = if (lastEntry != null && lastEntry.manga.id == manga.id) {
// Append new chapters
val newChapters = lastEntry.chapters.addUniqueById(chapters)
// If no chapters were added, do nothing
if (newChapters.size == lastEntry.chapters.size) return
// Last entry matches the manga, reuse it to avoid decoding json from preferences
lastEntry.copy(chapters = newChapters)
} else {
val existingEntry = preferences.getString(manga.id!!.toString(), null)
if (existingEntry != null) {
// Existing entry found on preferences, decode json and add the new chapter
val savedEntry = json.decodeFromString<Entry>(existingEntry)
val newEntry =
if (lastEntry != null && lastEntry.manga.id == manga.id) {
// Append new chapters
val newChapters = savedEntry.chapters.addUniqueById(chapters)
val newChapters = lastEntry.chapters.addUniqueById(chapters)
// If no chapters were added, do nothing
if (newChapters.size == savedEntry.chapters.size) return
if (newChapters.size == lastEntry.chapters.size) return
savedEntry.copy(chapters = newChapters)
// Last entry matches the manga, reuse it to avoid decoding json from preferences
lastEntry.copy(chapters = newChapters)
} else {
// No entry has been found yet, create a new one
Entry(chapters.map { it.toEntry() }, manga.toEntry())
val existingEntry = preferences.getString(manga.id!!.toString(), null)
if (existingEntry != null) {
// Existing entry found on preferences, decode json and add the new chapter
val savedEntry = json.decodeFromString<Entry>(existingEntry)
// Append new chapters
val newChapters = savedEntry.chapters.addUniqueById(chapters)
// If no chapters were added, do nothing
if (newChapters.size == savedEntry.chapters.size) return
savedEntry.copy(chapters = newChapters)
} else {
// No entry has been found yet, create a new one
Entry(chapters.map { it.toEntry() }, manga.toEntry())
}
}
}
// Save current state
val json = json.encodeToString(newEntry)
@@ -97,15 +102,14 @@ class DownloadPendingDeleter(context: Context) {
/**
* Decodes all the chapters from preferences.
*/
private fun decodeAll(): List<Entry> {
return preferences.all.values.mapNotNull { rawEntry ->
private fun decodeAll(): List<Entry> =
preferences.all.values.mapNotNull { rawEntry ->
try {
(rawEntry as? String)?.let { json.decodeFromString<Entry>(it) }
} catch (e: Exception) {
null
}
}
}
/**
* Returns a copy of chapter entries ensuring no duplicates by chapter id.
@@ -154,35 +158,29 @@ class DownloadPendingDeleter(context: Context) {
/**
* Returns a manga entry from a manga model.
*/
private fun Manga.toEntry(): MangaEntry {
return MangaEntry(id!!, url, originalTitle, source)
}
private fun Manga.toEntry(): MangaEntry = MangaEntry(id!!, url, originalTitle, source)
/**
* Returns a chapter entry from a chapter model.
*/
private fun Chapter.toEntry(): ChapterEntry {
return ChapterEntry(id!!, url, name, scanlator)
}
private fun Chapter.toEntry(): ChapterEntry = ChapterEntry(id!!, url, name, scanlator)
/**
* Returns a manga model from a manga entry.
*/
private fun MangaEntry.toModel(): Manga {
return Manga.create(url, title, source).also {
private fun MangaEntry.toModel(): Manga =
Manga.create(url, title, source).also {
it.id = id
}
}
/**
* Returns a chapter model from a chapter entry.
*/
private fun ChapterEntry.toModel(): Chapter {
return Chapter.create().also {
private fun ChapterEntry.toModel(): Chapter =
Chapter.create().also {
it.id = id
it.url = url
it.name = name
it.scanlator = scanlator
}
}
}

View File

@@ -25,8 +25,9 @@ import uy.kohesive.injekt.injectLazy
*
* @param context the application context.
*/
class DownloadProvider(private val context: Context) {
class DownloadProvider(
private val context: Context,
) {
/**
* Preferences helper.
*/
@@ -37,16 +38,21 @@ class DownloadProvider(private val context: Context) {
/**
* The root directory for downloads.
*/
private var downloadsDir = preferences.downloadsDirectory().get().let {
val dir = UniFile.fromUri(context, it.toUri())
DiskUtil.createNoMediaFile(dir, context)
dir
}
private var downloadsDir =
preferences.downloadsDirectory().get().let {
val dir = UniFile.fromUri(context, it.toUri())
DiskUtil.createNoMediaFile(dir, context)
dir
}
init {
preferences.downloadsDirectory().asFlow().drop(1).onEach {
downloadsDir = UniFile.fromUri(context, it.toUri())
}.launchIn(scope)
preferences
.downloadsDirectory()
.asFlow()
.drop(1)
.onEach {
downloadsDir = UniFile.fromUri(context, it.toUri())
}.launchIn(scope)
}
/**
@@ -55,9 +61,13 @@ class DownloadProvider(private val context: Context) {
* @param manga the manga to query.
* @param source the source of the manga.
*/
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
internal fun getMangaDir(
manga: Manga,
source: Source,
): UniFile {
try {
return downloadsDir.createDirectory(getSourceDirName(source))
return downloadsDir
.createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga))
} catch (e: NullPointerException) {
throw Exception(context.getString(R.string.invalid_download_location))
@@ -69,9 +79,7 @@ class DownloadProvider(private val context: Context) {
*
* @param source the source to query.
*/
fun findSourceDir(source: Source): UniFile? {
return downloadsDir.findFile(getSourceDirName(source), true)
}
fun findSourceDir(source: Source): UniFile? = downloadsDir.findFile(getSourceDirName(source), true)
/**
* Returns the download directory for a manga if it exists.
@@ -79,7 +87,10 @@ class DownloadProvider(private val context: Context) {
* @param manga the manga to query.
* @param source the source of the manga.
*/
fun findMangaDir(manga: Manga, source: Source): UniFile? {
fun findMangaDir(
manga: Manga,
source: Source,
): UniFile? {
val sourceDir = findSourceDir(source)
return sourceDir?.findFile(getMangaDirName(manga), true)
}
@@ -91,9 +102,14 @@ class DownloadProvider(private val context: Context) {
* @param manga the manga of the chapter.
* @param source the source of the chapter.
*/
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
fun findChapterDir(
chapter: Chapter,
manga: Manga,
source: Source,
): UniFile? {
val mangaDir = findMangaDir(manga, source)
return getValidChapterDirNames(chapter).asSequence()
return getValidChapterDirNames(chapter)
.asSequence()
.mapNotNull { mangaDir?.findFile(it, true) ?: mangaDir?.findFile("$it.cbz", true) }
.firstOrNull()
}
@@ -105,10 +121,17 @@ class DownloadProvider(private val context: Context) {
* @param manga the manga of the chapter.
* @param source the source of the chapter.
*/
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
fun findChapterDirs(
chapters: List<Chapter>,
manga: Manga,
source: Source,
): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return chapters.mapNotNull { chapter ->
getValidChapterDirNames(chapter).map { listOf(it, "$it.cbz") }.flatten().asSequence()
getValidChapterDirNames(chapter)
.map { listOf(it, "$it.cbz") }
.flatten()
.asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull()
}
@@ -140,7 +163,11 @@ class DownloadProvider(private val context: Context) {
}
}
fun renameMangaFolder(from: String, to: String, sourceId: Long) {
fun renameMangaFolder(
from: String,
to: String,
sourceId: Long,
) {
val sourceManager by injectLazy<SourceManager>()
val source = sourceManager.get(sourceId) ?: return
val sourceDir = findSourceDir(source)
@@ -194,7 +221,11 @@ class DownloadProvider(private val context: Context) {
* @param manga the manga of the chapter.
* @param source the source of the chapter.
*/
fun findTempChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
fun findTempChapterDirs(
chapters: List<Chapter>,
manga: Manga,
source: Source,
): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return chapters.mapNotNull { mangaDir.findFile("${getChapterDirName(it)}_tmp") }
}
@@ -204,45 +235,42 @@ class DownloadProvider(private val context: Context) {
*
* @param source the source to query.
*/
fun getSourceDirName(source: Source): String {
return source.toString()
}
fun getSourceDirName(source: Source): String = source.toString()
/**
* Returns the download directory name for a manga.
*
* @param manga the manga to query.
*/
fun getMangaDirName(manga: Manga): String {
return DiskUtil.buildValidFilename(manga.originalTitle)
}
fun getMangaDirName(manga: Manga): String = DiskUtil.buildValidFilename(manga.originalTitle)
/**
* Returns the chapter directory name for a chapter.
*
* @param chapter the chapter to query.
*/
fun getChapterDirName(chapter: Chapter, includeBlank: Boolean = false): String {
return DiskUtil.buildValidFilename(
fun getChapterDirName(
chapter: Chapter,
includeBlank: Boolean = false,
): String =
DiskUtil.buildValidFilename(
if (!chapter.scanlator.isNullOrBlank()) {
"${chapter.scanlator}_${chapter.name}"
} else {
(if (includeBlank) "_" else "") + chapter.name
},
)
}
/**
* Returns valid downloaded chapter directory names.
*
* @param chapter the chapter to query.
*/
fun getValidChapterDirNames(chapter: Chapter): List<String> {
return listOf(
fun getValidChapterDirNames(chapter: Chapter): List<String> =
listOf(
getChapterDirName(chapter),
getChapterDirName(chapter, true),
// Legacy chapter directory name used in v0.8.4 and before
DiskUtil.buildValidFilename(chapter.name),
).distinct()
}
}

View File

@@ -22,7 +22,6 @@ class DownloadStore(
context: Context,
private val sourceManager: SourceManager,
) {
/**
* Preference file where active downloads are stored.
*/
@@ -72,26 +71,26 @@ class DownloadStore(
*
* @param download the download.
*/
private fun getKey(download: Download): String {
return download.chapter.id!!.toString()
}
private fun getKey(download: Download): String = download.chapter.id!!.toString()
/**
* Returns the list of downloads to restore. It should be called in a background thread.
*/
fun restore(): List<Download> {
val objs = preferences.all
.mapNotNull { it.value as? String }
.mapNotNull { deserialize(it) }
.sortedBy { it.order }
val objs =
preferences.all
.mapNotNull { it.value as? String }
.mapNotNull { deserialize(it) }
.sortedBy { it.order }
val downloads = mutableListOf<Download>()
if (objs.isNotEmpty()) {
val cachedManga = mutableMapOf<Long, Manga?>()
for ((mangaId, chapterId) in objs) {
val manga = cachedManga.getOrPut(mangaId) {
db.getManga(mangaId).executeAsBlocking()
} ?: continue
val manga =
cachedManga.getOrPut(mangaId) {
db.getManga(mangaId).executeAsBlocking()
} ?: continue
val source = sourceManager.get(manga.source) as? HttpSource ?: continue
val chapter = db.getChapter(chapterId).executeAsBlocking() ?: continue
downloads.add(Download(source, manga, chapter))
@@ -118,13 +117,12 @@ class DownloadStore(
*
* @param string the download as string.
*/
private fun deserialize(string: String): DownloadObject? {
return try {
private fun deserialize(string: String): DownloadObject? =
try {
json.decodeFromString<DownloadObject>(string)
} catch (e: Exception) {
null
}
}
/**
* Class used for download serialization
@@ -134,5 +132,9 @@ class DownloadStore(
* @param order the order of the download in the queue.
*/
@Serializable
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
data class DownloadObject(
val mangaId: Long,
val chapterId: Long,
val order: Int,
)
}

View File

@@ -204,10 +204,14 @@ class Downloader(
*
* @param isNotification value that determines if status is set (needed for view updates)
*/
fun clearQueue(manga: Manga, isNotification: Boolean = false) {
fun clearQueue(
manga: Manga,
isNotification: Boolean = false,
) {
// Needed to update the chapter view
if (isNotification) {
queue.filter { it.status == Download.State.QUEUE && it.manga.id == manga.id }
queue
.filter { it.status == Download.State.QUEUE && it.manga.id == manga.id }
.forEach { it.status = Download.State.NOT_DOWNLOADED }
}
queue.remove(manga)
@@ -224,32 +228,34 @@ class Downloader(
private fun initializeSubscription() {
if (isRunning) return
subscription = downloadsRelay.concatMapIterable { it }
// Concurrently download from 5 different sources
.groupBy { it.source }
.flatMap(
{ bySource ->
bySource.concatMap { download ->
Observable.fromCallable {
runBlocking { downloadChapter(download) }
download
}.subscribeOn(Schedulers.io())
}
},
5,
)
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
completeDownload(it)
},
{ error ->
Timber.e(error)
notifier.onError(error.message)
stop()
},
)
subscription =
downloadsRelay
.concatMapIterable { it }
// Concurrently download from 5 different sources
.groupBy { it.source }
.flatMap(
{ bySource ->
bySource.concatMap { download ->
Observable
.fromCallable {
runBlocking { downloadChapter(download) }
download
}.subscribeOn(Schedulers.io())
}
},
5,
).onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(
{
completeDownload(it)
},
{ error ->
Timber.e(error)
notifier.onError(error.message)
stop()
},
)
}
/**
@@ -267,7 +273,11 @@ class Downloader(
* @param chapters the list of chapters to download.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchIO {
fun queueChapters(
manga: Manga,
chapters: List<Chapter>,
autoStart: Boolean,
) = launchIO {
if (chapters.isEmpty()) {
return@launchIO
}
@@ -275,20 +285,23 @@ class Downloader(
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO
val wasEmpty = queue.isEmpty()
// Called in background thread, the operation can be slow with SAF.
val chaptersWithoutDir = async {
chapters
// Filter out those already downloaded.
.filter { provider.findChapterDir(it, manga, source) == null }
// Add chapters to queue from the start.
.sortedByDescending { it.source_order }
}
val chaptersWithoutDir =
async {
chapters
// Filter out those already downloaded.
.filter { provider.findChapterDir(it, manga, source) == null }
// Add chapters to queue from the start.
.sortedByDescending { it.source_order }
}
// Runs in main thread (synchronization needed).
val chaptersToQueue = chaptersWithoutDir.await()
// Filter out those already enqueued.
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
// Create a download for each one.
.map { Download(source, manga, it) }
val chaptersToQueue =
chaptersWithoutDir
.await()
// Filter out those already enqueued.
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
// Create a download for each one.
.map { Download(source, manga, it) }
if (chaptersToQueue.isNotEmpty()) {
queue.addAll(chaptersToQueue)
@@ -301,10 +314,11 @@ class Downloader(
// Start downloader if needed
if (autoStart && wasEmpty) {
val queuedDownloads = queue.count { it.source !is UnmeteredSource }
val maxDownloadsFromSource = queue
.groupBy { it.source }
.filterKeys { it !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
val maxDownloadsFromSource =
queue
.groupBy { it.source }
.filterKeys { it !is UnmeteredSource }
.maxOfOrNull { it.value.size } ?: 0
if (
queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
@@ -338,10 +352,11 @@ class Downloader(
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R &&
!Environment.isExternalStorageManager()
) {
val intent = Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
"package:${context.packageName}".toUri(),
)
val intent =
Intent(
Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION,
"package:${context.packageName}".toUri(),
)
notifier.onError(
context.getString(R.string.external_storage_download_notice),
@@ -356,28 +371,31 @@ class Downloader(
try {
// If the page list already exists, start from the file
val pageList = download.pages ?: run {
// Otherwise, pull page list from network and add them to download object
val pages = download.source.getPageList(download.chapter)
val pageList =
download.pages ?: run {
// Otherwise, pull page list from network and add them to download object
val pages = download.source.getPageList(download.chapter)
if (pages.isEmpty()) {
throw Exception(context.getString(R.string.no_pages_found))
if (pages.isEmpty()) {
throw Exception(context.getString(R.string.no_pages_found))
}
// Don't trust index from source
val reIndexedPages =
pages.mapIndexed { index, page ->
Page(
index,
page.url,
page.imageUrl,
page.uri,
)
}
download.pages = reIndexedPages
reIndexedPages
}
// Don't trust index from source
val reIndexedPages = pages.mapIndexed { index, page ->
Page(
index,
page.url,
page.imageUrl,
page.uri,
)
}
download.pages = reIndexedPages
reIndexedPages
}
// Delete all temporary (unfinished) files
tmpDir.listFiles()
tmpDir
.listFiles()
?.filter { it.name!!.endsWith(".tmp") }
?.forEach { it.delete() }
@@ -395,14 +413,14 @@ class Downloader(
// Start downloading images, consider we can have downloaded images already
// Concurrently do 2 pages at a time
pageList.asFlow()
pageList
.asFlow()
.flatMapMerge(concurrency = 2) { page ->
flow {
withIOContext { getOrDownloadImage(page, download, tmpDir) }
emit(page)
}.flowOn(Dispatchers.IO)
}
.collect {
}.collect {
// Do when page is downloaded.
notifier.onProgressChange(download)
}
@@ -449,17 +467,19 @@ class Downloader(
val chapName = download.chapter.preferredChapterName(context, download.manga, preferences)
try {
// If the image is already downloaded, do nothing. Otherwise download from network
val file = when {
imageFile != null -> imageFile
chapterCache.isImageInCache(page.imageUrl!!) -> moveImageFromCache(
chapterCache.getImageFile(
page.imageUrl!!,
),
tmpDir,
filename,
)
else -> downloadImage(page, download.source, tmpDir, filename)
}
val file =
when {
imageFile != null -> imageFile
chapterCache.isImageInCache(page.imageUrl!!) ->
moveImageFromCache(
chapterCache.getImageFile(
page.imageUrl!!,
),
tmpDir,
filename,
)
else -> downloadImage(page, download.source, tmpDir, filename)
}
// When the page is ready, set page path, progress (just in case) and status
val success = splitTallImageIfNeeded(page, tmpDir)
@@ -520,8 +540,7 @@ class Downloader(
} else {
false
}
}
.first()
}.first()
}
/**
@@ -531,7 +550,11 @@ class Downloader(
* @param tmpDir the temporary directory of the download.
* @param filename the filename of the image.
*/
private fun moveImageFromCache(cacheFile: File, tmpDir: UniFile, filename: String): UniFile {
private fun moveImageFromCache(
cacheFile: File,
tmpDir: UniFile,
filename: String,
): UniFile {
val tmpFile = tmpDir.createFile("$filename.tmp")
cacheFile.inputStream().use { input ->
tmpFile.openOutputStream().use { output ->
@@ -551,25 +574,34 @@ class Downloader(
* @param response the network response of the image.
* @param file the file where the image is already downloaded.
*/
private fun getImageExtension(response: Response, file: UniFile): String {
private fun getImageExtension(
response: Response,
file: UniFile,
): String {
// Read content type if available.
val mime = response.body.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" }
// Else guess from the uri.
?: context.contentResolver.getType(file.uri)
// Else read magic numbers.
?: ImageUtil.findImageType { file.openInputStream() }?.mime
val mime =
response.body.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" }
// Else guess from the uri.
?: context.contentResolver.getType(file.uri)
// Else read magic numbers.
?: ImageUtil.findImageType { file.openInputStream() }?.mime
return ImageUtil.getExtensionFromMimeType(mime)
}
private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile): Boolean {
private fun splitTallImageIfNeeded(
page: Page,
tmpDir: UniFile,
): Boolean {
if (!preferences.splitTallImages().get()) return true
val filename = String.format("%03d", page.number)
val imageFile = tmpDir.listFiles()?.find { it.name!!.startsWith(filename) }
?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number))
val imageFilePath = imageFile.filePath
?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number))
val imageFile =
tmpDir.listFiles()?.find { it.name!!.startsWith(filename) }
?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number))
val imageFilePath =
imageFile.filePath
?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number))
// check if the original page was previously split before then skip.
if (imageFile.name!!.contains("__")) return true
@@ -601,19 +633,21 @@ class Downloader(
// Ensure that all pages has been downloaded
if (download.downloadedImages < downloadPageCount) return
// Ensure that the chapter folder has all the pages
val downloadedImagesCount = tmpDir.listFiles().orEmpty().count {
val fileName = it.name.orEmpty()
when {
fileName in listOf(/*COMIC_INFO_FILE, */NOMEDIA_FILE) -> false
fileName.endsWith(".tmp") -> false
// Only count the first split page and not the others
fileName.contains("__") && !fileName.endsWith("__001.jpg") -> false
else -> true
val downloadedImagesCount =
tmpDir.listFiles().orEmpty().count {
val fileName = it.name.orEmpty()
when {
fileName in listOf(/*COMIC_INFO_FILE, */NOMEDIA_FILE) -> false
fileName.endsWith(".tmp") -> false
// Only count the first split page and not the others
fileName.contains("__") && !fileName.endsWith("__001.jpg") -> false
else -> true
}
}
}
download.status = if (downloadedImagesCount == downloadPageCount) {
// TODO: Uncomment when #8537 is resolved
download.status =
if (downloadedImagesCount == downloadPageCount) {
// TODO: Uncomment when #8537 is resolved
// val chapterUrl = download.source.getChapterUrl(download.chapter)
// createComicInfoFile(
// tmpDir,
@@ -622,20 +656,20 @@ class Downloader(
// chapterUrl,
// )
// Only rename the directory if it's downloaded
if (preferences.saveChaptersAsCBZ().get()) {
archiveChapter(mangaDir, dirname, tmpDir)
// Only rename the directory if it's downloaded
if (preferences.saveChaptersAsCBZ().get()) {
archiveChapter(mangaDir, dirname, tmpDir)
} else {
tmpDir.renameTo(dirname)
}
cache.addChapter(dirname, download.manga)
DiskUtil.createNoMediaFile(tmpDir, context)
Download.State.DOWNLOADED
} else {
tmpDir.renameTo(dirname)
Download.State.ERROR
}
cache.addChapter(dirname, download.manga)
DiskUtil.createNoMediaFile(tmpDir, context)
Download.State.DOWNLOADED
} else {
Download.State.ERROR
}
}
/**
@@ -654,15 +688,17 @@ class Downloader(
img.openInputStream().use { input ->
val data = input.readBytes()
val size = img.length()
val entry = ZipEntry(img.name).apply {
val crc = CRC32().apply {
update(data)
}
setCrc(crc.value)
val entry =
ZipEntry(img.name).apply {
val crc =
CRC32().apply {
update(data)
}
setCrc(crc.value)
compressedSize = size
setSize(size)
}
compressedSize = size
setSize(size)
}
zipOut.putNextEntry(entry)
zipOut.write(data)
}
@@ -712,9 +748,7 @@ class Downloader(
/**
* Returns true if all the queued downloads are in DOWNLOADED or ERROR state.
*/
private fun areAllDownloadsFinished(): Boolean {
return queue.none { it.status <= Download.State.DOWNLOADING }
}
private fun areAllDownloadsFinished(): Boolean = queue.none { it.status <= Download.State.DOWNLOADING }
companion object {
const val TMP_DIR_SUFFIX = "_tmp"

View File

@@ -7,8 +7,11 @@ import eu.kanade.tachiyomi.source.online.HttpSource
import rx.subjects.PublishSubject
import kotlin.math.roundToInt
class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
class Download(
val source: HttpSource,
val manga: Manga,
val chapter: Chapter,
) {
var pages: List<Page>? = null
val totalProgress: Int

View File

@@ -13,9 +13,7 @@ import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue(
private val store: DownloadStore,
private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>(),
) :
List<Download> by queue {
) : List<Download> by queue {
private val statusSubject = PublishSubject.create<Download>()
private val updatedRelay = PublishRelay.create<Unit>()
@@ -59,7 +57,9 @@ class DownloadQueue(
}
fun remove(chapters: List<Chapter>) {
for (chapter in chapters) { remove(chapter) }
for (chapter in chapters) {
remove(chapter)
}
}
fun remove(manga: Manga) {
@@ -83,12 +83,13 @@ class DownloadQueue(
private fun setPagesFor(download: Download) {
if (download.status == Download.State.DOWNLOADING) {
if (download.pages != null) {
for (page in download.pages!!)
for (page in download.pages!!) {
scope.launch {
page.statusFlow.collectLatest {
callListeners(download)
}
}
}
}
callListeners(download)
} else if (download.status == Download.State.DOWNLOADED || download.status == Download.State.ERROR) {
@@ -123,6 +124,7 @@ class DownloadQueue(
interface DownloadListener {
fun updateDownload(download: Download)
fun updateDownloads()
}
}

View File

@@ -14,28 +14,33 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class CoilSetup(context: Context) {
class CoilSetup(
context: Context,
) {
init {
val imageLoader = ImageLoader.Builder(context).apply {
val callFactoryInit = { Injekt.get<NetworkHelper>().client }
val diskCacheInit = { CoilDiskCache.get(context) }
components {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
add(TachiyomiImageDecoder.Factory())
add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(MangaCoverKeyer())
}
callFactory(callFactoryInit)
diskCache(diskCacheInit)
memoryCache { MemoryCache.Builder(context).maxSizePercent(0.40).build() }
crossfade(true)
allowRgb565(context.getSystemService<ActivityManager>()!!.isLowRamDevice)
allowHardware(true)
}.build()
val imageLoader =
ImageLoader
.Builder(context)
.apply {
val callFactoryInit = { Injekt.get<NetworkHelper>().client }
val diskCacheInit = { CoilDiskCache.get(context) }
components {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
add(ImageDecoderDecoder.Factory())
} else {
add(GifDecoder.Factory())
}
add(TachiyomiImageDecoder.Factory())
add(MangaCoverFetcher.Factory(lazy(callFactoryInit), lazy(diskCacheInit)))
add(MangaCoverKeyer())
}
callFactory(callFactoryInit)
diskCache(diskCacheInit)
memoryCache { MemoryCache.Builder(context).maxSizePercent(0.40).build() }
crossfade(true)
allowRgb565(context.getSystemService<ActivityManager>()!!.isLowRamDevice)
allowHardware(true)
}.build()
Coil.setImageLoader(imageLoader)
}
}
@@ -44,19 +49,18 @@ class CoilSetup(context: Context) {
* Direct copy of Coil's internal SingletonDiskCache so that [MangaCoverFetcher] can access it.
*/
internal object CoilDiskCache {
private const val FOLDER_NAME = "image_cache"
private var instance: DiskCache? = null
@Synchronized
fun get(context: Context): DiskCache {
return instance ?: run {
fun get(context: Context): DiskCache =
instance ?: run {
val safeCacheDir = context.cacheDir.apply { mkdirs() }
// Create the singleton disk cache instance.
DiskCache.Builder()
DiskCache
.Builder()
.directory(safeCacheDir.resolve(FOLDER_NAME))
.build()
.also { instance = it }
}
}
}

View File

@@ -14,15 +14,15 @@ class CoverViewTarget(
val progress: View? = null,
val scaleType: ImageView.ScaleType = ImageView.ScaleType.CENTER_CROP,
) : ImageViewTarget(view) {
override fun onError(error: Drawable?) {
progress?.isVisible = false
view.scaleType = ImageView.ScaleType.CENTER
val vector = VectorDrawableCompat.create(
view.context.resources,
R.drawable.ic_broken_image_24dp,
null,
)
val vector =
VectorDrawableCompat.create(
view.context.resources,
R.drawable.ic_broken_image_24dp,
null,
)
vector?.setTint(view.context.getResourceColor(android.R.attr.textColorSecondary))
view.setImageDrawable(vector)
}

View File

@@ -19,7 +19,6 @@ class LibraryMangaImageTarget(
override val view: ImageView,
val manga: Manga,
) : ImageViewTarget(view) {
private val coverCache: CoverCache by injectLazy()
override fun onError(error: Drawable?) {
@@ -34,7 +33,8 @@ class LibraryMangaImageTarget(
BitmapFactory.decodeFile(file.path, options)
if (options.outWidth == -1 || options.outHeight == -1) {
file.delete()
view.context.imageLoader.memoryCache?.remove(MemoryCache.Key(manga.key()))
view.context.imageLoader.memoryCache
?.remove(MemoryCache.Key(manga.key()))
}
}
}
@@ -48,12 +48,14 @@ inline fun ImageView.loadManga(
imageLoader: ImageLoader = context.imageLoader,
builder: ImageRequest.Builder.() -> Unit = {},
): Disposable {
val request = ImageRequest.Builder(context)
.data(manga)
.target(LibraryMangaImageTarget(this, manga))
.apply(builder)
.memoryCacheKey(manga.key())
.build()
val request =
ImageRequest
.Builder(context)
.data(manga)
.target(LibraryMangaImageTarget(this, manga))
.apply(builder)
.memoryCacheKey(manga.key())
.build()
return imageLoader.enqueue(request)
}
@@ -66,12 +68,15 @@ fun Palette.getBestColor(): Int? {
val mutedSaturationLimit = if (mutedPopulation > vibPopulation * 3f) 0.1f else 0.25f
return when {
(dominantSwatch?.hsl?.get(1) ?: 0f) >= .25f &&
domLum <= .8f && domLum > .2f -> dominantSwatch?.rgb
domLum <= .8f &&
domLum > .2f -> dominantSwatch?.rgb
vibPopulation >= mutedPopulation * 0.75f -> vibrantSwatch?.rgb
mutedPopulation > vibPopulation * 1.5f &&
(mutedSwatch?.hsl?.get(1) ?: 0f) > mutedSaturationLimit -> mutedSwatch?.rgb
else -> arrayListOf(vibrantSwatch, lightVibrantSwatch, darkVibrantSwatch).maxByOrNull {
if (it === vibrantSwatch) (it?.population ?: -1) * 3 else it?.population ?: -1
}?.rgb
else ->
arrayListOf(vibrantSwatch, lightVibrantSwatch, darkVibrantSwatch)
.maxByOrNull {
if (it === vibrantSwatch) (it?.population ?: -1) * 3 else it?.population ?: -1
}?.rgb
}
}

View File

@@ -46,7 +46,6 @@ class MangaCoverFetcher(
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher {
// For non-custom cover
private val diskCacheKey: String? by lazy { MangaCoverKeyer().key(manga, options) }
private lateinit var url: String
@@ -156,11 +155,13 @@ class MangaCoverFetcher(
}
private fun newRequest(): Request {
val request = Request.Builder()
.url(url)
.headers(sourceLazy.value?.headers ?: options.headers)
// Support attaching custom data to the network request.
.tag(Parameters::class.java, options.parameters)
val request =
Request
.Builder()
.url(url)
.headers(sourceLazy.value?.headers ?: options.headers)
// Support attaching custom data to the network request.
.tag(Parameters::class.java, options.parameters)
val diskRead = options.diskCachePolicy.readEnabled
val networkRead = options.networkCachePolicy.readEnabled
@@ -170,11 +171,12 @@ class MangaCoverFetcher(
!networkRead && diskRead -> {
request.cacheControl(CacheControl.FORCE_CACHE)
}
networkRead && !diskRead -> if (options.diskCachePolicy.writeEnabled) {
request.cacheControl(CacheControl.FORCE_NETWORK)
} else {
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
networkRead && !diskRead ->
if (options.diskCachePolicy.writeEnabled) {
request.cacheControl(CacheControl.FORCE_NETWORK)
} else {
request.cacheControl(CACHE_CONTROL_FORCE_NETWORK_NO_CACHE)
}
!networkRead && !diskRead -> {
// This causes the request to fail with a 504 Unsatisfiable Request.
request.cacheControl(CACHE_CONTROL_NO_NETWORK_NO_CACHE)
@@ -184,7 +186,10 @@ class MangaCoverFetcher(
return request.build()
}
private fun moveSnapshotToCoverCache(snapshot: DiskCache.Snapshot, cacheFile: File?): File? {
private fun moveSnapshotToCoverCache(
snapshot: DiskCache.Snapshot,
cacheFile: File?,
): File? {
if (cacheFile == null) return null
return try {
diskCacheLazy.value.run {
@@ -200,7 +205,10 @@ class MangaCoverFetcher(
}
}
private fun writeResponseToCoverCache(response: Response, cacheFile: File?): File? {
private fun writeResponseToCoverCache(
response: Response,
cacheFile: File?,
): File? {
if (cacheFile == null || !options.diskCachePolicy.writeEnabled) return null
return try {
response.peekBody(Long.MAX_VALUE).source().use { input ->
@@ -213,7 +221,10 @@ class MangaCoverFetcher(
}
}
private fun writeSourceToCoverCache(input: Source, cacheFile: File) {
private fun writeSourceToCoverCache(
input: Source,
cacheFile: File,
) {
cacheFile.parentFile?.mkdirs()
cacheFile.delete()
try {
@@ -226,9 +237,8 @@ class MangaCoverFetcher(
}
}
private fun readFromDiskCache(): DiskCache.Snapshot? {
return if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
}
private fun readFromDiskCache(): DiskCache.Snapshot? =
if (options.diskCachePolicy.readEnabled) diskCacheLazy.value[diskCacheKey!!] else null
private fun writeToDiskCache(
snapshot: DiskCache.Snapshot?,
@@ -238,11 +248,12 @@ class MangaCoverFetcher(
snapshot?.closeQuietly()
return null
}
val editor = if (snapshot != null) {
snapshot.closeAndEdit()
} else {
diskCacheLazy.value.edit(diskCacheKey!!)
} ?: return null
val editor =
if (snapshot != null) {
snapshot.closeAndEdit()
} else {
diskCacheLazy.value.edit(diskCacheKey!!)
} ?: return null
try {
diskCacheLazy.value.fileSystem.write(editor.data) {
response.body!!.source().readAll(this)
@@ -257,11 +268,13 @@ class MangaCoverFetcher(
}
}
private fun DiskCache.Snapshot.toImageSource(): ImageSource {
return ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
}
private fun DiskCache.Snapshot.toImageSource(): ImageSource = ImageSource(file = data, diskCacheKey = diskCacheKey, closeable = this)
private fun setRatioAndColorsInScope(manga: Manga, ogFile: File? = null, force: Boolean = false) {
private fun setRatioAndColorsInScope(
manga: Manga,
ogFile: File? = null,
force: Boolean = false,
) {
fileScope.launch {
MangaCoverMetadata.setRatioAndColors(manga, ogFile, force)
}
@@ -273,54 +286,67 @@ class MangaCoverFetcher(
return null
}
val extension = url
.substringBeforeLast('#') // Strip the fragment.
.substringBeforeLast('?') // Strip the query.
.substringAfterLast('/') // Get the last path segment.
.substringAfterLast('.', missingDelimiterValue = "") // Get the file extension.
val extension =
url
.substringBeforeLast('#') // Strip the fragment.
.substringBeforeLast('?') // Strip the query.
.substringAfterLast('/') // Get the last path segment.
.substringAfterLast('.', missingDelimiterValue = "") // Get the file extension.
return getMimeTypeFromExtension(extension)
}
private fun fileLoader(file: File): FetchResult {
return SourceResult(
private fun fileLoader(file: File): FetchResult =
SourceResult(
source = ImageSource(file = file.toOkioPath(), diskCacheKey = diskCacheKey),
mimeType = "image/*",
dataSource = DataSource.DISK,
)
}
private fun getResourceType(cover: String?): Type? {
return when {
private fun getResourceType(cover: String?): Type? =
when {
cover.isNullOrEmpty() -> null
cover.startsWith("http") || cover.startsWith("Custom-", true) -> Type.URL
cover.startsWith("/") || cover.startsWith("file://") -> Type.File
else -> null
}
}
class Factory(
private val callFactoryLazy: Lazy<Call.Factory>,
private val diskCacheLazy: Lazy<DiskCache>,
) : Fetcher.Factory<Manga> {
private val coverCache: CoverCache by injectLazy()
private val sourceManager: SourceManager by injectLazy()
override fun create(data: Manga, options: Options, imageLoader: ImageLoader): Fetcher {
override fun create(
data: Manga,
options: Options,
imageLoader: ImageLoader,
): Fetcher {
val source = lazy { sourceManager.get(data.source) as? HttpSource }
return MangaCoverFetcher(data, source, options, coverCache, callFactoryLazy, diskCacheLazy)
}
}
private enum class Type {
File, URL;
File,
URL,
}
companion object {
const val useCustomCover = "use_custom_cover"
private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE = CacheControl.Builder().noCache().noStore().build()
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE = CacheControl.Builder().noCache().onlyIfCached().build()
private val CACHE_CONTROL_FORCE_NETWORK_NO_CACHE =
CacheControl
.Builder()
.noCache()
.noStore()
.build()
private val CACHE_CONTROL_NO_NETWORK_NO_CACHE =
CacheControl
.Builder()
.noCache()
.onlyIfCached()
.build()
}
}

View File

@@ -6,7 +6,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.storage.DiskUtil
class MangaCoverKeyer : Keyer<Manga> {
override fun key(data: Manga, options: Options): String? {
override fun key(
data: Manga,
options: Options,
): String? {
if (data.thumbnail_url.isNullOrBlank()) return null
return if (!data.favorite) {
data.thumbnail_url!!

View File

@@ -16,12 +16,15 @@ import tachiyomi.decoder.ImageDecoder
/**
* A [Decoder] that uses built-in [ImageDecoder] to decode images that is not supported by the system.
*/
class TachiyomiImageDecoder(private val resources: ImageSource, private val options: Options) : Decoder {
class TachiyomiImageDecoder(
private val resources: ImageSource,
private val options: Options,
) : Decoder {
override suspend fun decode(): DecodeResult {
val decoder = resources.sourceOrNull()?.use {
ImageDecoder.newInstance(it.inputStream())
}
val decoder =
resources.sourceOrNull()?.use {
ImageDecoder.newInstance(it.inputStream())
}
check(decoder != null && decoder.width > 0 && decoder.height > 0) { "Failed to initialize decoder." }
@@ -37,16 +40,20 @@ class TachiyomiImageDecoder(private val resources: ImageSource, private val opti
}
class Factory : Decoder.Factory {
override fun create(result: SourceResult, options: Options, imageLoader: ImageLoader): Decoder? {
override fun create(
result: SourceResult,
options: Options,
imageLoader: ImageLoader,
): Decoder? {
if (!isApplicable(result.source.source())) return null
return TachiyomiImageDecoder(result.source, options)
}
private fun isApplicable(source: BufferedSource): Boolean {
val type = source.peek().inputStream().use {
ImageUtil.findImageType(it)
}
val type =
source.peek().inputStream().use {
ImageUtil.findImageType(it)
}
return when (type) {
ImageUtil.ImageType.AVIF, ImageUtil.ImageType.JXL -> true
ImageUtil.ImageType.HEIF -> Build.VERSION.SDK_INT < Build.VERSION_CODES.O

View File

@@ -9,8 +9,9 @@ import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import java.io.File
class CustomMangaManager(val context: Context) {
class CustomMangaManager(
val context: Context,
) {
private val editJson = File(context.getExternalFilesDir(null), "edits.json")
private var customMangaMap = mutableMapOf<Long, Manga>()
@@ -20,8 +21,8 @@ class CustomMangaManager(val context: Context) {
}
companion object {
fun Manga.toJson(): MangaJson {
return MangaJson(
fun Manga.toJson(): MangaJson =
MangaJson(
id!!,
title,
author,
@@ -30,7 +31,6 @@ class CustomMangaManager(val context: Context) {
genre?.split(", ")?.toTypedArray(),
status.takeUnless { it == -1 },
)
}
}
fun getManga(manga: Manga): Manga? = customMangaMap[manga.id]
@@ -38,17 +38,21 @@ class CustomMangaManager(val context: Context) {
private fun fetchCustomData() {
if (!editJson.exists() || !editJson.isFile) return
val json = try {
Json.decodeFromString<MangaList>(editJson.bufferedReader().use { it.readText() })
} catch (e: Exception) {
null
} ?: return
val json =
try {
Json.decodeFromString<MangaList>(editJson.bufferedReader().use { it.readText() })
} catch (e: Exception) {
null
} ?: return
val mangasJson = json.mangas ?: return
customMangaMap = mangasJson.mapNotNull { mangaObject ->
val id = mangaObject.id ?: return@mapNotNull null
id to mangaObject.toManga()
}.toMap().toMutableMap()
customMangaMap =
mangasJson
.mapNotNull { mangaObject ->
val id = mangaObject.id ?: return@mapNotNull null
id to mangaObject.toManga()
}.toMap()
.toMutableMap()
}
fun saveMangaInfo(manga: MangaJson) {
@@ -90,16 +94,16 @@ class CustomMangaManager(val context: Context) {
val genre: Array<String>? = null,
val status: Int? = null,
) {
fun toManga() = MangaImpl().apply {
id = this@MangaJson.id
title = this@MangaJson.title ?: ""
author = this@MangaJson.author
artist = this@MangaJson.artist
description = this@MangaJson.description
genre = this@MangaJson.genre?.joinToString(", ")
status = this@MangaJson.status ?: -1
}
fun toManga() =
MangaImpl().apply {
id = this@MangaJson.id
title = this@MangaJson.title ?: ""
author = this@MangaJson.author
artist = this@MangaJson.artist
description = this@MangaJson.description
genre = this@MangaJson.genre?.joinToString(", ")
status = this@MangaJson.status ?: -1
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
@@ -109,8 +113,6 @@ class CustomMangaManager(val context: Context) {
return true
}
override fun hashCode(): Int {
return id.hashCode()
}
override fun hashCode(): Int = id.hashCode()
}
}

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