Merge remote-tracking branch 'upstream/master'

This commit is contained in:
jmir1 2022-01-04 19:24:06 +01:00
commit a3d060ff42
266 changed files with 3448 additions and 3072 deletions

View file

@ -13,9 +13,10 @@ jobs:
steps:
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0
uses: styfle/cancel-workflow-action@0.9.1
with:
access_token: ${{ github.token }}
all_but_latest: true
- name: Clone repo
uses: actions/checkout@v2

View file

@ -10,6 +10,7 @@ jobs:
cancel:
runs-on: ubuntu-latest
steps:
- uses: styfle/cancel-workflow-action@0.8.0
- uses: styfle/cancel-workflow-action@0.9.1
with:
all_but_latest: true
workflow_id: ${{ github.event.workflow.id }}

View file

@ -3,7 +3,7 @@ name: Lock threads
on:
# Daily
schedule:
- cron: '0 * * * *'
- cron: '0 0 * * *'
# Manual trigger
workflow_dispatch:
inputs:
@ -12,8 +12,8 @@ jobs:
lock:
runs-on: ubuntu-latest
steps:
- uses: dessant/lock-threads@v2
- uses: dessant/lock-threads@v3
with:
github-token: ${{ github.token }}
issue-lock-inactive-days: '2'
pr-lock-inactive-days: '2'
issue-inactive-days: '2'
pr-inactive-days: '2'

View file

@ -29,7 +29,7 @@ android {
applicationId = "xyz.jmir.tachiyomi.mi"
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
versionCode = 71
versionCode = 72
versionName = "0.12.3.5"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
@ -140,7 +140,7 @@ android {
dependencies {
implementation(kotlin("reflect", version = BuildPluginsVersion.KOTLIN))
val coroutinesVersion = "1.5.2"
val coroutinesVersion = "1.6.0"
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutinesVersion")
implementation("org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutinesVersion")
@ -148,13 +148,13 @@ dependencies {
implementation("org.tachiyomi:source-api:1.1")
// AndroidX libraries
implementation("androidx.annotation:annotation:1.3.0")
implementation("androidx.annotation:annotation:1.4.0-alpha01")
implementation("androidx.appcompat:appcompat:1.4.0")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha04")
implementation("androidx.browser:browser:1.4.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.2")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0-beta01")
implementation("androidx.core:core-ktx:1.8.0-alpha01")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.2.0-rc01")
implementation("androidx.core:core-ktx:1.8.0-alpha02")
implementation("androidx.core:core-splashscreen:1.0.0-alpha02")
implementation("androidx.recyclerview:recyclerview:1.3.0-alpha01")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
@ -185,11 +185,13 @@ dependencies {
implementation("org.conscrypt:conscrypt-android:2.5.2")
// Data serialization (JSON, protobuf)
val kotlinSerializationVersion = "1.3.1"
val kotlinSerializationVersion = "1.3.2"
implementation("org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlinSerializationVersion")
implementation("org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlinSerializationVersion")
// JavaScript engine
implementation("app.cash.quickjs:quickjs-android:0.9.2")
// TODO: remove Duktape once all extensions are using QuickJS
implementation("com.squareup.duktape:duktape-android:1.4.0")
// HTML parser
@ -201,13 +203,13 @@ dependencies {
implementation("com.github.junrar:junrar:7.4.0")
// Database
implementation("androidx.sqlite:sqlite-ktx:2.2.0-rc01")
implementation("androidx.sqlite:sqlite-ktx:2.2.0")
implementation("com.github.inorichi.storio:storio-common:8be19de@aar")
implementation("com.github.inorichi.storio:storio-sqlite:8be19de@aar")
implementation("com.github.requery:sqlite-android:3.36.0")
// Preferences
implementation("androidx.preference:preference-ktx:1.2.0-beta01")
implementation("androidx.preference:preference-ktx:1.2.0-rc01")
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.4.0")
// Model View Presenter
@ -261,9 +263,9 @@ dependencies {
implementation("com.squareup.logcat:logcat:0.1")
// Crash reports/analytics
implementation("ch.acra:acra-http:5.8.1")
"standardImplementation"("com.google.firebase:firebase-crashlytics-ktx:18.2.4")
"standardImplementation"("com.google.firebase:firebase-analytics-ktx:20.0.0")
implementation("ch.acra:acra-http:5.8.4")
"standardImplementation"("com.google.firebase:firebase-crashlytics-ktx:18.2.6")
"standardImplementation"("com.google.firebase:firebase-analytics-ktx:20.0.2")
// Licenses
implementation("com.mikepenz:aboutlibraries-core:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")

View file

@ -16,6 +16,8 @@
-keep,allowoptimization class com.google.gson.** { public protected *; }
-keep,allowoptimization class com.github.salomonbrys.kotson.** { public protected *; }
-keep,allowoptimization class com.squareup.duktape.** { public protected *; }
-keep,allowoptimization class app.cash.quickjs.** { public protected *; }
-keep,allowoptimization class uy.kohesive.injekt.** { public protected *; }
##---------------Begin: proguard configuration for RxJava 1.x ----------
-dontwarn sun.misc.**

View file

@ -1,93 +0,0 @@
package com.google.android.material.appbar
import android.animation.ValueAnimator
import android.view.View
import android.view.animation.DecelerateInterpolator
import androidx.appcompat.widget.Toolbar
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.view.marginTop
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.view.findChild
import kotlin.math.roundToLong
/**
* Hide toolbar on scroll behavior for [AppBarLayout].
*
* Inside this package to access some package-private methods.
*/
class HideToolbarOnScrollBehavior : AppBarLayout.Behavior() {
@ViewCompat.NestedScrollType
private var lastStartedType: Int = 0
private var offsetAnimator: ValueAnimator? = null
private var toolbarHeight: Int = 0
override fun onStartNestedScroll(
parent: CoordinatorLayout,
child: AppBarLayout,
directTargetChild: View,
target: View,
nestedScrollAxes: Int,
type: Int
): Boolean {
lastStartedType = type
offsetAnimator?.cancel()
return super.onStartNestedScroll(parent, child, directTargetChild, target, nestedScrollAxes, type)
}
override fun onStopNestedScroll(
parent: CoordinatorLayout,
layout: AppBarLayout,
target: View,
type: Int
) {
super.onStopNestedScroll(parent, layout, target, type)
if (toolbarHeight == 0) {
toolbarHeight = layout.findChild<Toolbar>()?.height ?: 0
}
if (lastStartedType == ViewCompat.TYPE_TOUCH || type == ViewCompat.TYPE_NON_TOUCH) {
animateToolbarVisibility(
parent,
layout,
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
)
}
}
override fun onFlingFinished(parent: CoordinatorLayout, layout: AppBarLayout) {
super.onFlingFinished(parent, layout)
animateToolbarVisibility(
parent,
layout,
getTopBottomOffsetForScrollingSibling(layout) > -toolbarHeight / 2
)
}
private fun getTopBottomOffsetForScrollingSibling(abl: AppBarLayout): Int {
return topBottomOffsetForScrollingSibling - abl.marginTop
}
private fun animateToolbarVisibility(
coordinatorLayout: CoordinatorLayout,
child: AppBarLayout,
isVisible: Boolean
) {
val current = getTopBottomOffsetForScrollingSibling(child)
val target = if (isVisible) 0 else -toolbarHeight
if (current == target) return
offsetAnimator?.cancel()
offsetAnimator = ValueAnimator().apply {
interpolator = DecelerateInterpolator()
duration = (150 * child.context.animatorDurationScale).roundToLong()
addUpdateListener {
setHeaderTopBottomOffset(coordinatorLayout, child, it.animatedValue as Int)
}
setIntValues(current, target)
start()
}
}
}

View file

@ -7,12 +7,10 @@ import android.content.Context
import android.util.AttributeSet
import android.widget.TextView
import androidx.annotation.FloatRange
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.lifecycle.coroutineScope
import androidx.lifecycle.findViewTreeLifecycleOwner
import com.google.android.material.animation.AnimationUtils
import com.google.android.material.shape.MaterialShapeDrawable
import com.google.android.material.shape.getStateAlpha
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.view.findChild
import kotlinx.coroutines.flow.launchIn
@ -53,7 +51,7 @@ class TachiyomiAppBarLayout @JvmOverloads constructor(
private val offsetListener = OnOffsetChangedListener { appBarLayout, verticalOffset ->
// Show status bar foreground when offset
val foreground = (appBarLayout?.statusBarForeground as? MaterialShapeDrawable) ?: return@OnOffsetChangedListener
val start = foreground.getStateAlpha()
val start = foreground.alpha
val end = if (verticalOffset != 0) 255 else 0
statusBarForegroundAnimator?.cancel()
@ -81,8 +79,6 @@ class TachiyomiAppBarLayout @JvmOverloads constructor(
}
}
override fun getBehavior(): CoordinatorLayout.Behavior<AppBarLayout> = HideToolbarOnScrollBehavior()
/**
* Disabled. Lift on scroll is handled manually with [eu.kanade.tachiyomi.widget.TachiyomiCoordinatorLayout]
*/
@ -154,7 +150,7 @@ class TachiyomiAppBarLayout @JvmOverloads constructor(
}
val transparent = if (lifted) false else isTransparentWhenNotLifted
val fromAlpha = (background as? MaterialShapeDrawable)?.getStateAlpha() ?: background.alpha
val fromAlpha = (background as? MaterialShapeDrawable)?.alpha ?: background.alpha
val toAlpha = if (transparent) 0 else 255
if (fromAlpha != toAlpha) {
ValueAnimator.ofInt(fromAlpha, toAlpha).apply {

View file

@ -1,10 +0,0 @@
package com.google.android.material.shape
/**
* Use this instead of [MaterialShapeDrawable.getAlpha].
*
* https://github.com/material-components/material-components-android/issues/1796
*/
fun MaterialShapeDrawable.getStateAlpha(): Int {
return (constantState as? MaterialShapeDrawable.MaterialShapeDrawableState)?.alpha ?: alpha
}

View file

@ -28,9 +28,9 @@ import eu.kanade.tachiyomi.data.coil.TachiyomiImageDecoder
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.system.logcat

View file

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi
/**
* Used by extensions.
*
* @since extension-lib 1.3
*/
object AppInfo {
fun getVersionCode() = BuildConfig.VERSION_CODE
fun getVersionName() = BuildConfig.VERSION_NAME
}

View file

@ -6,9 +6,9 @@ import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.data.animelib.AnimelibUpdateJob
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.AppUpdateJob
import eu.kanade.tachiyomi.extension.AnimeExtensionUpdateJob
@ -18,6 +18,8 @@ import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import uy.kohesive.injekt.Injekt
@ -54,6 +56,8 @@ object Migrations {
return false
}
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (oldVersion < 14) {
// Restore jobs after upgrading to Evernote's job scheduler.
if (BuildConfig.INCLUDE_UPDATER) {
@ -100,8 +104,6 @@ object Migrations {
}
if (oldVersion < 44) {
// Reset sorting preference if using removed sort by source
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
@Suppress("DEPRECATION")
@ -113,7 +115,6 @@ object Migrations {
}
if (oldVersion < 52) {
// Migrate library filters to tri-state versions
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
fun convertBooleanPrefToTriState(key: String): Int {
val oldPrefValue = prefs.getBoolean(key, false)
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
@ -142,7 +143,6 @@ object Migrations {
}
if (oldVersion < 57) {
// Migrate DNS over HTTPS setting
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
@ -153,7 +153,6 @@ object Migrations {
}
if (oldVersion < 59) {
// Reset rotation to Free after replacing Lock
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
if (prefs.contains("pref_rotation_type_key")) {
prefs.edit {
putInt("pref_rotation_type_key", 1)
@ -172,7 +171,6 @@ object Migrations {
}
// Migrate Rotation and Viewer values to default values for viewer_flags
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val newOrientation = when (prefs.getInt("pref_rotation_type_key", 1)) {
1 -> OrientationType.FREE.flagValue
2 -> OrientationType.PORTRAIT.flagValue
@ -216,8 +214,6 @@ object Migrations {
AnimelibUpdateJob.setupTask(context)
}
if (oldVersion < 64) {
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
@ -263,6 +259,13 @@ object Migrations {
AnimelibUpdateJob.setupTask(context, 12)
}
}
if (oldVersion < 72) {
val oldUpdateOngoingOnly = prefs.getBoolean("pref_update_only_non_completed_key", true)
if (!oldUpdateOngoingOnly) {
preferences.libraryUpdateMangaRestriction() -= MANGA_ONGOING
}
}
return true
}

View file

@ -28,7 +28,6 @@ import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.util.Locale
import java.util.concurrent.TimeUnit
class LocalAnimeSource(private val context: Context) : AnimeCatalogueSource, UnmeteredSource {
@ -104,9 +103,9 @@ class LocalAnimeSource(private val context: Context) : AnimeCatalogueSource, Unm
when (state?.index) {
0 -> {
animeDirs = if (state.ascending) {
animeDirs.sortedBy { it.name.lowercase(Locale.ENGLISH) }
animeDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
} else {
animeDirs.sortedByDescending { it.name.lowercase(Locale.ENGLISH) }
animeDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it.name }))
}
}
1 -> {
@ -144,7 +143,7 @@ class LocalAnimeSource(private val context: Context) : AnimeCatalogueSource, Unm
.asSequence()
.mapNotNull { File(it, anime.key).listFiles()?.toList() }
.flatten()
.firstOrNull { it.extension.lowercase() == "json" }
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
return if (localDetails != null) {
val obj = json.decodeFromStream<JsonObject>(localDetails.inputStream())
@ -203,7 +202,7 @@ class LocalAnimeSource(private val context: Context) : AnimeCatalogueSource, Unm
}
private fun isSupportedFile(extension: String): Boolean {
return extension.lowercase(Locale.ROOT) in SUPPORTED_FILE_TYPES
return extension.lowercase() in SUPPORTED_FILE_TYPES
}
fun getFormat(episode: SEpisode): Format {

View file

@ -1,6 +0,0 @@
package eu.kanade.tachiyomi.annotations
// TODO: remove this when no longer used in extensions
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Nsfw

View file

@ -8,8 +8,8 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.preference.CHARGING
import eu.kanade.tachiyomi.data.preference.ONLY_ON_WIFI
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import uy.kohesive.injekt.Injekt
@ -39,10 +39,10 @@ class AnimelibUpdateJob(private val context: Context, workerParams: WorkerParame
val preferences = Injekt.get<PreferencesHelper>()
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
if (interval > 0) {
val restrictions = preferences.libraryUpdateRestriction().get()
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(CHARGING in restrictions)
.setRequiresCharging(DEVICE_CHARGING in restrictions)
.build()
val request = PeriodicWorkRequestBuilder<AnimelibUpdateJob>(
@ -62,8 +62,8 @@ class AnimelibUpdateJob(private val context: Context, workerParams: WorkerParame
}
fun requiresWifiConnection(preferences: PreferencesHelper): Boolean {
val restrictions = preferences.libraryUpdateRestriction().get()
return ONLY_ON_WIFI in restrictions
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
return DEVICE_ONLY_ON_WIFI in restrictions
}
}
}

View file

@ -1,58 +0,0 @@
package eu.kanade.tachiyomi.data.animelib
import eu.kanade.tachiyomi.data.database.models.Anime
import java.util.Collections
import kotlin.math.abs
/**
* This class will provide various functions to rank manga to efficiently schedule manga to update.
*/
object AnimelibUpdateRanker {
val rankingScheme = listOf(
(this::lexicographicRanking)(),
(this::latestFirstRanking)(),
(this::nextFirstRanking)()
)
/**
* Provides a total ordering over all the Mangas.
*
* Orders the manga based on the distance between the next expected update and now.
* The comparator is reversed, placing the smallest (and thus closest to updating now) first.
*/
fun nextFirstRanking(): Comparator<Anime> {
val time = System.currentTimeMillis()
return Collections.reverseOrder(
Comparator { animeFirst: Anime,
animeSecond: Anime ->
compareValues(abs(animeSecond.next_update - time), abs(animeFirst.next_update - time))
}
)
}
/**
* Provides a total ordering over all the [Manga]s.
*
* Assumption: An active [Manga] mActive is expected to have been last updated after an
* inactive [Manga] mInactive.
*
* Using this insight, function returns a Comparator for which mActive appears before mInactive.
* @return a Comparator that ranks manga based on relevance.
*/
private fun latestFirstRanking(): Comparator<Anime> =
Comparator { first: Anime, second: Anime ->
compareValues(second.last_update, first.last_update)
}
/**
* Provides a total ordering over all the [Anime]s.
*
* Order the manga lexicographically.
* @return a Comparator that ranks manga lexicographically based on the title.
*/
private fun lexicographicRanking(): Comparator<Anime> =
Comparator { first: Anime, second: Anime ->
compareValues(first.title, second.title)
}
}

View file

@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.animesource.model.SAnime
import eu.kanade.tachiyomi.animesource.model.toSAnime
import eu.kanade.tachiyomi.animesource.model.toSEpisode
import eu.kanade.tachiyomi.data.animelib.AnimelibUpdateRanker.rankingScheme
import eu.kanade.tachiyomi.data.animelib.AnimelibUpdateService.Companion.start
import eu.kanade.tachiyomi.data.cache.AnimeCoverCache
import eu.kanade.tachiyomi.data.database.AnimeDatabaseHelper
@ -24,6 +23,8 @@ import eu.kanade.tachiyomi.data.database.models.toAnimeInfo
import eu.kanade.tachiyomi.data.download.AnimeDownloadManager
import eu.kanade.tachiyomi.data.download.AnimeDownloadService
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.MANGA_FULLY_READ
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
@ -258,14 +259,20 @@ class AnimelibUpdateService(
listToInclude.minus(listToExclude)
}
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
if (target == Target.CHAPTERS) {
val restrictions = preferences.libraryUpdateMangaRestriction().get()
if (MANGA_ONGOING in restrictions) {
listToUpdate = listToUpdate.filterNot { it.status == SAnime.COMPLETED }
}
if (MANGA_FULLY_READ in restrictions) {
listToUpdate = listToUpdate.filter { it.unseen == 0 }
}
}
val selectedScheme = preferences.libraryUpdatePrioritization().get()
animeToUpdate = listToUpdate
.distinctBy { it.id }
.sortedWith(rankingScheme[selectedScheme])
.sortedBy { it.title }
// Warn when excessively checking a single source
val maxUpdatesFromSource = animeToUpdate
@ -545,6 +552,7 @@ class AnimelibUpdateService(
if (errors.isNotEmpty()) {
val file = createFileInCacheDir("aniyomi_update_errors.txt")
file.bufferedWriter().use { out ->
out.write(getString(R.string.library_errors_help, ERROR_LOG_HELP_URL) + "\n\n")
// Error file format:
// ! Error
// # Source
@ -570,3 +578,4 @@ class AnimelibUpdateService(
}
private const val ANIME_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
private const val ERROR_LOG_HELP_URL = "https://aniyomi.jmir.xyz/help/guides/troubleshooting"

View file

@ -20,7 +20,7 @@ class AnimeDbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/**
* Version of the database.
*/
const val DATABASE_VERSION = 113
const val DATABASE_VERSION = 114
}
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@ -91,6 +91,9 @@ class AnimeDbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
db.execSQL(AnimeTrackTable.insertFromTempTable)
db.execSQL(AnimeTrackTable.dropTempTable)
}
if (oldVersion < 14) {
db.execSQL(EpisodeTable.fixDateUploadIfNeeded)
}
}
override fun onConfigure(db: SupportSQLiteDatabase) {

View file

@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/**
* Version of the database.
*/
const val DATABASE_VERSION = 13
const val DATABASE_VERSION = 14
}
override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@ -91,6 +91,9 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
db.execSQL(TrackTable.insertFromTempTable)
db.execSQL(TrackTable.dropTempTable)
}
if (oldVersion < 14) {
db.execSQL(ChapterTable.fixDateUploadIfNeeded)
}
}
override fun onConfigure(db: SupportSQLiteDatabase) {

View file

@ -44,9 +44,9 @@ class AnimeCategoryPutResolver : DefaultPutResolver<AnimeCategory>() {
class AnimeCategoryGetResolver : DefaultGetResolver<AnimeCategory>() {
override fun mapFromCursor(cursor: Cursor): AnimeCategory = AnimeCategory().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
anime_id = cursor.getLong(cursor.getColumnIndex(COL_ANIME_ID))
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
anime_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ANIME_ID))
category_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_CATEGORY_ID))
}
}

View file

@ -47,10 +47,10 @@ open class AnimeHistoryPutResolver : DefaultPutResolver<AnimeHistory>() {
class AnimeHistoryGetResolver : DefaultGetResolver<AnimeHistory>() {
override fun mapFromCursor(cursor: Cursor): AnimeHistory = AnimeHistoryImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
episode_id = cursor.getLong(cursor.getColumnIndex(COL_EPISODE_ID))
last_seen = cursor.getLong(cursor.getColumnIndex(COL_LAST_SEEN))
time_seen = cursor.getLong(cursor.getColumnIndex(COL_TIME_SEEN))
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
episode_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_EPISODE_ID))
last_seen = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_SEEN))
time_seen = cursor.getLong(cursor.getColumnIndexOrThrow(COL_TIME_SEEN))
}
}

View file

@ -65,19 +65,19 @@ class AnimeTrackPutResolver : DefaultPutResolver<AnimeTrack>() {
class AnimeTrackGetResolver : DefaultGetResolver<AnimeTrack>() {
override fun mapFromCursor(cursor: Cursor): AnimeTrack = AnimeTrackImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
anime_id = cursor.getLong(cursor.getColumnIndex(COL_ANIME_ID))
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
last_episode_seen = cursor.getFloat(cursor.getColumnIndex(COL_LAST_EPISODE_SEEN))
total_episodes = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_EPISODES))
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
started_watching_date = cursor.getLong(cursor.getColumnIndex(COL_START_DATE))
finished_watching_date = cursor.getLong(cursor.getColumnIndex(COL_FINISH_DATE))
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
anime_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ANIME_ID))
sync_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SYNC_ID))
media_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_MEDIA_ID))
library_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LIBRARY_ID))
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
last_episode_seen = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_LAST_EPISODE_SEEN))
total_episodes = cursor.getInt(cursor.getColumnIndexOrThrow(COL_TOTAL_EPISODES))
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_SCORE))
tracking_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_TRACKING_URL))
started_watching_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_START_DATE))
finished_watching_date = cursor.getLong(cursor.getColumnIndexOrThrow(COL_FINISH_DATE))
}
}

View file

@ -29,7 +29,6 @@ import eu.kanade.tachiyomi.data.database.tables.AnimeTable.COL_TITLE
import eu.kanade.tachiyomi.data.database.tables.AnimeTable.COL_URL
import eu.kanade.tachiyomi.data.database.tables.AnimeTable.COL_VIEWER
import eu.kanade.tachiyomi.data.database.tables.AnimeTable.TABLE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_NEXT_UPDATE
class AnimeTypeMapping : SQLiteTypeMapping<Anime>(
AnimePutResolver(),
@ -63,7 +62,6 @@ class AnimePutResolver : DefaultPutResolver<Anime>() {
COL_THUMBNAIL_URL to obj.thumbnail_url,
COL_FAVORITE to obj.favorite,
COL_LAST_UPDATE to obj.last_update,
COL_NEXT_UPDATE to obj.next_update,
COL_INITIALIZED to obj.initialized,
COL_VIEWER to obj.viewer_flags,
COL_EPISODE_FLAGS to obj.episode_flags,
@ -74,24 +72,23 @@ class AnimePutResolver : DefaultPutResolver<Anime>() {
interface BaseAnimeGetResolver {
fun mapBaseFromCursor(anime: Anime, cursor: Cursor) = anime.apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
next_update = cursor.getLong(cursor.getColumnIndex(COL_NEXT_UPDATE))
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
episode_flags = cursor.getInt(cursor.getColumnIndex(COL_EPISODE_FLAGS))
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
source = cursor.getLong(cursor.getColumnIndexOrThrow(COL_SOURCE))
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
artist = cursor.getString(cursor.getColumnIndexOrThrow(COL_ARTIST))
author = cursor.getString(cursor.getColumnIndexOrThrow(COL_AUTHOR))
description = cursor.getString(cursor.getColumnIndexOrThrow(COL_DESCRIPTION))
genre = cursor.getString(cursor.getColumnIndexOrThrow(COL_GENRE))
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
thumbnail_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_THUMBNAIL_URL))
favorite = cursor.getInt(cursor.getColumnIndexOrThrow(COL_FAVORITE)) == 1
last_update = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_UPDATE))
initialized = cursor.getInt(cursor.getColumnIndexOrThrow(COL_INITIALIZED)) == 1
viewer_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_VIEWER))
episode_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_EPISODE_FLAGS))
cover_last_modified = cursor.getLong(cursor.getColumnIndexOrThrow(COL_COVER_LAST_MODIFIED))
date_added = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_ADDED))
}
}

View file

@ -47,10 +47,10 @@ class CategoryPutResolver : DefaultPutResolver<Category>() {
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))
id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_ID))
name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
order = cursor.getInt(cursor.getColumnIndexOrThrow(COL_ORDER))
flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_FLAGS))
}
}

View file

@ -63,18 +63,18 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
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))
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
scanlator = cursor.getString(cursor.getColumnIndexOrThrow(COL_SCANLATOR))
read = cursor.getInt(cursor.getColumnIndexOrThrow(COL_READ)) == 1
bookmark = cursor.getInt(cursor.getColumnIndexOrThrow(COL_BOOKMARK)) == 1
date_fetch = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_FETCH))
date_upload = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_UPLOAD))
last_page_read = cursor.getInt(cursor.getColumnIndexOrThrow(COL_LAST_PAGE_READ))
chapter_number = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_CHAPTER_NUMBER))
source_order = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SOURCE_ORDER))
}
}

View file

@ -65,19 +65,19 @@ class EpisodePutResolver : DefaultPutResolver<Episode>() {
class EpisodeGetResolver : DefaultGetResolver<Episode>() {
override fun mapFromCursor(cursor: Cursor): Episode = EpisodeImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
anime_id = cursor.getLong(cursor.getColumnIndex(COL_ANIME_ID))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
seen = cursor.getInt(cursor.getColumnIndex(COL_SEEN)) == 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_second_seen = cursor.getLong(cursor.getColumnIndex(COL_LAST_SECOND_SEEN))
total_seconds = cursor.getLong(cursor.getColumnIndex(COL_TOTAL_SECONDS))
episode_number = cursor.getFloat(cursor.getColumnIndex(COL_EPISODE_NUMBER))
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
anime_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ANIME_ID))
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
name = cursor.getString(cursor.getColumnIndexOrThrow(COL_NAME))
scanlator = cursor.getString(cursor.getColumnIndexOrThrow(COL_SCANLATOR))
seen = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SEEN)) == 1
bookmark = cursor.getInt(cursor.getColumnIndexOrThrow(COL_BOOKMARK)) == 1
date_fetch = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_FETCH))
date_upload = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_UPLOAD))
last_second_seen = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_SECOND_SEEN))
total_seconds = cursor.getLong(cursor.getColumnIndexOrThrow(COL_TOTAL_SECONDS))
episode_number = cursor.getFloat(cursor.getColumnIndexOrThrow(COL_EPISODE_NUMBER))
source_order = cursor.getInt(cursor.getColumnIndexOrThrow(COL_SOURCE_ORDER))
}
}

View file

@ -47,10 +47,10 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
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))
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
chapter_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_CHAPTER_ID))
last_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_READ))
time_read = cursor.getLong(cursor.getColumnIndexOrThrow(COL_TIME_READ))
}
}

View file

@ -44,9 +44,9 @@ class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
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))
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_MANGA_ID))
category_id = cursor.getInt(cursor.getColumnIndexOrThrow(COL_CATEGORY_ID))
}
}

View file

@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_NEXT_UPDATE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
@ -63,7 +62,6 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
COL_THUMBNAIL_URL to obj.thumbnail_url,
COL_FAVORITE to obj.favorite,
COL_LAST_UPDATE to obj.last_update,
COL_NEXT_UPDATE to obj.next_update,
COL_INITIALIZED to obj.initialized,
COL_VIEWER to obj.viewer_flags,
COL_CHAPTER_FLAGS to obj.chapter_flags,
@ -74,24 +72,23 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
interface BaseMangaGetResolver {
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))
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
next_update = cursor.getLong(cursor.getColumnIndex(COL_NEXT_UPDATE))
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
viewer_flags = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
id = cursor.getLong(cursor.getColumnIndexOrThrow(COL_ID))
source = cursor.getLong(cursor.getColumnIndexOrThrow(COL_SOURCE))
url = cursor.getString(cursor.getColumnIndexOrThrow(COL_URL))
artist = cursor.getString(cursor.getColumnIndexOrThrow(COL_ARTIST))
author = cursor.getString(cursor.getColumnIndexOrThrow(COL_AUTHOR))
description = cursor.getString(cursor.getColumnIndexOrThrow(COL_DESCRIPTION))
genre = cursor.getString(cursor.getColumnIndexOrThrow(COL_GENRE))
title = cursor.getString(cursor.getColumnIndexOrThrow(COL_TITLE))
status = cursor.getInt(cursor.getColumnIndexOrThrow(COL_STATUS))
thumbnail_url = cursor.getString(cursor.getColumnIndexOrThrow(COL_THUMBNAIL_URL))
favorite = cursor.getInt(cursor.getColumnIndexOrThrow(COL_FAVORITE)) == 1
last_update = cursor.getLong(cursor.getColumnIndexOrThrow(COL_LAST_UPDATE))
initialized = cursor.getInt(cursor.getColumnIndexOrThrow(COL_INITIALIZED)) == 1
viewer_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_VIEWER))
chapter_flags = cursor.getInt(cursor.getColumnIndexOrThrow(COL_CHAPTER_FLAGS))
cover_last_modified = cursor.getLong(cursor.getColumnIndexOrThrow(COL_COVER_LAST_MODIFIED))
date_added = cursor.getLong(cursor.getColumnIndexOrThrow(COL_DATE_ADDED))
}
}

View file

@ -65,19 +65,19 @@ class TrackPutResolver : DefaultPutResolver<Track>() {
class TrackGetResolver : DefaultGetResolver<Track>() {
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
media_id = cursor.getInt(cursor.getColumnIndex(COL_MEDIA_ID))
library_id = cursor.getLong(cursor.getColumnIndex(COL_LIBRARY_ID))
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
last_chapter_read = cursor.getFloat(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
started_reading_date = cursor.getLong(cursor.getColumnIndex(COL_START_DATE))
finished_reading_date = cursor.getLong(cursor.getColumnIndex(COL_FINISH_DATE))
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.getInt(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))
}
}

View file

@ -14,9 +14,6 @@ interface Anime : SAnime {
// last time the episode list changed in any way
var last_update: Long
// predicted next update time based on latest (by date) 4 episodes' deltas
var next_update: Long
var date_added: Long
var viewer_flags: Int

View file

@ -26,8 +26,6 @@ open class AnimeImpl : Anime {
override var last_update: Long = 0
override var next_update: Long = 0
override var date_added: Long = 0
override var initialized: Boolean = false

View file

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.database.models
class AnimelibAnime : AnimeImpl() {
var unread: Int = 0
var unseen: Int = 0
var category: Int = 0
}

View file

@ -16,9 +16,6 @@ interface Manga : SManga {
// last time the chapter list changed in any way
var last_update: Long
// predicted next update time based on latest (by date) 4 chapters' deltas
var next_update: Long
var date_added: Long
var viewer_flags: Int

View file

@ -26,8 +26,6 @@ open class MangaImpl : Manga {
override var last_update: Long = 0
override var next_update: Long = 0
override var date_added: Long = 0
override var initialized: Boolean = false

View file

@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.database.resolvers.AnimeCoverLastModifiedPutReso
import eu.kanade.tachiyomi.data.database.resolvers.AnimeFavoritePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.AnimeFlagsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.AnimeLastUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.AnimeNextUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.AnimeTitlePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.AnimelibAnimeGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdAnimeCountGetResolver
@ -108,11 +107,6 @@ interface AnimeQueries : DbProvider {
.withPutResolver(AnimeFlagsPutResolver(AnimeTable.COL_VIEWER, Anime::viewer_flags))
.prepare()
fun updateNextUpdated(manga: Anime) = db.put()
.`object`(manga)
.withPutResolver(AnimeNextUpdatedPutResolver())
.prepare()
fun updateLastUpdated(anime: Anime) = db.put()
.`object`(anime)
.withPutResolver(AnimeLastUpdatedPutResolver())

View file

@ -14,7 +14,6 @@ import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutReso
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaNextUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaTitlePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.SourceIdMangaCountGetResolver
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
@ -108,11 +107,6 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaFlagsPutResolver(MangaTable.COL_VIEWER, Manga::viewer_flags))
.prepare()
fun updateNextUpdated(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaNextUpdatedPutResolver())
.prepare()
fun updateLastUpdated(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaLastUpdatedPutResolver())

View file

@ -20,7 +20,7 @@ class AnimeEpisodeGetResolver : DefaultGetResolver<AnimeEpisode>() {
val anime = animeGetResolver.mapFromCursor(cursor)
val episode = episodeGetResolver.mapFromCursor(cursor)
anime.id = episode.anime_id
anime.url = cursor.getString(cursor.getColumnIndex("animeUrl"))
anime.url = cursor.getString(cursor.getColumnIndexOrThrow("animeUrl"))
return AnimeEpisode(anime, episode)
}

View file

@ -42,7 +42,7 @@ class AnimeEpisodeHistoryGetResolver : DefaultGetResolver<AnimeEpisodeHistory>()
// Make certain column conflicts are dealt with
anime.id = episode.anime_id
anime.url = cursor.getString(cursor.getColumnIndex("animeUrl"))
anime.url = cursor.getString(cursor.getColumnIndexOrThrow("animeUrl"))
episode.id = history.episode_id
// Return result

View file

@ -1,31 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.tables.AnimeTable
class AnimeNextUpdatedPutResolver : PutResolver<Anime>() {
override fun performPut(db: StorIOSQLite, anime: Anime) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(anime)
val contentValues = mapToContentValues(anime)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(anime: Anime) = UpdateQuery.builder()
.table(AnimeTable.TABLE)
.where("${AnimeTable.COL_ID} = ?")
.whereArgs(anime.id)
.build()
fun mapToContentValues(anime: Anime) = contentValuesOf(
AnimeTable.COL_NEXT_UPDATE to anime.next_update
)
}

View file

@ -16,8 +16,8 @@ class AnimelibAnimeGetResolver : DefaultGetResolver<AnimelibAnime>(), BaseAnimeG
val manga = AnimelibAnime()
mapBaseFromCursor(manga, cursor)
manga.unread = cursor.getInt(cursor.getColumnIndex(AnimeTable.COL_UNREAD))
manga.category = cursor.getInt(cursor.getColumnIndex(AnimeTable.COL_CATEGORY))
manga.unseen = cursor.getInt(cursor.getColumnIndexOrThrow(AnimeTable.COL_UNREAD))
manga.category = cursor.getInt(cursor.getColumnIndexOrThrow(AnimeTable.COL_CATEGORY))
return manga
}

View file

@ -16,8 +16,8 @@ class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGet
val manga = LibraryManga()
mapBaseFromCursor(manga, cursor)
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
manga.unread = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_UNREAD))
manga.category = cursor.getInt(cursor.getColumnIndexOrThrow(MangaTable.COL_CATEGORY))
return manga
}

View file

@ -20,7 +20,7 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
val manga = mangaGetResolver.mapFromCursor(cursor)
val chapter = chapterGetResolver.mapFromCursor(cursor)
manga.id = chapter.manga_id
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
manga.url = cursor.getString(cursor.getColumnIndexOrThrow("mangaUrl"))
return MangaChapter(manga, chapter)
}

View file

@ -42,7 +42,7 @@ class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>()
// Make certain column conflicts are dealt with
manga.id = chapter.manga_id
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
manga.url = cursor.getString(cursor.getColumnIndexOrThrow("mangaUrl"))
chapter.id = history.chapter_id
// Return result

View file

@ -1,31 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaNextUpdatedPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = contentValuesOf(
MangaTable.COL_NEXT_UPDATE to manga.next_update
)
}

View file

@ -15,8 +15,8 @@ class SourceIdAnimeCountGetResolver : DefaultGetResolver<SourceIdAnimeCount>() {
@SuppressLint("Range")
override fun mapFromCursor(cursor: Cursor): SourceIdAnimeCount {
val sourceID = cursor.getLong(cursor.getColumnIndex(AnimeTable.COL_SOURCE))
val count = cursor.getInt(cursor.getColumnIndex(COL_COUNT))
val sourceID = cursor.getLong(cursor.getColumnIndexOrThrow(AnimeTable.COL_SOURCE))
val count = cursor.getInt(cursor.getColumnIndexOrThrow(COL_COUNT))
return SourceIdAnimeCount(sourceID, count)
}

View file

@ -15,8 +15,8 @@ class SourceIdMangaCountGetResolver : DefaultGetResolver<SourceIdMangaCount>() {
@SuppressLint("Range")
override fun mapFromCursor(cursor: Cursor): SourceIdMangaCount {
val sourceID = cursor.getLong(cursor.getColumnIndex(MangaTable.COL_SOURCE))
val count = cursor.getInt(cursor.getColumnIndex(COL_COUNT))
val sourceID = cursor.getLong(cursor.getColumnIndexOrThrow(MangaTable.COL_SOURCE))
val count = cursor.getInt(cursor.getColumnIndexOrThrow(COL_COUNT))
return SourceIdMangaCount(sourceID, count)
}

View file

@ -26,6 +26,7 @@ object AnimeTable {
const val COL_FAVORITE = "favorite"
// Not actually used anymore
const val COL_LAST_UPDATE = "last_update"
const val COL_NEXT_UPDATE = "next_update"

View file

@ -62,4 +62,7 @@ object ChapterTable {
val addScanlator: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
val fixDateUploadIfNeeded: String
get() = "UPDATE $TABLE SET $COL_DATE_UPLOAD = $COL_DATE_FETCH WHERE $COL_DATE_UPLOAD = 0"
}

View file

@ -65,4 +65,7 @@ object EpisodeTable {
val addScanlator: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
val fixDateUploadIfNeeded: String
get() = "UPDATE $TABLE SET $COL_DATE_UPLOAD = $COL_DATE_FETCH WHERE $COL_DATE_UPLOAD = 0"
}

View file

@ -28,6 +28,7 @@ object MangaTable {
const val COL_LAST_UPDATE = "last_update"
// Not actually used anymore
const val COL_NEXT_UPDATE = "next_update"
const val COL_DATE_ADDED = "date_added"

View file

@ -206,14 +206,13 @@ internal class AnimeDownloadNotifier(private val context: Context) {
* being overwritten.
*
* @param error string containing error information.
* @param chapter string containing chapter title.
* @param episode string containing episode title.
*/
fun onError(error: String? = null, chapter: String? = null) {
fun onError(error: String? = null, episode: String? = null, animeTitle: String? = null) {
// Create notification
with(errorNotificationBuilder) {
setContentTitle(
chapter
?: context.getString(R.string.download_notifier_downloader_title)
animeTitle?.plus(": $episode") ?: context.getString(R.string.download_notifier_downloader_title)
)
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
setSmallIcon(R.drawable.ic_warning_white_24dp)

View file

@ -179,7 +179,9 @@ class AnimeDownloadService : Service() {
* Listens to downloader status. Enables or disables the wake lock depending on the status.
*/
private fun listenDownloaderState() {
subscriptions += downloadManager.runningRelay.subscribe { running ->
subscriptions += downloadManager.runningRelay
.doOnError { /* Swallow wakelock error */ }
.subscribe { running ->
if (running) {
wakeLock.acquireIfNeeded()
} else {

View file

@ -143,7 +143,7 @@ class DownloadCache(
mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles()
.orEmpty()
.mapNotNull { it.name }
.mapNotNull { it.name?.replace(".cbz", "") }
.toHashSet()
mangaDir.files = chapterDirs

View file

@ -39,7 +39,7 @@ class DownloadManager(
/**
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
*/
private val provider = DownloadProvider(context)
val provider = DownloadProvider(context)
/**
* Cache of downloaded chapters.
@ -327,15 +327,19 @@ class DownloadManager(
*/
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
val oldNames = provider.getValidChapterDirNames(oldChapter)
val newName = provider.getChapterDirName(newChapter)
val mangaDir = provider.getMangaDir(manga, source)
// Assume there's only 1 version of the chapter name formats present
val oldFolder = oldNames.asSequence()
val oldDownload = oldNames.asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull()
.firstOrNull() ?: return
if (oldFolder?.renameTo(newName) == true) {
var newName = provider.getChapterDirName(newChapter)
if (oldDownload.isFile && oldDownload.name?.endsWith(".cbz") == true) {
newName += ".cbz"
}
if (oldDownload.renameTo(newName)) {
cache.removeChapter(oldChapter, manga)
cache.addChapter(newName, mangaDir, manga)
} else {

View file

@ -209,12 +209,11 @@ internal class DownloadNotifier(private val context: Context) {
* @param error string containing error information.
* @param chapter string containing chapter title.
*/
fun onError(error: String? = null, chapter: String? = null) {
fun onError(error: String? = null, chapter: String? = null, mangaTitle: String? = null) {
// Create notification
with(errorNotificationBuilder) {
setContentTitle(
chapter
?: context.getString(R.string.download_notifier_downloader_title)
mangaTitle?.plus(": $chapter") ?: context.getString(R.string.download_notifier_downloader_title)
)
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
setSmallIcon(R.drawable.ic_warning_white_24dp)

View file

@ -148,10 +148,14 @@ class DownloadProvider(private val context: Context) {
* @param chapter the chapter to query.
*/
fun getValidChapterDirNames(chapter: Chapter): List<String> {
val chapterName = getChapterDirName(chapter)
return listOf(
getChapterDirName(chapter),
// Folder of images
chapterName,
// Archived chapters
"$chapterName.cbz",
// TODO: remove this
// Legacy chapter directory name used in v0.9.2 and before
DiskUtil.buildValidFilename(chapter.name)
)

View file

@ -176,7 +176,9 @@ class DownloadService : Service() {
* Listens to downloader status. Enables or disables the wake lock depending on the status.
*/
private fun listenDownloaderState() {
subscriptions += downloadManager.runningRelay.subscribe { running ->
subscriptions += downloadManager.runningRelay
.doOnError { /* Swallow wakelock error */ }
.subscribe { running ->
if (running) {
wakeLock.acquireIfNeeded()
} else {

View file

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.Page
@ -35,7 +36,11 @@ import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy
import java.io.BufferedOutputStream
import java.io.File
import java.util.zip.CRC32
import java.util.zip.ZipEntry
import java.util.zip.ZipOutputStream
/**
* This class is the one in charge of downloading chapters.
@ -60,6 +65,8 @@ class Downloader(
private val chapterCache: ChapterCache by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
/**
* Store for persisting downloads across restarts.
*/
@ -292,7 +299,7 @@ class Downloader(
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
download.status = Download.State.ERROR
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name, download.manga.title)
return@defer Observable.just(download)
}
@ -338,7 +345,7 @@ class Downloader(
// If the page list threw, it will resume here
.onErrorReturn { error ->
download.status = Download.State.ERROR
notifier.onError(error.message, download.chapter.name)
notifier.onError(error.message, download.chapter.name, download.manga.title)
download
}
}
@ -484,13 +491,51 @@ class Downloader(
// Only rename the directory if it's downloaded.
if (download.status == Download.State.DOWNLOADED) {
if (preferences.saveChaptersAsCBZ().get()) {
archiveChapter(mangaDir, dirname, tmpDir)
} else {
tmpDir.renameTo(dirname)
}
cache.addChapter(dirname, mangaDir, download.manga)
DiskUtil.createNoMediaFile(tmpDir, context)
}
}
/**
* Archive the chapter pages as a CBZ.
*/
private fun archiveChapter(
mangaDir: UniFile,
dirname: String,
tmpDir: UniFile,
) {
val zip = mangaDir.createFile("$dirname.cbz.tmp")
ZipOutputStream(BufferedOutputStream(zip.openOutputStream())).use { zipOut ->
zipOut.setMethod(ZipEntry.STORED)
tmpDir.listFiles()?.forEach { img ->
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)
compressedSize = size
setSize(size)
}
zipOut.putNextEntry(entry)
zipOut.write(data)
}
}
}
zip.renameTo("$dirname.cbz")
tmpDir.delete()
}
/**
* Completes a download. This method is called in the main thread.
*/

View file

@ -8,8 +8,8 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.preference.CHARGING
import eu.kanade.tachiyomi.data.preference.ONLY_ON_WIFI
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import uy.kohesive.injekt.Injekt
@ -39,10 +39,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
val preferences = Injekt.get<PreferencesHelper>()
val interval = prefInterval ?: preferences.libraryUpdateInterval().get()
if (interval > 0) {
val restrictions = preferences.libraryUpdateRestriction().get()
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiresCharging(CHARGING in restrictions)
.setRequiresCharging(DEVICE_CHARGING in restrictions)
.build()
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
@ -62,8 +62,8 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
}
fun requiresWifiConnection(preferences: PreferencesHelper): Boolean {
val restrictions = preferences.libraryUpdateRestriction().get()
return ONLY_ON_WIFI in restrictions
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
return DEVICE_ONLY_ON_WIFI in restrictions
}
}
}

View file

@ -1,58 +0,0 @@
package eu.kanade.tachiyomi.data.library
import eu.kanade.tachiyomi.data.database.models.Manga
import java.util.Collections
import kotlin.math.abs
/**
* This class will provide various functions to rank manga to efficiently schedule manga to update.
*/
object LibraryUpdateRanker {
val rankingScheme = listOf(
(this::lexicographicRanking)(),
(this::latestFirstRanking)(),
(this::nextFirstRanking)()
)
/**
* Provides a total ordering over all the Mangas.
*
* Orders the manga based on the distance between the next expected update and now.
* The comparator is reversed, placing the smallest (and thus closest to updating now) first.
*/
fun nextFirstRanking(): Comparator<Manga> {
val time = System.currentTimeMillis()
return Collections.reverseOrder(
Comparator { mangaFirst: Manga,
mangaSecond: Manga ->
compareValues(abs(mangaSecond.next_update - time), abs(mangaFirst.next_update - time))
}
)
}
/**
* Provides a total ordering over all the [Manga]s.
*
* Assumption: An active [Manga] mActive is expected to have been last updated after an
* inactive [Manga] mInactive.
*
* Using this insight, function returns a Comparator for which mActive appears before mInactive.
* @return a Comparator that ranks manga based on relevance.
*/
private fun latestFirstRanking(): Comparator<Manga> =
Comparator { first: Manga, second: Manga ->
compareValues(second.last_update, first.last_update)
}
/**
* Provides a total ordering over all the [Manga]s.
*
* Order the manga lexicographically.
* @return a Comparator that ranks manga lexicographically based on the title.
*/
private fun lexicographicRanking(): Comparator<Manga> =
Comparator { first: Manga, second: Manga ->
compareValues(first.title, second.title)
}
}

View file

@ -17,9 +17,10 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.MANGA_FULLY_READ
import eu.kanade.tachiyomi.data.preference.MANGA_ONGOING
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager
@ -258,14 +259,20 @@ class LibraryUpdateService(
listToInclude.minus(listToExclude)
}
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
if (target == Target.CHAPTERS) {
val restrictions = preferences.libraryUpdateMangaRestriction().get()
if (MANGA_ONGOING in restrictions) {
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
}
if (MANGA_FULLY_READ in restrictions) {
listToUpdate = listToUpdate.filter { it.unread == 0 }
}
}
val selectedScheme = preferences.libraryUpdatePrioritization().get()
mangaToUpdate = listToUpdate
.distinctBy { it.id }
.sortedWith(rankingScheme[selectedScheme])
.sortedBy { it.title }
// Warn when excessively checking a single source
val maxUpdatesFromSource = mangaToUpdate
@ -545,6 +552,7 @@ class LibraryUpdateService(
if (errors.isNotEmpty()) {
val file = createFileInCacheDir("tachiyomi_update_errors.txt")
file.bufferedWriter().use { out ->
out.write(getString(R.string.library_errors_help, ERROR_LOG_HELP_URL) + "\n\n")
// Error file format:
// ! Error
// # Source
@ -570,3 +578,4 @@ class LibraryUpdateService(
}
private const val MANGA_PER_SOURCE_QUEUE_WARNING_THRESHOLD = 60
private const val ERROR_LOG_HELP_URL = "https://tachiyomi.org/help/guides/troubleshooting"

View file

@ -5,56 +5,10 @@ package eu.kanade.tachiyomi.data.preference
*/
object PreferenceKeys {
const val themeMode = "pref_theme_mode_key"
const val appTheme = "pref_app_theme"
const val themeDarkAmoled = "pref_theme_dark_amoled_key"
const val confirmExit = "pref_confirm_exit"
const val hideBottomBarOnScroll = "pref_hide_bottom_bar_on_scroll"
const val sideNavIconAlignment = "pref_side_nav_icon_alignment"
const val enableTransitions = "pref_enable_transitions_key"
const val doubleTapAnimationSpeed = "pref_double_tap_anim_speed"
const val showPageNumber = "pref_show_page_number_key"
const val dualPageSplitPaged = "pref_dual_page_split"
const val dualPageSplitWebtoon = "pref_dual_page_split_webtoon"
const val dualPageInvertPaged = "pref_dual_page_invert"
const val dualPageInvertWebtoon = "pref_dual_page_invert_webtoon"
const val showReadingMode = "pref_show_reading_mode"
const val trueColor = "pref_true_color_key"
const val fullscreen = "fullscreen"
const val cutoutShort = "cutout_short"
const val keepScreenOn = "pref_keep_screen_on_key"
const val customBrightness = "pref_custom_brightness_key"
const val customBrightnessValue = "custom_brightness_value"
const val colorFilter = "pref_color_filter_key"
const val colorFilterValue = "color_filter_value"
const val colorFilterMode = "color_filter_mode"
const val grayscale = "pref_grayscale"
const val invertedColors = "pref_inverted_colors"
const val defaultReadingMode = "pref_default_reading_mode_key"
const val defaultOrientationType = "pref_default_orientation_type_key"
@ -75,69 +29,10 @@ object PreferenceKeys {
const val externalPlayerPreference = "external_player_preference"
const val imageScaleType = "pref_image_scale_type_key"
const val zoomStart = "pref_zoom_start_key"
const val readerTheme = "pref_reader_theme_key"
const val cropBorders = "crop_borders"
const val cropBordersWebtoon = "crop_borders_webtoon"
const val readWithTapping = "reader_tap"
const val pagerNavInverted = "reader_tapping_inverted"
const val webtoonNavInverted = "reader_tapping_inverted_webtoon"
const val readWithLongTap = "reader_long_tap"
const val readWithVolumeKeys = "reader_volume_keys"
const val readWithVolumeKeysInverted = "reader_volume_keys_inverted"
const val navigationModePager = "reader_navigation_mode_pager"
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
const val showNavigationOverlayOnStart = "reader_navigation_overlay_on_start"
const val readerHideThreshold = "reader_hide_threshold"
const val webtoonSidePadding = "webtoon_side_padding"
const val portraitColumns = "pref_library_columns_portrait_key"
const val landscapeColumns = "pref_library_columns_landscape_key"
const val jumpToChapters = "jump_to_chapters"
const val jumpToEpisodes = "jump_to_episodes"
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
const val lastUsedSource = "last_catalogue_source"
const val lastUsedAnimeSource = "last_anime_catalogue_source"
const val lastUsedCategory = "last_used_category"
const val lastUsedAnimeCategory = "last_used_anime_category"
const val sourceDisplayMode = "pref_display_mode_catalogue"
const val animesourceDisplayMode = "pref_display_mode_anime_catalogue"
const val enabledLanguages = "source_languages"
const val backupDirectory = "backup_directory"
const val downloadsDirectory = "download_directory"
const val useExternalDownloader = "use_external_downloader"
const val externalDownloaderSelection = "external_downloader_selection"
@ -148,31 +43,12 @@ object PreferenceKeys {
const val folderPerAnime = "create_folder_per_anime"
const val numberOfBackups = "backup_slots"
const val backupInterval = "backup_interval"
const val removeAfterReadSlots = "remove_after_read_slots"
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
const val removeBookmarkedChapters = "pref_remove_bookmarked"
const val libraryUpdateInterval = "pref_library_update_interval_key"
const val libraryUpdateRestriction = "library_update_restriction"
const val showUpdatesNavBadge = "library_update_show_tab_badge"
const val libraryUpdateCategories = "library_update_categories"
const val animelibUpdateCategories = "animelib_update_categories"
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
const val animelibUpdateCategoriesExclude = "animelib_update_categories_exclude"
const val libraryUpdatePrioritization = "library_update_prioritization"
const val downloadedOnly = "pref_downloaded_only"
const val filterDownloaded = "pref_filter_library_downloaded"
const val filterUnread = "pref_filter_library_unread"
@ -187,20 +63,8 @@ object PreferenceKeys {
const val migrationSortingMode = "pref_migration_sorting"
const val migrationSortingDirection = "pref_migration_direction"
const val automaticExtUpdates = "automatic_ext_updates"
const val showNsfwSource = "show_nsfw_source"
const val startScreen = "start_screen"
const val useAuthenticator = "use_biometric_lock"
const val lockAppAfter = "lock_app_after"
const val lastAppUnlock = "last_app_unlock"
const val secureScreen = "secure_screen"
const val hideNotificationContent = "hide_notification_content"
const val autoUpdateMetadata = "auto_update_metadata"
@ -209,48 +73,15 @@ object PreferenceKeys {
const val downloadNew = "download_new"
const val downloadNewCategories = "download_new_categories"
const val downloadNewCategoriesAnime = "download_new_categories_anime"
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
const val downloadNewCategoriesExcludeAnime = "download_new_categories_exclude_anime"
const val removeExcludeCategories = "remove_exclude_categories"
const val removeExcludeCategoriesAnime = "remove_exclude_categories_anime"
const val libraryDisplayMode = "pref_display_mode_library"
const val lang = "app_language"
const val relativeTime: String = "relative_time"
const val dateFormat = "app_date_format"
const val defaultCategory = "default_category"
const val defaultAnimeCategory = "default_anime_category"
const val categorizedDisplay = "categorized_display"
const val skipRead = "skip_read"
const val skipFiltered = "skip_filtered"
const val downloadBadge = "display_download_badge"
const val unreadBadge = "display_unread_badge"
const val languageBadge = "display_language_badge"
const val localBadge = "display_local_badge"
const val categoryTabs = "display_category_tabs"
const val animeCategoryTabs = "display_anime_category_tabs"
const val categoryNumberOfItems = "display_number_of_items"
const val animeCategoryNumberOfItems = "display_number_of_items_anime"
const val alwaysShowChapterTransition = "always_show_chapter_transition"
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
const val dohProvider = "doh_provider"
@ -279,12 +110,6 @@ object PreferenceKeys {
const val defaultEpisodeDisplayByNameOrNumber = "default_episode_display_by_name_or_number"
const val incognitoMode = "incognito_mode"
const val tabletUiMode = "tablet_ui_mode"
const val extensionInstaller = "extension_installer"
const val verboseLogging = "verbose_logging"
const val autoClearChapterCache = "auto_clear_chapter_cache"

View file

@ -2,8 +2,11 @@ package eu.kanade.tachiyomi.data.preference
import eu.kanade.tachiyomi.R
const val ONLY_ON_WIFI = "wifi"
const val CHARGING = "ac"
const val DEVICE_ONLY_ON_WIFI = "wifi"
const val DEVICE_CHARGING = "ac"
const val MANGA_ONGOING = "manga_ongoing"
const val MANGA_FULLY_READ = "manga_fully_read"
/**
* This class stores the values for the preferences in the application.

View file

@ -1,16 +1,16 @@
package eu.kanade.tachiyomi.data.preference
import android.content.Context
import android.os.Build
import android.os.Environment
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import com.google.android.material.color.DynamicColors
import com.tfcporciuncula.flow.FlowSharedPreferences
import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferenceValues.ThemeMode.system
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
@ -19,10 +19,8 @@ import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.util.system.MiuiUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
@ -30,25 +28,6 @@ import java.util.Locale
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
fun <T> Preference<T>.asImmediateFlow(block: (T) -> Unit): Flow<T> {
block(get())
return asFlow()
.onEach { block(it) }
}
operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
set(get() + item)
}
operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
set(get() - item)
}
fun Preference<Boolean>.toggle(): Boolean {
set(!get())
return get()
}
class PreferencesHelper(val context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
@ -70,17 +49,17 @@ class PreferencesHelper(val context: Context) {
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
fun hideBottomBarOnScroll() = flowPrefs.getBoolean(Keys.hideBottomBarOnScroll, true)
fun hideBottomBarOnScroll() = flowPrefs.getBoolean("pref_hide_bottom_bar_on_scroll", true)
fun sideNavIconAlignment() = flowPrefs.getInt(Keys.sideNavIconAlignment, 0)
fun sideNavIconAlignment() = flowPrefs.getInt("pref_side_nav_icon_alignment", 0)
fun useAuthenticator() = flowPrefs.getBoolean(Keys.useAuthenticator, false)
fun useAuthenticator() = flowPrefs.getBoolean("use_biometric_lock", false)
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
fun lockAppAfter() = flowPrefs.getInt("lock_app_after", 0)
fun lastAppUnlock() = flowPrefs.getLong(Keys.lastAppUnlock, 0)
fun lastAppUnlock() = flowPrefs.getLong("last_app_unlock", 0)
fun secureScreen() = flowPrefs.getBoolean(Keys.secureScreen, false)
fun secureScreen() = flowPrefs.getBoolean("secure_screen", false)
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
@ -88,49 +67,55 @@ class PreferencesHelper(val context: Context) {
fun autoUpdateTrackers() = prefs.getBoolean(Keys.autoUpdateTrackers, false)
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, system)
fun themeMode() = flowPrefs.getEnum(
"pref_theme_mode_key",
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { Values.ThemeMode.system } else { Values.ThemeMode.light }
)
fun appTheme() = flowPrefs.getEnum(Keys.appTheme, Values.AppTheme.DEFAULT)
fun appTheme() = flowPrefs.getEnum(
"pref_app_theme",
if (DynamicColors.isDynamicColorAvailable()) { Values.AppTheme.MONET } else { Values.AppTheme.DEFAULT }
)
fun themeDarkAmoled() = flowPrefs.getBoolean(Keys.themeDarkAmoled, false)
fun themeDarkAmoled() = flowPrefs.getBoolean("pref_theme_dark_amoled_key", false)
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
fun pageTransitions() = flowPrefs.getBoolean("pref_enable_transitions_key", true)
fun doubleTapAnimSpeed() = flowPrefs.getInt(Keys.doubleTapAnimationSpeed, 500)
fun doubleTapAnimSpeed() = flowPrefs.getInt("pref_double_tap_anim_speed", 500)
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
fun showPageNumber() = flowPrefs.getBoolean("pref_show_page_number_key", true)
fun dualPageSplitPaged() = flowPrefs.getBoolean(Keys.dualPageSplitPaged, false)
fun dualPageSplitPaged() = flowPrefs.getBoolean("pref_dual_page_split", false)
fun dualPageSplitWebtoon() = flowPrefs.getBoolean(Keys.dualPageSplitWebtoon, false)
fun dualPageSplitWebtoon() = flowPrefs.getBoolean("pref_dual_page_split_webtoon", false)
fun dualPageInvertPaged() = flowPrefs.getBoolean(Keys.dualPageInvertPaged, false)
fun dualPageInvertPaged() = flowPrefs.getBoolean("pref_dual_page_invert", false)
fun dualPageInvertWebtoon() = flowPrefs.getBoolean(Keys.dualPageInvertWebtoon, false)
fun dualPageInvertWebtoon() = flowPrefs.getBoolean("pref_dual_page_invert_webtoon", false)
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
fun trueColor() = flowPrefs.getBoolean("pref_true_color_key", false)
fun fullscreen() = flowPrefs.getBoolean(Keys.fullscreen, true)
fun fullscreen() = flowPrefs.getBoolean("fullscreen", true)
fun cutoutShort() = flowPrefs.getBoolean(Keys.cutoutShort, true)
fun cutoutShort() = flowPrefs.getBoolean("cutout_short", true)
fun keepScreenOn() = flowPrefs.getBoolean(Keys.keepScreenOn, true)
fun keepScreenOn() = flowPrefs.getBoolean("pref_keep_screen_on_key", true)
fun customBrightness() = flowPrefs.getBoolean(Keys.customBrightness, false)
fun customBrightness() = flowPrefs.getBoolean("pref_custom_brightness_key", false)
fun customBrightnessValue() = flowPrefs.getInt(Keys.customBrightnessValue, 0)
fun customBrightnessValue() = flowPrefs.getInt("custom_brightness_value", 0)
fun colorFilter() = flowPrefs.getBoolean(Keys.colorFilter, false)
fun colorFilter() = flowPrefs.getBoolean("pref_color_filter_key", false)
fun colorFilterValue() = flowPrefs.getInt(Keys.colorFilterValue, 0)
fun colorFilterValue() = flowPrefs.getInt("color_filter_value", 0)
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
fun colorFilterMode() = flowPrefs.getInt("color_filter_mode", 0)
fun grayscale() = flowPrefs.getBoolean(Keys.grayscale, false)
fun grayscale() = flowPrefs.getBoolean("pref_grayscale", false)
fun invertedColors() = flowPrefs.getBoolean(Keys.invertedColors, false)
fun invertedColors() = flowPrefs.getBoolean("pref_inverted_colors", false)
fun defaultReadingMode() = prefs.getInt(Keys.defaultReadingMode, ReadingModeType.RIGHT_TO_LEFT.flagValue)
@ -160,72 +145,63 @@ class PreferencesHelper(val context: Context) {
fun skipLengthPreference() = prefs.getString(Keys.skipLengthPreference, "10")!!.toInt()
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
fun imageScaleType() = flowPrefs.getInt("pref_image_scale_type_key", 1)
fun zoomStart() = flowPrefs.getInt(Keys.zoomStart, 1)
fun zoomStart() = flowPrefs.getInt("pref_zoom_start_key", 1)
fun readerTheme() = flowPrefs.getInt(Keys.readerTheme, 1)
fun readerTheme() = flowPrefs.getInt("pref_reader_theme_key", 1)
fun watcherTheme() = flowPrefs.getInt(Keys.readerTheme, 1)
fun alwaysShowChapterTransition() = flowPrefs.getBoolean("always_show_chapter_transition", true)
fun alwaysShowChapterTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
fun cropBorders() = flowPrefs.getBoolean("crop_borders", false)
fun alwaysShowEpisodeTransition() = flowPrefs.getBoolean(Keys.alwaysShowChapterTransition, true)
fun cropBordersWebtoon() = flowPrefs.getBoolean("crop_borders_webtoon", false)
fun cropBorders() = flowPrefs.getBoolean(Keys.cropBorders, false)
fun webtoonSidePadding() = flowPrefs.getInt("webtoon_side_padding", 0)
fun cropBordersWebtoon() = flowPrefs.getBoolean(Keys.cropBordersWebtoon, false)
fun readWithTapping() = flowPrefs.getBoolean("reader_tap", true)
fun webtoonSidePadding() = flowPrefs.getInt(Keys.webtoonSidePadding, 0)
fun pagerNavInverted() = flowPrefs.getEnum("reader_tapping_inverted", Values.TappingInvertMode.NONE)
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
fun webtoonNavInverted() = flowPrefs.getEnum("reader_tapping_inverted_webtoon", Values.TappingInvertMode.NONE)
fun pagerNavInverted() = flowPrefs.getEnum(Keys.pagerNavInverted, Values.TappingInvertMode.NONE)
fun readWithLongTap() = flowPrefs.getBoolean("reader_long_tap", true)
fun webtoonNavInverted() = flowPrefs.getEnum(Keys.webtoonNavInverted, Values.TappingInvertMode.NONE)
fun readWithVolumeKeys() = flowPrefs.getBoolean("reader_volume_keys", false)
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean("reader_volume_keys_inverted", false)
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
fun navigationModePager() = flowPrefs.getInt("reader_navigation_mode_pager", 0)
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
fun navigationModeWebtoon() = flowPrefs.getInt("reader_navigation_mode_webtoon", 0)
fun navigationModePager() = flowPrefs.getInt(Keys.navigationModePager, 0)
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean("reader_navigation_overlay_new_user", true)
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean("reader_navigation_overlay_on_start", false)
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
fun readerHideThreshold() = flowPrefs.getEnum("reader_hide_threshold", Values.ReaderHideThreshold.LOW)
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean(Keys.showNavigationOverlayOnStart, false)
fun portraitColumns() = flowPrefs.getInt("pref_library_columns_portrait_key", 0)
fun readerHideTreshold() = flowPrefs.getEnum(Keys.readerHideThreshold, Values.ReaderHideThreshold.LOW)
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
fun landscapeColumns() = flowPrefs.getInt("pref_library_columns_landscape_key", 0)
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
fun jumpToEpisodes() = prefs.getBoolean(Keys.jumpToEpisodes, false)
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, true)
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
fun lastUsedSource() = flowPrefs.getLong("last_catalogue_source", -1)
fun lastUsedAnimeSource() = flowPrefs.getLong(Keys.lastUsedAnimeSource, -1)
fun lastUsedAnimeSource() = flowPrefs.getLong("last_anime_catalogue_source", -1)
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
fun lastUsedCategory() = flowPrefs.getInt("last_used_category", 0)
fun lastUsedAnimeCategory() = flowPrefs.getInt(Keys.lastUsedAnimeCategory, 0)
fun lastUsedAnimeCategory() = flowPrefs.getInt("last_used_anime_category", 0)
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayModeSetting.COMPACT_GRID)
fun animesourceDisplayMode() = flowPrefs.getEnum(Keys.animesourceDisplayMode, DisplayModeSetting.COMPACT_GRID)
fun sourceDisplayMode() = flowPrefs.getEnum("pref_display_mode_catalogue", DisplayModeSetting.COMPACT_GRID)
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("all", "en", Locale.getDefault().language))
fun enabledLanguages() = flowPrefs.getStringSet("source_languages", setOf("all", "en", Locale.getDefault().language))
fun trackUsername(sync: TrackService) = prefs.getString(Keys.trackUsername(sync.id), "")
@ -242,16 +218,16 @@ class PreferencesHelper(val context: Context) {
fun anilistScoreType() = flowPrefs.getString("anilist_score_type", Anilist.POINT_10)
fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
fun backupsDirectory() = flowPrefs.getString("backup_directory", defaultBackupDir.toString())
fun relativeTime() = flowPrefs.getInt(Keys.relativeTime, 7)
fun relativeTime() = flowPrefs.getInt("relative_time", 7)
fun dateFormat(format: String = flowPrefs.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault())
}
fun downloadsDirectory() = flowPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
fun downloadsDirectory() = flowPrefs.getString("download_directory", defaultDownloadsDir.toString())
fun useExternalDownloader() = prefs.getBoolean(Keys.useExternalDownloader, false)
@ -259,13 +235,13 @@ class PreferencesHelper(val context: Context) {
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", false)
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
fun folderPerAnime() = prefs.getBoolean(Keys.folderPerAnime, false)
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 1)
fun numberOfBackups() = flowPrefs.getInt(Keys.numberOfBackups, 1)
fun backupInterval() = flowPrefs.getInt(Keys.backupInterval, 0)
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
fun removeAfterReadSlots() = prefs.getInt(Keys.removeAfterReadSlots, -1)
@ -273,44 +249,43 @@ class PreferencesHelper(val context: Context) {
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
fun removeExcludeCategories() = flowPrefs.getStringSet(Keys.removeExcludeCategories, emptySet())
fun removeExcludeAnimeCategories() = flowPrefs.getStringSet(Keys.removeExcludeCategoriesAnime, emptySet())
fun removeExcludeCategories() = flowPrefs.getStringSet("remove_exclude_categories", emptySet())
fun removeExcludeAnimeCategories() = flowPrefs.getStringSet("remove_exclude_categories_anime", emptySet())
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
fun libraryUpdateInterval() = flowPrefs.getInt("pref_library_update_interval_key", 24)
fun libraryUpdateRestriction() = flowPrefs.getStringSet(Keys.libraryUpdateRestriction, setOf(ONLY_ON_WIFI))
fun libraryUpdateDeviceRestriction() = flowPrefs.getStringSet("library_update_restriction", setOf(DEVICE_ONLY_ON_WIFI))
fun libraryUpdateMangaRestriction() = flowPrefs.getStringSet("library_update_manga_restriction", setOf(MANGA_FULLY_READ, MANGA_ONGOING))
fun showUpdatesNavBadge() = flowPrefs.getBoolean(Keys.showUpdatesNavBadge, false)
fun showUpdatesNavBadge() = flowPrefs.getBoolean("library_update_show_tab_badge", false)
fun unreadUpdatesCount() = flowPrefs.getInt("library_unread_updates_count", 0)
fun unseenUpdatesCount() = flowPrefs.getInt("library_unseen_updates_count", 0)
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
fun animelibUpdateCategories() = flowPrefs.getStringSet(Keys.animelibUpdateCategories, emptySet())
fun libraryUpdateCategories() = flowPrefs.getStringSet("library_update_categories", emptySet())
fun animelibUpdateCategories() = flowPrefs.getStringSet("animelib_update_categories", emptySet())
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
fun animelibUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.animelibUpdateCategoriesExclude, emptySet())
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet("library_update_categories_exclude", emptySet())
fun animelibUpdateCategoriesExclude() = flowPrefs.getStringSet("animelib_update_categories_exclude", emptySet())
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
fun libraryDisplayMode() = flowPrefs.getEnum("pref_display_mode_library", DisplayModeSetting.COMPACT_GRID)
fun libraryDisplayMode() = flowPrefs.getEnum(Keys.libraryDisplayMode, DisplayModeSetting.COMPACT_GRID)
fun downloadBadge() = flowPrefs.getBoolean("display_download_badge", false)
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
fun localBadge() = flowPrefs.getBoolean("display_local_badge", true)
fun localBadge() = flowPrefs.getBoolean(Keys.localBadge, true)
fun downloadedOnly() = flowPrefs.getBoolean("pref_downloaded_only", false)
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
fun unreadBadge() = flowPrefs.getBoolean("display_unread_badge", true)
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
fun languageBadge() = flowPrefs.getBoolean("display_language_badge", false)
fun languageBadge() = flowPrefs.getBoolean(Keys.languageBadge, false)
fun categoryTabs() = flowPrefs.getBoolean("display_category_tabs", true)
fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true)
fun animeCategoryTabs() = flowPrefs.getBoolean("display_anime_category_tabs", true)
fun animeCategoryTabs() = flowPrefs.getBoolean(Keys.animeCategoryTabs, true)
fun categoryNumberOfItems() = flowPrefs.getBoolean("display_number_of_items", false)
fun categoryNumberOfItems() = flowPrefs.getBoolean(Keys.categoryNumberOfItems, false)
fun animeCategoryNumberOfItems() = flowPrefs.getBoolean(Keys.animeCategoryNumberOfItems, false)
fun animeCategoryNumberOfItems() = flowPrefs.getBoolean("display_number_of_items_anime", false)
fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
@ -326,9 +301,9 @@ class PreferencesHelper(val context: Context) {
fun migrationSortingMode() = flowPrefs.getEnum(Keys.migrationSortingMode, MigrationSourcesController.SortSetting.ALPHABETICAL)
fun migrationSortingDirection() = flowPrefs.getEnum(Keys.migrationSortingDirection, MigrationSourcesController.DirectionSetting.ASCENDING)
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
fun automaticExtUpdates() = flowPrefs.getBoolean("automatic_ext_updates", true)
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
fun showNsfwSource() = flowPrefs.getBoolean("show_nsfw_source", true)
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
@ -350,19 +325,19 @@ class PreferencesHelper(val context: Context) {
fun pinnedAnimeSources() = flowPrefs.getStringSet("pinned_anime_catalogues", emptySet())
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
fun downloadNew() = flowPrefs.getBoolean("download_new", false)
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
fun downloadNewCategoriesAnime() = flowPrefs.getStringSet(Keys.downloadNewCategoriesAnime, emptySet())
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
fun downloadNewCategoriesAnimeExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExcludeAnime, emptySet())
fun downloadNewCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
fun downloadNewCategoriesAnime() = flowPrefs.getStringSet("download_new_categories_anime", emptySet())
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
fun downloadNewCategoriesAnimeExclude() = flowPrefs.getStringSet("download_new_categories_exclude_anime", emptySet())
fun lang() = flowPrefs.getString(Keys.lang, "")
fun lang() = flowPrefs.getString("app_language", "")
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
fun defaultAnimeCategory() = prefs.getInt(Keys.defaultAnimeCategory, -1)
fun categorisedDisplaySettings() = flowPrefs.getBoolean(Keys.categorizedDisplay, false)
fun categorizedDisplaySettings() = flowPrefs.getBoolean("categorized_display", false)
fun skipRead() = prefs.getBoolean(Keys.skipRead, false)
@ -400,13 +375,13 @@ class PreferencesHelper(val context: Context) {
fun sortEpisodeByAscendingOrDescending() = prefs.getInt(Keys.defaultEpisodeSortByAscendingOrDescending, Anime.EPISODE_SORT_DESC)
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
fun incognitoMode() = flowPrefs.getBoolean("incognito_mode", false)
fun tabletUiMode() = flowPrefs.getEnum(Keys.tabletUiMode, Values.TabletUiMode.AUTOMATIC)
fun tabletUiMode() = flowPrefs.getEnum("tablet_ui_mode", Values.TabletUiMode.AUTOMATIC)
fun extensionInstaller() = flowPrefs.getEnum(
Keys.extensionInstaller,
if (MiuiUtil.isMiui()) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER
"extension_installer",
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER
)
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, false)

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.track.anilist
import okhttp3.Interceptor
import okhttp3.Response
import java.io.IOException
class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Interceptor {
@ -28,12 +29,12 @@ class AnilistInterceptor(val anilist: Anilist, private var token: String?) : Int
// Refresh access token if null or expired.
if (oauth!!.isExpired()) {
anilist.logout()
throw Exception("Token expired")
throw IOException("Token expired")
}
// Throw on null auth.
if (oauth == null) {
throw Exception("No authentication token")
throw IOException("No authentication token")
}
// Add the authorization header to the original request.

View file

@ -16,15 +16,6 @@ class BangumiInterceptor(val bangumi: Bangumi) : Interceptor {
*/
private var oauth: OAuth? = bangumi.restoreToken()
fun addToken(token: String, oidFormBody: FormBody): FormBody {
val newFormBody = FormBody.Builder()
for (i in 0 until oidFormBody.size) {
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
}
newFormBody.add("access_token", token)
return newFormBody.build()
}
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
@ -65,4 +56,13 @@ class BangumiInterceptor(val bangumi: Bangumi) : Interceptor {
bangumi.saveToken(oauth)
}
private fun addToken(token: String, oidFormBody: FormBody): FormBody {
val newFormBody = FormBody.Builder()
for (i in 0 until oidFormBody.size) {
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
}
newFormBody.add("access_token", token)
return newFormBody.build()
}
}

View file

@ -64,7 +64,8 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
suspend fun search(query: String): List<TrackSearch> {
return withIOContext {
val url = "$baseApiUrl/manga".toUri().buildUpon()
.appendQueryParameter("q", query)
// MAL API throws a 400 when the query is over 64 characters...
.appendQueryParameter("q", query.take(64))
.appendQueryParameter("nsfw", "true")
.build()
authClient.newCall(GET(url.toString()))
@ -86,6 +87,8 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
suspend fun searchAnime(query: String): List<AnimeTrackSearch> {
return withIOContext {
val url = "$baseApiUrl/anime".toUri().buildUpon()
// MAL API throws a 400 when the query is over 64 characters...
.appendQueryParameter("q", query.take(64))
.appendQueryParameter("q", query)
.appendQueryParameter("nsfw", "true")
.build()

View file

@ -5,6 +5,7 @@ import kotlinx.serialization.json.Json
import okhttp3.Interceptor
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
import java.io.IOException
class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var token: String?) : Interceptor {
@ -16,7 +17,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
val originalRequest = chain.request()
if (token.isNullOrEmpty()) {
throw Exception("Not authenticated with MyAnimeList")
throw IOException("Not authenticated with MyAnimeList")
}
if (oauth == null) {
oauth = myanimelist.loadOAuth()
@ -30,7 +31,7 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
}
}
if (oauth == null) {
throw Exception("No authentication token")
throw IOException("No authentication token")
}
// Add the authorization header to the original request

View file

@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.extension.api.AnimeExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.AnimeExtension
import eu.kanade.tachiyomi.extension.model.AnimeLoadResult
@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.extension.util.AnimeExtensionInstallReceiver
import eu.kanade.tachiyomi.extension.util.AnimeExtensionInstaller
import eu.kanade.tachiyomi.extension.util.AnimeExtensionLoader
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async

View file

@ -5,7 +5,6 @@ import android.graphics.drawable.Drawable
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep
@ -16,6 +15,7 @@ import eu.kanade.tachiyomi.extension.util.ExtensionLoader
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.async

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.extension.api
import android.content.Context
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.extension.model.AvailableExtensionSources
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.extension.util.ExtensionLoader
@ -80,12 +81,23 @@ internal class ExtensionGithubApi {
versionCode = it.code,
lang = it.lang,
isNsfw = it.nsfw == 1,
sources = it.sources?.toExtensionSources() ?: emptyList(),
apkName = it.apk,
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}"
)
}
}
private fun List<ExtensionSourceJsonObject>.toExtensionSources(): List<AvailableExtensionSources> {
return this.map {
AvailableExtensionSources(
name = it.name,
id = it.id,
baseUrl = it.baseUrl
)
}
}
fun getApkUrl(extension: Extension.Available): String {
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
}
@ -94,12 +106,21 @@ internal class ExtensionGithubApi {
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
@Serializable
data class ExtensionJsonObject(
private data class ExtensionJsonObject(
val name: String,
val pkg: String,
val apk: String,
val version: String,
val code: Long,
val lang: String,
val code: Long,
val version: String,
val nsfw: Int,
val sources: List<ExtensionSourceJsonObject>?,
)
@Serializable
private data class ExtensionSourceJsonObject(
val name: String,
val id: Long,
val baseUrl: String
)

View file

@ -32,6 +32,7 @@ sealed class Extension {
override val versionCode: Long,
override val lang: String,
override val isNsfw: Boolean,
val sources: List<AvailableExtensionSources>,
val apkName: String,
val iconUrl: String
) : Extension()
@ -46,3 +47,9 @@ sealed class Extension {
override val isNsfw: Boolean = false
) : Extension()
}
data class AvailableExtensionSources(
val name: String,
val id: Long,
val baseUrl: String
)

View file

@ -110,7 +110,7 @@ internal class AnimeExtensionInstaller(private val context: Context) {
.map {
downloadManager.query(query).use { cursor ->
cursor.moveToFirst()
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
}
}
// Ignore duplicate results
@ -249,7 +249,7 @@ internal class AnimeExtensionInstaller(private val context: Context) {
downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) {
val localUri = cursor.getString(
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI)
).removePrefix(FILE_SCHEME)
installApk(id, File(localUri).getUriCompat(context))

View file

@ -110,7 +110,7 @@ internal class ExtensionInstaller(private val context: Context) {
.map {
downloadManager.query(query).use { cursor ->
cursor.moveToFirst()
cursor.getInt(cursor.getColumnIndex(DownloadManager.COLUMN_STATUS))
cursor.getInt(cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_STATUS))
}
}
// Ignore duplicate results
@ -249,7 +249,7 @@ internal class ExtensionInstaller(private val context: Context) {
downloadManager.query(query).use { cursor ->
if (cursor.moveToFirst()) {
val localUri = cursor.getString(
cursor.getColumnIndex(DownloadManager.COLUMN_LOCAL_URI)
cursor.getColumnIndexOrThrow(DownloadManager.COLUMN_LOCAL_URI)
).removePrefix(FILE_SCHEME)
installApk(id, File(localUri).getUriCompat(context))

View file

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.network.interceptor
import android.annotation.SuppressLint
import android.content.Context
import android.os.Build
import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
@ -10,6 +11,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.isOutdated
@ -37,6 +39,13 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
* Application class.
*/
private val initWebView by lazy {
// Crashes on some devices. We skip this in some cases since the only impact is slower
// WebView init in those rare cases.
// See https://bugs.chromium.org/p/chromium/issues/detail?id=1279562
if (Build.VERSION.SDK_INT == Build.VERSION_CODES.S && DeviceUtil.isSamsung) {
return@lazy
}
WebSettings.getDefaultUserAgent(context)
}
@ -68,9 +77,12 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
resolveWithWebView(originalRequest, oldCookie)
return chain.proceed(originalRequest)
} catch (e: Exception) {
}
// Because OkHttp's enqueue only handles IOExceptions, wrap the exception so that
// we don't crash the entire app
catch (e: CloudflareBypassException) {
throw IOException(context.getString(R.string.information_cloudflare_bypass_failure))
} catch (e: Exception) {
throw IOException(e)
}
}
@ -162,7 +174,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
context.toast(R.string.information_webview_outdated, Toast.LENGTH_LONG)
}
throw Exception(context.getString(R.string.information_cloudflare_bypass_failure))
throw CloudflareBypassException()
}
}
@ -172,3 +184,5 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
private val COOKIE_NAMES = listOf("cf_clearance")
}
}
private class CloudflareBypassException : Exception()

View file

@ -14,6 +14,8 @@ import java.util.concurrent.TimeUnit
* permits = 5, period = 1, unit = seconds => 5 requests per second
* permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes
*
* @since extension-lib 1.3
*
* @param permits {Int} Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1.
* @param unit {TimeUnit} The unit of time for the period. Defaults to seconds.

View file

@ -15,6 +15,8 @@ import java.util.concurrent.TimeUnit
* httpUrl = "api.manga.com".toHttpUrlOrNull(), permits = 5, period = 1, unit = seconds => 5 requests per second to api.manga.com
* httpUrl = "imagecdn.manga.com".toHttpUrlOrNull(), permits = 10, period = 2, unit = minutes => 10 requests per 2 minutes to imagecdn.manga.com
*
* @since extension-lib 1.3
*
* @param httpUrl {HttpUrl} The url host that this interceptor should handle. Will get url's host by using HttpUrl.host()
* @param permits {Int} Number of requests allowed within a period of units.
* @param period {Long} The limiting duration. Defaults to 1.

View file

@ -34,7 +34,6 @@ import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.FileInputStream
import java.io.InputStream
import java.util.Locale
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile
@ -111,9 +110,9 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
when (state?.index) {
0 -> {
mangaDirs = if (state.ascending) {
mangaDirs.sortedBy { it.name.lowercase(Locale.ENGLISH) }
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
} else {
mangaDirs.sortedByDescending { it.name.lowercase(Locale.ENGLISH) }
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it.name }))
}
}
1 -> {
@ -176,7 +175,7 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
.asSequence()
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
.flatten()
.firstOrNull { it.extension.lowercase() == "json" }
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
return if (localDetails != null) {
val obj = json.decodeFromStream<JsonObject>(localDetails.inputStream())

View file

@ -15,7 +15,6 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.annotation.FloatRange
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.os.bundleOf
import androidx.core.view.ViewCompat
@ -102,6 +101,7 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.getCoordinates
import eu.kanade.tachiyomi.util.view.shrinkOnScroll
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
@ -121,7 +121,7 @@ import kotlin.math.min
class AnimeController :
NucleusController<MangaControllerBinding, AnimePresenter>,
FabController,
ActionMode.Callback,
ActionModeWithToolbar.Callback,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
BaseEpisodesAdapter.OnEpisodeClickListener,
@ -179,7 +179,7 @@ class AnimeController :
/**
* Action mode for multiple selection.
*/
private var actionMode: ActionMode? = null
private var actionMode: ActionModeWithToolbar? = null
/**
* Selected items. Used to restore selections after a rotation.
@ -260,11 +260,6 @@ class AnimeController :
it.layoutManager = LinearLayoutManager(view.context)
it.setHasFixedSize(true)
}
binding.actionToolbar.applyInsetter {
type(navigationBars = true) {
margin(bottom = true, horizontal = true)
}
}
if (anime == null || source == null) return
@ -330,7 +325,7 @@ class AnimeController :
actionFabScrollListener = actionFab?.shrinkOnScroll(episodeRecycler)
// Initially set FAB invisible; will become visible if unseen episodes are present
actionFab?.isVisible = false
actionFab?.hide()
binding.swipeRefresh.refreshes()
.onEach {
@ -365,6 +360,9 @@ class AnimeController :
}
private fun updateToolbarTitleAlpha(@FloatRange(from = 0.0, to = 1.0) alpha: Float? = null) {
// Controller may actually already be destroyed by the time this gets run
binding ?: return
val scrolledList = binding.fullRecycler ?: binding.infoRecycler!!
(activity as? MainActivity)?.binding?.appbar?.titleTextAlpha = when {
// Specific alpha provided
@ -417,16 +415,19 @@ class AnimeController :
val context = view?.context ?: return
val adapter = episodesAdapter ?: return
val fab = actionFab ?: return
fab.isVisible = adapter.items.any { !it.seen }
if (adapter.items.any { it.seen }) {
fab.text = context.getString(R.string.action_resume)
}
if (adapter.items.any { !it.seen }) {
fab.show()
} else {
fab.hide()
}
}
override fun onDestroyView(view: View) {
recyclerViewUpdatesToolbarTitleAlpha(false)
destroyActionModeIfNeeded()
binding.actionToolbar.destroy()
animeInfoAdapter = null
episodesHeaderAdapter = null
episodesAdapter = null
@ -1157,11 +1158,7 @@ class AnimeController :
private fun createActionModeIfNeeded() {
if (actionMode == null) {
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
binding.actionToolbar.show(
actionMode!!,
R.menu.episode_selection
) { onActionItemClicked(it!!) }
actionMode = (activity as MainActivity).startActionModeAndToolbar(this)
}
}
@ -1177,6 +1174,10 @@ class AnimeController :
return true
}
override fun onCreateActionToolbar(menuInflater: MenuInflater, menu: Menu) {
menuInflater.inflate(R.menu.chapter_selection, menu)
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val count = episodesAdapter?.selectedItemCount ?: 0
if (count == 0) {
@ -1185,27 +1186,24 @@ class AnimeController :
} else {
mode.title = count.toString()
val episodes = getSelectedEpisodes()
binding.actionToolbar.findItem(R.id.action_download)?.isVisible = !isLocalSource && episodes.any { !it.isDownloaded }
binding.actionToolbar.findItem(R.id.action_delete)?.isVisible = !isLocalSource && episodes.any { it.isDownloaded }
binding.actionToolbar.findItem(R.id.action_bookmark)?.isVisible = episodes.any { !it.episode.bookmark }
binding.actionToolbar.findItem(R.id.action_remove_bookmark)?.isVisible = episodes.all { it.episode.bookmark }
binding.actionToolbar.findItem(R.id.action_mark_as_read)?.isVisible = episodes.any { !it.episode.seen }
binding.actionToolbar.findItem(R.id.action_mark_as_unread)?.isVisible = episodes.all { it.episode.seen }
binding.actionToolbar.findItem(R.id.action_play_externally)?.isVisible = !preferences.alwaysUseExternalPlayer()
binding.actionToolbar.findItem(R.id.action_play_internally)?.isVisible = preferences.alwaysUseExternalPlayer()
// Hide FAB to avoid interfering with the bottom action toolbar
actionFab?.isVisible = false
actionFab?.hide()
}
return false
return true
}
override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
val episodes = getSelectedEpisodes()
if (episodes.isEmpty()) return
toolbar.findToolbarItem(R.id.action_download)?.isVisible = !isLocalSource && episodes.any { !it.isDownloaded }
toolbar.findToolbarItem(R.id.action_delete)?.isVisible = !isLocalSource && episodes.any { it.isDownloaded }
toolbar.findToolbarItem(R.id.action_bookmark)?.isVisible = episodes.any { !it.episode.bookmark }
toolbar.findToolbarItem(R.id.action_remove_bookmark)?.isVisible = episodes.all { it.episode.bookmark }
toolbar.findToolbarItem(R.id.action_mark_as_read)?.isVisible = episodes.any { !it.episode.seen }
toolbar.findToolbarItem(R.id.action_mark_as_unread)?.isVisible = episodes.all { it.episode.seen }
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return onActionItemClicked(item)
}
private fun onActionItemClicked(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_select_all -> selectAll()
R.id.action_select_inverse -> selectInverse()
@ -1224,11 +1222,13 @@ class AnimeController :
}
override fun onDestroyActionMode(mode: ActionMode) {
binding.actionToolbar.hide()
episodesAdapter?.mode = SelectableAdapter.Mode.SINGLE
episodesAdapter?.clearSelection()
selectedEpisodes.clear()
actionMode = null
}
override fun onDestroyActionToolbar() {
updateFabVisibility()
}

View file

@ -49,7 +49,7 @@ class AnimelibAdapter(
private var boundViews = arrayListOf<View>()
private val isPerCategory by lazy { preferences.categorisedDisplaySettings().get() }
private val isPerCategory by lazy { preferences.categorizedDisplaySettings().get() }
private var currentDisplayMode = preferences.libraryDisplayMode().get()
init {

View file

@ -7,7 +7,6 @@ import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.core.view.isVisible
import com.bluelinelabs.conductor.ControllerChangeHandler
@ -16,14 +15,12 @@ import com.google.android.material.tabs.TabLayout
import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay
import com.tfcporciuncula.flow.Preference
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
import eu.kanade.tachiyomi.data.animelib.AnimelibUpdateService
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
import eu.kanade.tachiyomi.ui.anime.AnimeController
import eu.kanade.tachiyomi.ui.base.controller.RootController
@ -32,9 +29,11 @@ import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.animesource.globalsearch.GlobalAnimeSearchController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.openInBrowser
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ActionModeWithToolbar
import eu.kanade.tachiyomi.widget.EmptyView
import eu.kanade.tachiyomi.widget.materialdialogs.QuadStateTextView
import kotlinx.coroutines.flow.drop
@ -55,7 +54,7 @@ class AnimelibController(
) : SearchableNucleusController<LibraryControllerBinding, AnimelibPresenter>(bundle),
RootController,
TabbedController,
ActionMode.Callback,
ActionModeWithToolbar.Callback,
ChangeAnimeCategoriesDialog.Listener,
DeleteAnimelibAnimesDialog.Listener {
@ -67,7 +66,7 @@ class AnimelibController(
/**
* Action mode for selections.
*/
private var actionMode: ActionMode? = null
private var actionMode: ActionModeWithToolbar? = null
/**
* Currently selected animes.
@ -170,12 +169,6 @@ class AnimelibController(
override fun onViewCreated(view: View) {
super.onViewCreated(view)
binding.actionToolbar.applyInsetter {
type(navigationBars = true) {
margin(bottom = true, horizontal = true)
}
}
adapter = AnimelibAdapter(this)
binding.libraryPager.adapter = adapter
binding.libraryPager.pageSelections()
@ -202,7 +195,7 @@ class AnimelibController(
is AnimelibSettingsSheet.Filter.FilterGroup -> onFilterChanged()
is AnimelibSettingsSheet.Sort.SortGroup -> onSortChanged()
is AnimelibSettingsSheet.Display.DisplayGroup -> {
val delay = if (preferences.categorisedDisplaySettings().get()) 125L else 0L
val delay = if (preferences.categorizedDisplaySettings().get()) 125L else 0L
Observable.timer(delay, TimeUnit.MILLISECONDS, AndroidSchedulers.mainThread())
.subscribe {
@ -233,7 +226,6 @@ class AnimelibController(
override fun onDestroyView(view: View) {
destroyActionModeIfNeeded()
binding.actionToolbar.destroy()
adapter?.onDestroy()
adapter = null
settingsSheet = null
@ -377,13 +369,10 @@ class AnimelibController(
* Creates the action mode if it's not created already.
*/
fun createActionModeIfNeeded() {
if (actionMode == null) {
actionMode = (activity as AppCompatActivity).startSupportActionMode(this)
binding.actionToolbar.show(
actionMode!!,
R.menu.library_selection
) { onActionItemClicked(it!!) }
(activity as? MainActivity)?.showBottomNav(false)
val activity = activity
if (actionMode == null && activity is MainActivity) {
actionMode = activity.startActionModeAndToolbar(this)
activity.showBottomNav(false)
}
}
@ -455,6 +444,10 @@ class AnimelibController(
return true
}
override fun onCreateActionToolbar(menuInflater: MenuInflater, menu: Menu) {
menuInflater.inflate(R.menu.library_selection, menu)
}
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
val count = selectedAnimes.size
if (count == 0) {
@ -462,17 +455,17 @@ class AnimelibController(
destroyActionModeIfNeeded()
} else {
mode.title = count.toString()
binding.actionToolbar.findItem(R.id.action_download_unread)?.isVisible = selectedAnimes.any { it.source != LocalAnimeSource.ID }
}
return false
return true
}
override fun onPrepareActionToolbar(toolbar: ActionModeWithToolbar, menu: Menu) {
if (selectedAnimes.isEmpty()) return
toolbar.findToolbarItem(R.id.action_download_unread)?.isVisible =
selectedAnimes.any { it.source != LocalAnimeSource.ID }
}
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
return onActionItemClicked(item)
}
private fun onActionItemClicked(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_move_to_category -> showChangeAnimeCategoriesDialog()
R.id.action_download_unseen -> downloadUnseenEpisodes()
@ -486,12 +479,11 @@ class AnimelibController(
return true
}
override fun onDestroyActionMode(mode: ActionMode?) {
override fun onDestroyActionMode(mode: ActionMode) {
// Clear all the anime selections and notify child views.
selectedAnimes.clear()
selectionRelay.call(AnimelibSelectionEvent.Cleared())
binding.actionToolbar.hide()
(activity as? MainActivity)?.showBottomNav(true)
actionMode = null

View file

@ -129,7 +129,7 @@ class AnimelibPresenter(
val filterFnUnread: (AnimelibItem) -> Boolean = unread@{ item ->
if (filterUnread == State.IGNORE.value) return@unread true
val isUnread = item.anime.unread != 0
val isUnread = item.anime.unseen != 0
return@unread if (filterUnread == State.INCLUDE.value) isUnread
else !isUnread
@ -212,7 +212,7 @@ class AnimelibPresenter(
}
item.unreadCount = if (showUnreadBadges) {
item.anime.unread
item.anime.unseen
} else {
// Unset unread count if not enabled
-1
@ -286,10 +286,10 @@ class AnimelibPresenter(
SortModeSetting.LAST_CHECKED -> i2.anime.last_update.compareTo(i1.anime.last_update)
SortModeSetting.UNREAD -> when {
// Ensure unread content comes first
i1.anime.unread == i2.anime.unread -> 0
i1.anime.unread == 0 -> if (sortAscending) 1 else -1
i2.anime.unread == 0 -> if (sortAscending) -1 else 1
else -> i1.anime.unread.compareTo(i2.anime.unread)
i1.anime.unseen == i2.anime.unseen -> 0
i1.anime.unseen == 0 -> if (sortAscending) 1 else -1
i2.anime.unseen == 0 -> if (sortAscending) -1 else 1
else -> i1.anime.unseen.compareTo(i2.anime.unseen)
}
SortModeSetting.TOTAL_CHAPTERS -> {
val anime1TotalEpisode = totalEpisodeAnime[i1.anime.id!!] ?: 0
@ -369,7 +369,7 @@ class AnimelibPresenter(
*/
private fun getAnimelibAnimesObservable(): Observable<AnimelibMap> {
val defaultLibraryDisplayMode = preferences.libraryDisplayMode()
val shouldSetFromCategory = preferences.categorisedDisplaySettings()
val shouldSetFromCategory = preferences.categorizedDisplaySettings()
return db.getAnimelibAnimes().asRxObservable()
.map { list ->
list.map { animelibAnime ->

View file

@ -245,7 +245,7 @@ class AnimelibSettingsSheet(
SortDirectionSetting.DESCENDING
}
if (preferences.categorisedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
currentCategory?.sortDirection = flag.flag
db.insertCategory(currentCategory!!).executeAsBlocking()
@ -267,7 +267,7 @@ class AnimelibSettingsSheet(
else -> throw NotImplementedError("Unknown display mode")
}
if (preferences.categorisedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
currentCategory?.sortMode = flag.flag
db.insertCategory(currentCategory!!).executeAsBlocking()
@ -304,7 +304,7 @@ class AnimelibSettingsSheet(
// Gets user preference of currently selected display mode at current category
private fun getDisplayModePreference(): DisplayModeSetting {
return if (preferences.categorisedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
return if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
DisplayModeSetting.fromFlag(currentCategory?.displayMode)
} else {
preferences.libraryDisplayMode().get()
@ -353,7 +353,7 @@ class AnimelibSettingsSheet(
else -> throw NotImplementedError("Unknown display mode")
}
if (preferences.categorisedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
if (preferences.categorizedDisplaySettings().get() && currentCategory != null && currentCategory?.id != 0) {
currentCategory?.displayMode = flag.flag
db.insertCategory(currentCategory!!).executeAsBlocking()

View file

@ -45,7 +45,7 @@ open class BasePresenter<V> : RxPresenter<V>() {
* @param onNext function to execute when the observable emits an item.
* @param onError function to execute when the observable throws an error.
*/
fun <T> Observable<T>.subscribeFirst(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = compose(deliverFirst<T>()).subscribe(split(onNext, onError)).apply { add(this) }
fun <T> Observable<T>.subscribeFirst(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit) = { _, _ -> }) = compose(deliverFirst<T>()).subscribe(split(onNext, onError)).apply { add(this) }
/**
* Subscribes an observable with [deliverLatestCache] and adds it to the presenter's lifecycle
@ -54,7 +54,7 @@ open class BasePresenter<V> : RxPresenter<V>() {
* @param onNext function to execute when the observable emits an item.
* @param onError function to execute when the observable throws an error.
*/
fun <T> Observable<T>.subscribeLatestCache(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = compose(deliverLatestCache<T>()).subscribe(split(onNext, onError)).apply { add(this) }
fun <T> Observable<T>.subscribeLatestCache(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit) = { _, _ -> }) = compose(deliverLatestCache<T>()).subscribe(split(onNext, onError)).apply { add(this) }
/**
* Subscribes an observable with [deliverLatestCache] and adds it to the presenter's lifecycle
@ -72,7 +72,7 @@ open class BasePresenter<V> : RxPresenter<V>() {
* @param onNext function to execute when the observable emits an item.
* @param onError function to execute when the observable throws an error.
*/
fun <T> Observable<T>.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = compose(deliverReplay<T>()).subscribe(split(onNext, onError)).apply { add(this) }
fun <T> Observable<T>.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit) = { _, _ -> }) = compose(deliverReplay<T>()).subscribe(split(onNext, onError)).apply { add(this) }
/**
* Subscribes an observable with [DeliverWithView] and adds it to the presenter's lifecycle
@ -81,7 +81,7 @@ open class BasePresenter<V> : RxPresenter<V>() {
* @param onNext function to execute when the observable emits an item.
* @param onError function to execute when the observable throws an error.
*/
fun <T> Observable<T>.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null) = compose(DeliverWithView<V, T>(view())).subscribe(split(onNext, onError)).apply { add(this) }
fun <T> Observable<T>.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit) = { _, _ -> }) = compose(DeliverWithView<V, T>(view())).subscribe(split(onNext, onError)).apply { add(this) }
/**
* A deliverable that only emits to the view if attached, otherwise the event is ignored.

View file

@ -13,6 +13,7 @@ import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.databinding.ExtensionControllerBinding
import eu.kanade.tachiyomi.extension.model.AnimeExtension
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
@ -56,7 +57,8 @@ open class AnimeExtensionController :
return AnimeExtensionPresenter(activity!!)
}
override fun createBinding(inflater: LayoutInflater) = ExtensionControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater) =
ExtensionControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
@ -189,11 +191,22 @@ open class AnimeExtensionController :
private fun updateExtensionsList() {
if (query.isNotBlank()) {
val extensionNames = query.split(",")
val queries = query.split(",")
adapter?.updateDataSet(
extensions.filter {
extensionNames.any { queriedName ->
it.extension.name.contains(queriedName, ignoreCase = true)
queries.any { query ->
when (it.extension) {
is AnimeExtension.Installed -> {
it.extension.sources.any {
it.name.contains(query, ignoreCase = true) ||
it.id == query.toLongOrNull() ||
if (it is AnimeHttpSource) { it.baseUrl.contains(query, ignoreCase = true) } else false
} || it.extension.name.contains(query, ignoreCase = true)
}
is AnimeExtension.Untrusted, is AnimeExtension.Available -> {
it.extension.name.contains(query, ignoreCase = true)
}
}
}
}
)

View file

@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.browse.animeextension
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.extension.AnimeExtensionManager
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.LocaleHelper

View file

@ -63,9 +63,17 @@ open class AnimeExtensionPresenter(
val items = mutableListOf<AnimeExtensionItem>()
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwSources || !it.isNsfw) }.sortedBy { it.name }
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwSources || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.name }))
val untrustedSorted = untrusted.sortedBy { it.name }
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwSources || !it.isNsfw) }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwSources || !it.isNsfw) }
.sortedWith(
compareBy<AnimeExtension.Installed> { !it.isObsolete }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }
)
val untrustedSorted = untrusted.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
val availableSorted = available
// Filter out already installed extensions and disabled languages
.filter { avail ->
@ -74,7 +82,7 @@ open class AnimeExtensionPresenter(
avail.lang in activeLangs &&
(showNsfwSources || !avail.isNsfw)
}
.sortedBy { it.name }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
if (updatesSorted.isNotEmpty()) {
val header = AnimeExtensionGroupItem(context.getString(R.string.ext_updates_pending), updatesSorted.size, true)

View file

@ -26,8 +26,6 @@ import eu.kanade.tachiyomi.animesource.getPreferenceKey
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
import eu.kanade.tachiyomi.extension.model.AnimeExtension
import eu.kanade.tachiyomi.network.NetworkHelper
@ -35,8 +33,9 @@ import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.preference.DSL
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.switchSettingsPreference
import eu.kanade.tachiyomi.util.system.LocaleHelper
@ -122,11 +121,7 @@ class AnimeExtensionDetailsController(bundle: Bundle? = null) :
.map { source -> LocaleHelper.getSourceDisplayName(source.lang, context) to source }
.sortedWith(compareBy({ (_, source) -> !source.isEnabled() }, { (lang, _) -> lang.lowercase() }))
.forEach { (lang, source) ->
val preferenceBlock = {
sourceSwitchPreference(source, LocaleHelper.getSourceDisplayName(lang, context))
}
preferenceBlock()
sourceSwitchPreference(source, lang)
}
}
@ -135,20 +130,12 @@ class AnimeExtensionDetailsController(bundle: Bundle? = null) :
.groupBy { (it as AnimeCatalogueSource).lang }
.toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) })
.forEach { entry ->
val preferenceBlock = {
entry.value
.sortedWith(compareBy({ source -> !source.isEnabled() }, { source -> source.name.lowercase() }))
.forEach { source ->
sourceSwitchPreference(source, source.toString())
}
}
preferenceCategory {
title = LocaleHelper.getSourceDisplayName(entry.key, context)
preferenceBlock()
}
}
}
private fun PreferenceScreen.sourceSwitchPreference(source: AnimeSource, name: String) {

View file

@ -20,8 +20,6 @@ import eu.kanade.tachiyomi.animesource.AnimeCatalogueSource
import eu.kanade.tachiyomi.animesource.AnimeSource
import eu.kanade.tachiyomi.animesource.LocalAnimeSource
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
@ -32,6 +30,8 @@ import eu.kanade.tachiyomi.ui.browse.animesource.browse.BrowseAnimeSourceControl
import eu.kanade.tachiyomi.ui.browse.animesource.globalsearch.GlobalAnimeSearchController
import eu.kanade.tachiyomi.ui.browse.animesource.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get

View file

@ -9,10 +9,10 @@ import eu.kanade.tachiyomi.animesource.AnimeSourceManager
import eu.kanade.tachiyomi.animesource.getPreferenceKey
import eu.kanade.tachiyomi.animesource.icon
import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.preference.switchPreferenceCategory
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.LocaleHelper
@ -42,7 +42,7 @@ class AnimeSourceFilterController : SettingsController() {
)
orderedLangs.forEach { lang ->
val sources = sourcesByLang[lang].orEmpty().sortedBy { it.name.lowercase() }
val sources = sourcesByLang[lang].orEmpty().sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
// Create a preference group and set initial state and change listener
switchPreferenceCategory {

View file

@ -29,7 +29,6 @@ import eu.kanade.tachiyomi.animesource.online.AnimeHttpSource
import eu.kanade.tachiyomi.data.database.models.Anime
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.databinding.SourceControllerBinding
import eu.kanade.tachiyomi.ui.anime.AnimeController
import eu.kanade.tachiyomi.ui.animelib.ChangeAnimeCategoriesDialog
@ -41,6 +40,7 @@ import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.openInBrowser
@ -159,13 +159,12 @@ open class BrowseAnimeSourceController(bundle: Bundle) :
)
filterSheet?.setFilters(presenter.filterItems)
// TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly
filterSheet?.setOnShowListener { actionFab?.isVisible = false }
filterSheet?.setOnDismissListener { actionFab?.isVisible = true }
filterSheet?.setOnShowListener { actionFab?.hide() }
filterSheet?.setOnDismissListener { actionFab?.show() }
actionFab?.setOnClickListener { filterSheet?.show() }
actionFab?.isVisible = true
actionFab?.show()
}
override fun configureFab(fab: ExtendedFloatingActionButton) {
@ -175,7 +174,7 @@ open class BrowseAnimeSourceController(bundle: Bundle) :
fab.setIconResource(R.drawable.ic_filter_list_24dp)
// Controlled by initFilterSheet()
fab.isVisible = false
fab.hide()
initFilterSheet()
}

View file

@ -209,7 +209,9 @@ open class GlobalAnimeSearchController(
* Opens a catalogue with the given search.
*/
override fun onTitleClick(source: AnimeCatalogueSource) {
presenter.preferences.lastUsedAnimeSource().set(source.id)
if (!preferences.incognitoMode().get()) {
preferences.lastUsedSource().set(source.id)
}
router.pushController(BrowseAnimeSourceController(source, presenter.query).withFadeTransaction())
}
}

View file

@ -15,6 +15,7 @@ import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ExtensionControllerBinding
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.BrowseController
@ -56,7 +57,8 @@ open class ExtensionController :
return ExtensionPresenter(activity!!)
}
override fun createBinding(inflater: LayoutInflater) = ExtensionControllerBinding.inflate(inflater)
override fun createBinding(inflater: LayoutInflater) =
ExtensionControllerBinding.inflate(inflater)
override fun onViewCreated(view: View) {
super.onViewCreated(view)
@ -189,11 +191,27 @@ open class ExtensionController :
private fun updateExtensionsList() {
if (query.isNotBlank()) {
val extensionNames = query.split(",")
val queries = query.split(",")
adapter?.updateDataSet(
extensions.filter {
extensionNames.any { queriedName ->
it.extension.name.contains(queriedName, ignoreCase = true)
queries.any { query ->
when (it.extension) {
is Extension.Available -> {
it.extension.sources.any {
it.name.contains(query, ignoreCase = true) ||
it.baseUrl.contains(query, ignoreCase = true) ||
it.id == query.toLongOrNull()
} || it.extension.name.contains(query, ignoreCase = true)
}
is Extension.Installed -> {
it.extension.sources.any {
it.name.contains(query, ignoreCase = true) ||
it.id == query.toLongOrNull() ||
if (it is HttpSource) { it.baseUrl.contains(query, ignoreCase = true) } else false
} || it.extension.name.contains(query, ignoreCase = true)
}
is Extension.Untrusted -> it.extension.name.contains(query, ignoreCase = true)
}
}
}
)

View file

@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.ui.browse.extension
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.LocaleHelper

View file

@ -63,9 +63,17 @@ open class ExtensionPresenter(
val items = mutableListOf<ExtensionItem>()
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwSources || !it.isNsfw) }.sortedBy { it.name }
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwSources || !it.isNsfw) }.sortedWith(compareBy({ !it.isObsolete }, { it.name }))
val untrustedSorted = untrusted.sortedBy { it.name }
val updatesSorted = installed.filter { it.hasUpdate && (showNsfwSources || !it.isNsfw) }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
val installedSorted = installed.filter { !it.hasUpdate && (showNsfwSources || !it.isNsfw) }
.sortedWith(
compareBy<Extension.Installed> { !it.isObsolete }
.thenBy(String.CASE_INSENSITIVE_ORDER) { it.name }
)
val untrustedSorted = untrusted.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
val availableSorted = available
// Filter out already installed extensions and disabled languages
.filter { avail ->
@ -74,7 +82,7 @@ open class ExtensionPresenter(
avail.lang in activeLangs &&
(showNsfwSources || !avail.isNsfw)
}
.sortedBy { it.name }
.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
if (updatesSorted.isNotEmpty()) {
val header = ExtensionGroupItem(context.getString(R.string.ext_updates_pending), updatesSorted.size, true)

View file

@ -21,8 +21,6 @@ import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.network.NetworkHelper
@ -35,8 +33,9 @@ import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.preference.DSL
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.switchSettingsPreference
import eu.kanade.tachiyomi.util.system.LocaleHelper
@ -122,11 +121,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
.map { source -> LocaleHelper.getSourceDisplayName(source.lang, context) to source }
.sortedWith(compareBy({ (_, source) -> !source.isEnabled() }, { (lang, _) -> lang.lowercase() }))
.forEach { (lang, source) ->
val preferenceBlock = {
sourceSwitchPreference(source, LocaleHelper.getSourceDisplayName(lang, context))
}
preferenceBlock()
sourceSwitchPreference(source, lang)
}
}
@ -135,20 +130,12 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
.groupBy { (it as CatalogueSource).lang }
.toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) })
.forEach { entry ->
val preferenceBlock = {
entry.value
.sortedWith(compareBy({ source -> !source.isEnabled() }, { source -> source.name.lowercase() }))
.forEach { source ->
sourceSwitchPreference(source, source.toString())
}
}
preferenceCategory {
title = LocaleHelper.getSourceDisplayName(entry.key, context)
preferenceBlock()
}
}
}
private fun PreferenceScreen.sourceSwitchPreference(source: Source, name: String) {

View file

@ -17,8 +17,6 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
@ -32,6 +30,8 @@ import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.view.onAnimationsFinished
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get

View file

@ -5,14 +5,14 @@ import androidx.preference.CheckBoxPreference
import androidx.preference.PreferenceGroup
import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.preference.minusAssign
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.plusAssign
import eu.kanade.tachiyomi.util.preference.switchPreferenceCategory
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.LocaleHelper
@ -42,7 +42,7 @@ class SourceFilterController : SettingsController() {
)
orderedLangs.forEach { lang ->
val sources = sourcesByLang[lang].orEmpty().sortedBy { it.name.lowercase() }
val sources = sourcesByLang[lang].orEmpty().sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
// Create a preference group and set initial state and change listener
switchPreferenceCategory {

View file

@ -24,7 +24,6 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.databinding.SourceControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
@ -41,6 +40,7 @@ import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.openInBrowser
@ -159,13 +159,12 @@ open class BrowseSourceController(bundle: Bundle) :
)
filterSheet?.setFilters(presenter.filterItems)
// TODO: [ExtendedFloatingActionButton] hide/show methods don't work properly
filterSheet?.setOnShowListener { actionFab?.isVisible = false }
filterSheet?.setOnDismissListener { actionFab?.isVisible = true }
filterSheet?.setOnShowListener { actionFab?.hide() }
filterSheet?.setOnDismissListener { actionFab?.show() }
actionFab?.setOnClickListener { filterSheet?.show() }
actionFab?.isVisible = true
actionFab?.show()
}
override fun configureFab(fab: ExtendedFloatingActionButton) {
@ -175,7 +174,7 @@ open class BrowseSourceController(bundle: Bundle) :
fab.setIconResource(R.drawable.ic_filter_list_24dp)
// Controlled by initFilterSheet()
fab.isVisible = false
fab.hide()
initFilterSheet()
}

View file

@ -209,7 +209,9 @@ open class GlobalSearchController(
* Opens a catalogue with the given search.
*/
override fun onTitleClick(source: CatalogueSource) {
presenter.preferences.lastUsedSource().set(source.id)
if (!preferences.incognitoMode().get()) {
preferences.lastUsedSource().set(source.id)
}
router.pushController(BrowseSourceController(source, presenter.query).withFadeTransaction())
}
}

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