Compare commits

...

67 Commits

Author SHA1 Message Date
26ddc6e3aa Release v0.13.5 2022-07-08 15:52:48 -04:00
1dc4a52f61 Bump dependencies 2022-07-08 09:11:36 -04:00
473a4fec70 Fix cherry pick errors 2022-07-08 09:11:28 -04:00
1919c2d925 Update default user agent string
(cherry picked from commit 7d3fe0ed43)
2022-07-08 08:58:55 -04:00
71e31e6c03 Add MIME type mapping for image/jxl (fixes #7117)
(cherry picked from commit 591df8abcc)
2022-07-08 08:58:46 -04:00
c01df7f0a1 Increase height of transition view in webtoon viewers (fixes #7242)
(cherry picked from commit 46734c525f)
2022-07-08 08:57:45 -04:00
6024f6175b Extension API: change fallback source and logic (#7400)
* Extension API: change fallback source and logic

* remove ghproxy

(cherry picked from commit 284445c364)
2022-07-08 08:56:51 -04:00
33500e5b69 RateLimitInterceptor: ignore canceled calls (#7389)
* RateLimitInterceptor: ignore canceled calls

* SpecificHostRateLimit: ignore canceled calls

(cherry picked from commit 5b8cd68cf3)
2022-07-08 08:56:29 -04:00
17899a6d6d Add new "Lavender" theme (#7343)
* Add new "Lavender" theme

* Add light theme values for Lavender theme

* Fix order of enums

* Fix accented UI elements in set categories sheet being different colors

Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
(cherry picked from commit ad106bd884)
2022-07-08 08:56:15 -04:00
4c3eb68d3a Use primary color for excluded tristate filter icon (fixes #7360)
(cherry picked from commit 3ca1ce4636)
2022-07-08 08:55:59 -04:00
29ced9642d Fix downloader crash related to UnmeteredSource (#7365)
Fix crash when starting a download with chaqpters from a UnmeteredSource

(cherry picked from commit 470a576441)
2022-07-08 08:55:52 -04:00
af82591d85 Fix accented UI elements in library sheet being different colors
(cherry picked from commit cd5bcc3673)
2022-07-08 08:55:38 -04:00
5bc4a446ec Fix wrapped long page numbers in reader (closes #7300)
(cherry picked from commit 6bc484617e)
2022-07-08 08:55:01 -04:00
83e93b254e Don't show clipboard copy confirmation toast on Android 13 or above
(cherry picked from commit 40f5d26945)
2022-07-08 08:53:46 -04:00
49c7dd0cac Add more DoH providers (#7256)
* Add more DoH providers

* Fix IPs

(cherry picked from commit 18ea6c4f65)
2022-07-08 08:53:41 -04:00
96d2fb62e4 ChapterSourceSync: set default timestamp to max timestamp (#7197)
(cherry picked from commit dd5da56695)
2022-07-08 08:53:19 -04:00
c76a136d3f Fix global update ignoring network constraint (#7188)
* update library update network constraint logic

* add explicit 'only on unmetered network' update constraint

(cherry picked from commit 63238b388d)
2022-07-08 08:52:49 -04:00
940409a4c3 Local Source - qol, cleanup and cover related fixes (#7166)
* Local Source - qol, cleanup and cover related fixes

* Review Changes

(cherry picked from commit ad17eb1386)
2022-07-08 08:52:26 -04:00
071dd88ef8 Add ability to show manga when clicking item in migration search process (#7134)
(cherry picked from commit bbb69482e1)
2022-07-08 08:51:26 -04:00
a58a4634e2 Fix reader menu appearing then disappearing in webtoon viewer when there is no next chapter (#7115)
(cherry picked from commit 6580f5771f)
2022-07-08 08:51:13 -04:00
5979e72662 Fix webtoon viewer showing transition view when going to next/prev chapter using next/prev button (#7133)
(cherry picked from commit b21bcc2d45)
2022-07-08 08:51:04 -04:00
010436e797 Change jsDelivr CDN URL to Fastly (#7156)
(cherry picked from commit 7b242bf118)
2022-07-08 08:50:54 -04:00
980709cccb Use jsDelivr as fallback when GitHub can't be reached for extensions (closes #5517)
Re-implementation of 24bb2f02dc

(cherry picked from commit d61bfd7caf)
2022-07-08 08:50:35 -04:00
fe80356756 Save reader progress when activity is paused (#7121)
(cherry picked from commit f1ab34e27c)
2022-07-08 08:50:06 -04:00
cecf532ffd Fix category tabs incorrect scroll position (#7120)
(cherry picked from commit 6d655ff757)
2022-07-08 08:49:57 -04:00
6cb255e60a Add switch to DownloadPageLoader when chapter is downloaded (#7119)
(cherry picked from commit 63627c81eb)
2022-07-08 08:49:48 -04:00
b46fb7d1e1 Fix "Move to top" showing at the most top item in download queue (#7109)
(cherry picked from commit b26daf8824)
2022-07-08 08:49:21 -04:00
8874193927 Update build workflow actions
(cherry picked from commit 8bee5accb7)
2022-07-08 08:49:04 -04:00
a4515ad251 Check for app updates by comparing semver (#7100)
Instead of just checking whether the current app version *matches* with
latest app version in GitHub Releases, compare the semver from the tag
names to check whether the latter is greater and the app needs an update

Reference: semver spec #11 https://semver.org/#spec-item-11

Co-authored-by: Andreas <6576096+ghostbear@users.noreply.github.com>

Co-authored-by: Andreas <6576096+ghostbear@users.noreply.github.com>
(cherry picked from commit e7ed130f2a)
2022-07-08 08:48:39 -04:00
55b0b57699 Use theme primary color for slider track (#7102)
(cherry picked from commit bc053580ad)
2022-07-08 08:48:00 -04:00
aab7795b4c Don't save categories in backup if not selected (#7101)
Currently, manually created backups contain list of categories even if
Categories option is not selected during Backup Prompt. This leads to
empty categories being created when restoring such backup files

This commit adds a check before saving categories list info to the
backup file. The check is the same check which is used while backing up
category info of manga in library

Tested and worked successfully on app installed on Android 12

(cherry picked from commit 11c01235ac)
2022-07-08 08:47:51 -04:00
196a8e6829 Rename "navigation layout" to "tap zones"
(cherry picked from commit c49d862fc5)
2022-07-08 08:47:43 -04:00
972cd98d7b Fix removing manga from library reverts during global update (#7063)
* Fix removing manga from library reverts during global update

* Review Changes

* Review changes 2

(cherry picked from commit c4088bad12)
2022-07-08 08:47:12 -04:00
a16b5d241b Add -r flag to ShizukuInstaller createCommand (#7080)
(cherry picked from commit 49d3ddb830)
2022-07-08 08:46:49 -04:00
bfa918140f Fix Android 13 icon sizing
(cherry picked from commit 9fdc803c14)
2022-07-08 08:46:27 -04:00
0721de5b81 Add links to website FAQ for library update and download warning notifications
(cherry picked from commit 70698e6494)
2022-07-08 08:45:48 -04:00
a409fde519 Download new chapters when only excluded categories is selected (#6984)
(cherry picked from commit 06bec0ad54)
2022-07-08 08:45:29 -04:00
8e34a30dce Fix skipped library entries and size warning notifications using same ID
(cherry picked from commit 91ed3a4a5f)
2022-07-08 08:43:55 -04:00
ba43462041 Fix update warning notifications being cut off (fixes #6983)
(cherry picked from commit 20145f7a12)
2022-07-08 08:43:47 -04:00
c8ae936ce9 Default to downloading as CBZ (closes #6942)
Generally seems fine. People with weak devices may experience some issues, but they can toggle it off/extract the archives separately if needed.

(cherry picked from commit 883945e3e8)
2022-07-08 08:43:39 -04:00
853f949140 Add battery not low restriction for global updates (closes #6980)
(cherry picked from commit 3feea71146)
2022-07-08 08:43:31 -04:00
615b01a006 Fix chapter transition setting for one page chapters (#6998)
(cherry picked from commit 5e32b8e49f)
2022-07-08 08:43:14 -04:00
0eb5a3176b Delete entire app_webview folder when clearing WebView data
(cherry picked from commit 6e95fde4ec)
2022-07-08 08:43:01 -04:00
867a5a3ea0 Move clear webview data action to network group
(cherry picked from commit bf0bb5aa88)
2022-07-08 08:42:45 -04:00
42eaaa497f Release v0.13.4 2022-04-22 17:29:18 -04:00
96c894ce5b Revert history Compose/SQLDelight changes 2022-04-22 17:27:58 -04:00
c0214103a9 Temporarily remove chapter name cleaning
To be added back in a more consistent manner later around the app. Probably when more things are Compose-y with less repetition.
2022-04-22 14:03:43 -04:00
2b76a97989 Add advanced setting to clear WebView data 2022-04-22 14:00:42 -04:00
9d77052d9c Enable verbose logging in dev flavor by default (#6979) 2022-04-22 12:34:53 -04:00
b4981058a2 Add indexes to creational tables (#6974) 2022-04-22 08:03:07 -04:00
032aa64195 Lift Compose theme to abstract controller 2022-04-21 22:58:28 -04:00
7c8e8317a8 Simplify history item description building 2022-04-21 22:47:51 -04:00
eb1cfc4cd4 Add abstract ComposeController 2022-04-21 22:42:37 -04:00
f1e5cccee7 Add placeholder color for Compose manga covers 2022-04-21 19:02:54 -04:00
bc2ed763bd Default auto backups to 2 2022-04-21 17:13:33 -04:00
a35995b898 Fix crash on History tab when there is no next chapter (#6970) 2022-04-21 16:48:45 -04:00
b1f46ed830 Migrate History screen database calls to SQLDelight (#6933)
* Migrate History screen database call to SQLDelight

- Move all migrations to SQLDelight
- Move all tables to SQLDelight

Co-authored-by: inorichi <3521738+inorichi@users.noreply.github.com>

* Changes from review comments

* Add adapters to database

* Remove logging of database version in App

* Change query name for paging source queries

* Update migrations

* Make SQLite Callback handle migration

- To ensure it updates the database

* Use SQLDelight Schema version for Callback database version

Co-authored-by: inorichi <3521738+inorichi@users.noreply.github.com>
2022-04-21 15:45:56 -04:00
6c1565a7d4 Make links in new update dialog clickable
Co-authored-by: Jays2Kings <Jays2Kings@users.noreply.github.com>
2022-04-19 22:39:33 -04:00
2ca6b655ad Replace ignore button in new update dialog with link to GitHub page
Not enough room for 3 buttons. Users can still tap outside or back out of the dialog if they want to ignore it.
2022-04-18 22:45:58 -04:00
a83a481ac8 Update junrar 2022-04-18 17:14:47 -04:00
65a8b63b3b Move chapter name cleaning logic to holder (fixes #6955) 2022-04-18 09:26:43 -04:00
b20ca36db9 Fix AppBar not unlifting when scrolling using ComposeView (#6952) 2022-04-17 14:33:35 -04:00
189f92d7e8 Show better error message when empty backup creation is attempted (closes #6941) 2022-04-17 11:51:24 -04:00
cdd4ec6233 Increase default OkHttp call timeout to 2 minutes
Which is still stupidly high, but maybe it'll be lenient enough for certain people.
2022-04-17 11:32:47 -04:00
ef1bb4e800 Show parsed Markdown for new version info (closes #6940) 2022-04-17 11:30:05 -04:00
c475acd1ea Migrate History screen to Compose (#6922)
* Migrate History screen to Compose

- Migrate screen
- Strip logic from presenter into use cases and repository
- Setup for other screen being able to migrate to Compose with Theme

* Changes from review comments
2022-04-17 10:36:22 -04:00
7d50d7ff52 Add elevation to navigation rails (#6947)
Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
2022-04-17 10:29:09 -04:00
67 changed files with 846 additions and 379 deletions

View File

@ -3,7 +3,7 @@
I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v0.13.3)
- To the latest version of the app (stable is v0.13.5)
- All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions

View File

@ -53,7 +53,7 @@ body:
label: Tachiyomi version
description: You can find your Tachiyomi version in **More → About**.
placeholder: |
Example: "0.13.3"
Example: "0.13.5"
validations:
required: true
@ -98,7 +98,7 @@ body:
required: true
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
required: true
- label: I have updated the app to version **[0.13.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
- label: I have updated the app to version **[0.13.5](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true
- label: I have updated all installed extensions.
required: true

View File

@ -33,7 +33,7 @@ body:
required: true
- label: If this is an issue with an extension, I should be opening an issue in the [extensions repository](https://github.com/tachiyomiorg/tachiyomi-extensions/issues/new/choose).
required: true
- label: I have updated the app to version **[0.13.3](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
- label: I have updated the app to version **[0.13.5](https://github.com/tachiyomiorg/tachiyomi/releases/latest)**.
required: true
- label: I will fill out all of the requested information in this form.
required: true

View File

@ -24,9 +24,10 @@ jobs:
uses: actions/dependency-review-action@v1
- name: Set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
java-version: 11
distribution: adopt
- name: Copy CI gradle.properties
run: |

View File

@ -25,9 +25,10 @@ jobs:
uses: gradle/wrapper-validation-action@v1
- name: Set up JDK 11
uses: actions/setup-java@v1
uses: actions/setup-java@v3
with:
java-version: 11
distribution: adopt
- name: Copy CI gradle.properties
run: |

View File

@ -24,8 +24,8 @@ android {
applicationId = "eu.kanade.tachiyomi"
minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk
versionCode = 79
versionName = "0.13.3"
versionCode = 81
versionName = "0.13.5"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -219,6 +219,7 @@ dependencies {
exclude(group = "androidx.viewpager", module = "viewpager")
}
implementation(libs.insetter)
implementation(libs.markwon)
// Conductor
implementation(libs.bundles.conductor)

View File

@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.util.preference.asImmediateFlow
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.animatorDurationScale
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.notification
import kotlinx.coroutines.flow.launchIn
@ -174,7 +175,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
}
protected open fun setupAcra() {
if (BuildConfig.FLAVOR != "dev") {
if (isDevFlavor.not()) {
initAcra {
buildConfigClass = BuildConfig::class.java
excludeMatchingSharedPreferencesKeys = listOf(".*username.*", ".*password.*", ".*token.*")

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.full
import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
@ -54,7 +55,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
backup = Backup(
backupManga(databaseManga, flags),
backupCategories(),
backupCategories(flags),
emptyList(),
backupExtensionInfo(databaseManga),
)
@ -90,6 +91,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
}
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
if (byteArray.isEmpty()) {
throw IllegalStateException(context.getString(R.string.empty_backup_error))
}
file.openOutputStream().also {
// Force overwrite old file
(it as? FileOutputStream)?.channel?.truncate(0)
@ -128,10 +133,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*
* @return list of [BackupCategory] to be backed up
*/
private fun backupCategories(): List<BackupCategory> {
return databaseHelper.getCategories()
.executeAsBlocking()
.map { BackupCategory.copyFrom(it) }
private fun backupCategories(options: Int): List<BackupCategory> {
// Check if user wants category information in backup
return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
databaseHelper.getCategories()
.executeAsBlocking()
.map { BackupCategory.copyFrom(it) }
} else {
emptyList()
}
}
/**

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.download
import android.app.PendingIntent
import android.content.Context
import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat
@ -187,16 +188,17 @@ internal class DownloadNotifier(private val context: Context) {
* @param timeout duration after which to automatically dismiss the notification.
* Only works on Android 8+.
*/
fun onWarning(reason: String, timeout: Long? = null) {
fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) {
with(errorNotificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(reason)
setStyle(NotificationCompat.BigTextStyle().bigText(reason))
setSmallIcon(R.drawable.ic_warning_white_24dp)
setAutoCancel(true)
clearActions()
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
timeout?.let { setTimeoutAfter(it) }
contentIntent?.let { setContentIntent(it) }
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
}

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.webkit.MimeTypeMap
import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay
@ -11,6 +10,8 @@ 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.library.LibraryUpdateNotifier
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.UnmeteredSource
@ -276,7 +277,8 @@ class Downloader(
val maxDownloadsFromSource = queue
.groupBy { it.source }
.filterKeys { it !is UnmeteredSource }
.maxOf { it.value.size }
.maxOfOrNull { it.value.size }
?: 0
if (
queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
@ -285,6 +287,7 @@ class Downloader(
notifier.onWarning(
context.getString(R.string.download_queue_size_warning),
WARNING_NOTIF_TIMEOUT_MS,
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
)
}
}
@ -468,7 +471,7 @@ class Downloader(
// Else read magic numbers.
?: ImageUtil.findImageType { file.openInputStream() }?.mime
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
return ImageUtil.getExtensionFromMimeType(mime)
}
/**

View File

@ -8,9 +8,7 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
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.data.preference.*
import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -21,8 +19,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>()
if (requiresWifiConnection(preferences) && !context.isConnectedToWifi()) {
Result.failure()
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
return Result.failure()
}
return if (LibraryUpdateService.start(context)) {
@ -41,8 +40,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (interval > 0) {
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
val constraints = Constraints.Builder()
.setRequiredNetworkType(NetworkType.CONNECTED)
.setRequiredNetworkType(if (DEVICE_NETWORK_NOT_METERED in restrictions) { NetworkType.UNMETERED } else { NetworkType.CONNECTED })
.setRequiresCharging(DEVICE_CHARGING in restrictions)
.setRequiresBatteryNotLow(DEVICE_BATTERY_NOT_LOW in restrictions)
.build()
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
@ -60,10 +60,5 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
}
}
fun requiresWifiConnection(preferences: PreferencesHelper): Boolean {
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
return DEVICE_ONLY_ON_WIFI in restrictions
}
}
}

View File

@ -93,9 +93,10 @@ class LibraryUpdateNotifier(private val context: Context) {
fun showQueueSizeWarningNotification() {
val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
setContentTitle(context.getString(R.string.label_warning))
setContentText(context.getString(R.string.notification_size_warning))
setStyle(NotificationCompat.BigTextStyle().bigText(context.getString(R.string.notification_size_warning)))
setSmallIcon(R.drawable.ic_warning_white_24dp)
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
setContentIntent(NotificationHandler.openUrl(context, HELP_WARNING_URL))
}
context.notificationManager.notify(
@ -340,6 +341,10 @@ class LibraryUpdateNotifier(private val context: Context) {
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
companion object {
const val HELP_WARNING_URL = "https://tachiyomi.org/help/faq/#why-does-the-app-warn-about-large-bulk-updates-and-downloads"
}
}
private const val NOTIF_MAX_CHAPTERS = 5

View File

@ -174,6 +174,8 @@ class LibraryUpdateService(
*/
override fun onDestroy() {
updateJob?.cancel()
// Despite what Android Studio
// states this can be null
ioScope?.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
@ -233,8 +235,7 @@ class LibraryUpdateService(
/**
* Adds list of manga to be updated.
*
* @param category the ID of the category to update, or -1 if no category specified.
* @param target the target to update.
* @param categoryId the ID of the category to update, or -1 if no category specified.
*/
fun addMangaToQueue(categoryId: Int) {
val libraryManga = db.getLibraryMangas().executeAsBlocking()
@ -274,12 +275,11 @@ class LibraryUpdateService(
}
/**
* Method that updates the given list of manga. It's called in a background thread, so it's safe
* Method that updates manga in [mangaToUpdate]. It's called in a background thread, so it's safe
* to do heavy operations or network calls here.
* For each manga it calls [updateManga] and updates the notification showing the current
* progress.
*
* @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update.
*/
suspend fun updateChapterList() {
@ -305,35 +305,38 @@ class LibraryUpdateService(
return@async
}
// Don't continue to update if manga not in library
db.getManga(manga.id!!).executeAsBlocking() ?: return@forEach
withUpdateNotification(
currentlyUpdatingManga,
progressCount,
manga,
) { manga ->
) { mangaWithNotif ->
try {
when {
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> {
skippedUpdates.add(manga to getString(R.string.skipped_reason_completed))
}
MANGA_HAS_UNREAD in restrictions && manga.unreadCount != 0 -> {
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_caught_up))
}
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasStarted -> {
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_started))
}
MANGA_NON_COMPLETED in restrictions && mangaWithNotif.status == SManga.COMPLETED ->
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_completed))
MANGA_HAS_UNREAD in restrictions && mangaWithNotif.unreadCount != 0 ->
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_caught_up))
MANGA_NON_READ in restrictions && mangaWithNotif.totalChapters > 0 && !mangaWithNotif.hasStarted ->
skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_started))
else -> {
// Convert to the manga that contains new chapters
val (newChapters, _) = updateManga(manga)
val (newChapters, _) = updateManga(mangaWithNotif)
if (newChapters.isNotEmpty()) {
if (manga.shouldDownloadNewChapters(db, preferences)) {
downloadChapters(manga, newChapters)
if (mangaWithNotif.shouldDownloadNewChapters(db, preferences)) {
downloadChapters(mangaWithNotif, newChapters)
hasDownloads.set(true)
}
// Convert to the manga that contains new chapters
newUpdates.add(
manga to newChapters.sortedByDescending { ch -> ch.source_order }
mangaWithNotif to newChapters.sortedByDescending { ch -> ch.source_order }
.toTypedArray(),
)
}
@ -352,11 +355,11 @@ class LibraryUpdateService(
e.message
}
}
failedUpdates.add(manga to errorMessage)
failedUpdates.add(mangaWithNotif to errorMessage)
}
if (preferences.autoUpdateTrackers()) {
updateTrackings(manga, loggedServices)
updateTrackings(mangaWithNotif, loggedServices)
}
}
}
@ -404,6 +407,7 @@ class LibraryUpdateService(
suspend fun updateManga(manga: Manga): Pair<List<Chapter>, List<Chapter>> {
val source = sourceManager.getOrStub(manga.source)
var networkSManga: SManga? = null
// Update manga details metadata
if (preferences.autoUpdateMetadata()) {
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
@ -415,14 +419,26 @@ class LibraryUpdateService(
sManga.thumbnail_url = manga.thumbnail_url
}
manga.copyFrom(sManga)
db.insertManga(manga).executeAsBlocking()
networkSManga = sManga
}
val chapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() }
return syncChaptersWithSource(db, chapters, manga, source)
// Get manga from database to account for if it was removed
// from library or database
val dbManga = db.getManga(manga.id!!).executeAsBlocking()
?: return Pair(emptyList(), emptyList())
// Copy into [dbManga] to retain favourite value
networkSManga?.let {
dbManga.copyFrom(it)
db.insertManga(dbManga).executeAsBlocking()
}
// [dbmanga] was used so that manga data doesn't get overwritten
// incase manga gets new chapter
return syncChaptersWithSource(db, chapters, dbManga, source)
}
private suspend fun updateCovers() {
@ -445,16 +461,16 @@ class LibraryUpdateService(
currentlyUpdatingManga,
progressCount,
manga,
) { manga ->
sourceManager.get(manga.source)?.let { source ->
) { mangaWithNotif ->
sourceManager.get(mangaWithNotif.source)?.let { source ->
try {
val networkManga =
source.getMangaDetails(manga.toMangaInfo())
source.getMangaDetails(mangaWithNotif.toMangaInfo())
val sManga = networkManga.toSManga()
manga.prepUpdateCover(coverCache, sManga, true)
mangaWithNotif.prepUpdateCover(coverCache, sManga, true)
sManga.thumbnail_url?.let {
manga.thumbnail_url = it
db.insertManga(manga).executeAsBlocking()
mangaWithNotif.thumbnail_url = it
db.insertManga(mangaWithNotif).executeAsBlocking()
}
} catch (e: Throwable) {
// Ignore errors and continue

View File

@ -30,7 +30,7 @@ object Notifications {
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
const val ID_LIBRARY_ERROR = -102
const val CHANNEL_LIBRARY_SKIPPED = "library_skipped_channel"
const val ID_LIBRARY_SKIPPED = -103
const val ID_LIBRARY_SKIPPED = -104
/**
* Notification channel and ids used by the downloader.

View File

@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.data.preference
import eu.kanade.tachiyomi.R
const val DEVICE_ONLY_ON_WIFI = "wifi"
const val DEVICE_NETWORK_NOT_METERED = "network_not_metered"
const val DEVICE_CHARGING = "ac"
const val DEVICE_BATTERY_NOT_LOW = "battery_not_low"
const val MANGA_NON_COMPLETED = "manga_ongoing"
const val MANGA_HAS_UNREAD = "manga_fully_read"
@ -28,13 +30,14 @@ object PreferenceValues {
enum class AppTheme(val titleResId: Int?) {
DEFAULT(R.string.label_default),
MONET(R.string.theme_monet),
GREEN_APPLE(R.string.theme_greenapple),
LAVENDER(R.string.theme_lavender),
MIDNIGHT_DUSK(R.string.theme_midnightdusk),
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
YOTSUBA(R.string.theme_yotsuba),
TAKO(R.string.theme_tako),
GREEN_APPLE(R.string.theme_greenapple),
TEALTURQUOISE(R.string.theme_tealturquoise),
YINYANG(R.string.theme_yinyang),
YOTSUBA(R.string.theme_yotsuba),
// Deprecated
DARK_BLUE(null),

View File

@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import java.io.File
import java.text.DateFormat
@ -203,11 +204,11 @@ class PreferencesHelper(val context: Context) {
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true)
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", false)
fun saveChaptersAsCBZ() = flowPrefs.getBoolean("save_chapter_as_cbz", true)
fun folderPerManga() = prefs.getBoolean(Keys.folderPerManga, false)
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 1)
fun numberOfBackups() = flowPrefs.getInt("backup_slots", 2)
fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
@ -277,10 +278,10 @@ class PreferencesHelper(val context: Context) {
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
fun downloadNew() = flowPrefs.getBoolean("download_new", false)
fun downloadNewChapter() = flowPrefs.getBoolean("download_new", false)
fun downloadNewCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
fun downloadNewChapterCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
fun downloadNewChapterCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
@ -319,7 +320,7 @@ class PreferencesHelper(val context: Context) {
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER,
)
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, false)
fun verboseLogging() = prefs.getBoolean(Keys.verboseLogging, isDevFlavor)
fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)

View File

@ -56,6 +56,7 @@ class AppUpdateChecker {
private fun isNewVersion(versionTag: String): Boolean {
// Removes prefixes like "r" or "v"
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
val oldVersion = BuildConfig.VERSION_NAME.replace("[^\\d.]".toRegex(), "")
return if (BuildConfig.PREVIEW) {
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
@ -64,7 +65,15 @@ class AppUpdateChecker {
} else {
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
// tagged as something like "v0.1.2"
newVersion != BuildConfig.VERSION_NAME
val newSemVer = newVersion.split(".").map { it.toInt() }
val oldSemVer = oldVersion.split(".").map { it.toInt() }
oldSemVer.mapIndexed { index, i ->
if (newSemVer[index] > i) {
return true
}
}
false
}
}
}

View File

@ -11,7 +11,9 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.serialization.Serializable
import logcat.LogPriority
import uy.kohesive.injekt.injectLazy
import java.util.Date
import java.util.concurrent.TimeUnit
@ -21,11 +23,27 @@ internal class ExtensionGithubApi {
private val networkService: NetworkHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
private var requiresFallbackSource = false
suspend fun findExtensions(): List<Extension.Available> {
return withIOContext {
val extensions = networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.await()
val githubResponse = if (requiresFallbackSource) null else try {
networkService.client
.newCall(GET("${REPO_URL_PREFIX}index.min.json"))
.await()
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Failed to get extensions from GitHub" }
requiresFallbackSource = true
null
}
val response = githubResponse ?: run {
networkService.client
.newCall(GET("${FALLBACK_REPO_URL_PREFIX}index.min.json"))
.await()
}
val extensions = response
.parseAs<List<ExtensionJsonObject>>()
.toExtensions()
@ -85,7 +103,7 @@ internal class ExtensionGithubApi {
hasChangelog = it.hasChangelog == 1,
sources = it.sources?.toExtensionSources() ?: emptyList(),
apkName = it.apk,
iconUrl = "${REPO_URL_PREFIX}icon/${it.apk.replace(".apk", ".png")}",
iconUrl = "${getUrlPrefix()}icon/${it.apk.replace(".apk", ".png")}",
)
}
}
@ -101,11 +119,20 @@ internal class ExtensionGithubApi {
}
fun getApkUrl(extension: Extension.Available): String {
return "${REPO_URL_PREFIX}apk/${extension.apkName}"
return "${getUrlPrefix()}apk/${extension.apkName}"
}
private fun getUrlPrefix(): String {
return if (requiresFallbackSource) {
FALLBACK_REPO_URL_PREFIX
} else {
REPO_URL_PREFIX
}
}
}
private const val REPO_URL_PREFIX = "https://raw.githubusercontent.com/tachiyomiorg/tachiyomi-extensions/repo/"
private const val FALLBACK_REPO_URL_PREFIX = "https://gcore.jsdelivr.net/gh/tachiyomiorg/tachiyomi-extensions@repo/"
@Serializable
private data class ExtensionJsonObject(

View File

@ -52,9 +52,9 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
service.contentResolver.openInputStream(entry.uri)!!.use {
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
"pm install-create --user current -i ${service.packageName} -S $size"
"pm install-create --user current -r -i ${service.packageName} -S $size"
} else {
"pm install-create -i ${service.packageName} -S $size"
"pm install-create -r -i ${service.packageName} -S $size"
}
val createResult = exec(createCommand)
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value

View File

@ -13,6 +13,10 @@ const val PREF_DOH_CLOUDFLARE = 1
const val PREF_DOH_GOOGLE = 2
const val PREF_DOH_ADGUARD = 3
const val PREF_DOH_QUAD9 = 4
const val PREF_DOH_ALIDNS = 5
const val PREF_DOH_DNSPOD = 6
const val PREF_DOH_360 = 7
const val PREF_DOH_QUAD101 = 8
fun OkHttpClient.Builder.dohCloudflare() = dns(
DnsOverHttps.Builder().client(build())
@ -68,3 +72,51 @@ fun OkHttpClient.Builder.dohQuad9() = dns(
)
.build(),
)
fun OkHttpClient.Builder.dohAliDNS() = dns(
DnsOverHttps.Builder().client(build())
.url("https://dns.alidns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("223.5.5.5"),
InetAddress.getByName("223.6.6.6"),
InetAddress.getByName("2400:3200::1"),
InetAddress.getByName("2400:3200:baba::1"),
)
.build(),
)
fun OkHttpClient.Builder.dohDNSPod() = dns(
DnsOverHttps.Builder().client(build())
.url("https://doh.pub/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("1.12.12.12"),
InetAddress.getByName("120.53.53.53"),
)
.build(),
)
fun OkHttpClient.Builder.doh360() = dns(
DnsOverHttps.Builder().client(build())
.url("https://doh.360.cn/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("101.226.4.6"),
InetAddress.getByName("218.30.118.6"),
InetAddress.getByName("123.125.81.6"),
InetAddress.getByName("140.207.198.6"),
InetAddress.getByName("180.163.249.75"),
InetAddress.getByName("101.199.113.208"),
InetAddress.getByName("36.99.170.86"),
)
.build(),
)
fun OkHttpClient.Builder.dohQuad101() = dns(
DnsOverHttps.Builder().client(build())
.url("https://dns.twnic.tw/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("101.101.101.101"),
InetAddress.getByName("2001:de4::101"),
InetAddress.getByName("2001:de4::102"),
)
.build(),
)

View File

@ -27,7 +27,7 @@ class NetworkHelper(context: Context) {
.cookieJar(cookieManager)
.connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(30, TimeUnit.SECONDS)
.callTimeout(90, TimeUnit.SECONDS)
.callTimeout(2, TimeUnit.MINUTES)
// .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
.addInterceptor(UserAgentInterceptor())
@ -43,6 +43,10 @@ class NetworkHelper(context: Context) {
PREF_DOH_GOOGLE -> builder.dohGoogle()
PREF_DOH_ADGUARD -> builder.dohAdGuard()
PREF_DOH_QUAD9 -> builder.dohQuad9()
PREF_DOH_ALIDNS -> builder.dohAliDNS()
PREF_DOH_DNSPOD -> builder.dohDNSPod()
PREF_DOH_360 -> builder.doh360()
PREF_DOH_QUAD101 -> builder.dohQuad101()
}
return builder

View File

@ -4,6 +4,7 @@ import android.os.SystemClock
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.IOException
import java.util.concurrent.TimeUnit
/**
@ -36,6 +37,11 @@ private class RateLimitInterceptor(
private val rateLimitMillis = unit.toMillis(period)
override fun intercept(chain: Interceptor.Chain): Response {
// Ignore canceled calls, otherwise they would jam the queue
if (chain.call().isCanceled()) {
throw IOException()
}
synchronized(requestQueue) {
val now = SystemClock.elapsedRealtime()
val waitTime = if (requestQueue.size < permits) {
@ -51,6 +57,11 @@ private class RateLimitInterceptor(
}
}
// Final check
if (chain.call().isCanceled()) {
throw IOException()
}
if (requestQueue.size == permits) {
requestQueue.removeAt(0)
}

View File

@ -5,6 +5,7 @@ import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.OkHttpClient
import okhttp3.Response
import java.io.IOException
import java.util.concurrent.TimeUnit
/**
@ -41,9 +42,13 @@ class SpecificHostRateLimitInterceptor(
private val host = httpUrl.host
override fun intercept(chain: Interceptor.Chain): Response {
if (chain.request().url.host != host) {
// Ignore canceled calls, otherwise they would jam the queue
if (chain.call().isCanceled()) {
throw IOException()
} else if (chain.request().url.host != host) {
return chain.proceed(chain.request())
}
synchronized(requestQueue) {
val now = SystemClock.elapsedRealtime()
val waitTime = if (requestQueue.size < permits) {
@ -59,6 +64,11 @@ class SpecificHostRateLimitInterceptor(
}
}
// Final check
if (chain.call().isCanceled()) {
throw IOException()
}
if (requestQueue.size == permits) {
requestQueue.removeAt(0)
}

View File

@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.source
import android.content.Context
import com.github.junrar.Archive
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.model.MangasPage
@ -17,7 +19,6 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.EpubFile
import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.runBlocking
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
@ -26,10 +27,11 @@ import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonPrimitive
import logcat.LogPriority
import rx.Observable
import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.FileInputStream
@ -37,130 +39,104 @@ import java.io.InputStream
import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource {
companion object {
const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
private const val COVER_NAME = "cover.jpg"
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
val dir = getBaseDirectories(context).firstOrNull()
if (dir == null) {
input.close()
return null
}
var cover = getCoverFile(File("${dir.absolutePath}/${manga.url}"))
if (cover == null) {
cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
}
// It might not exist if using the external SD card
cover.parentFile?.mkdirs()
input.use {
cover.outputStream().use {
input.copyTo(it)
}
}
manga.thumbnail_url = cover.absolutePath
return cover
}
/**
* Returns valid cover file inside [parent] directory.
*/
private fun getCoverFile(parent: File): File? {
return parent.listFiles()?.find { it.nameWithoutExtension == "cover" }?.takeIf {
it.isFile && ImageUtil.isImage(it.name) { it.inputStream() }
}
}
private fun getBaseDirectories(context: Context): List<File> {
val c = context.getString(R.string.app_name) + File.separator + "local"
return DiskUtil.getExternalStorages(context).map { File(it.absolutePath, c) }
}
}
class LocalSource(
private val context: Context,
private val coverCache: CoverCache = Injekt.get(),
) : CatalogueSource, UnmeteredSource {
private val json: Json by injectLazy()
override val id = ID
override val name = context.getString(R.string.local_source)
override val lang = "other"
override val supportsLatest = true
override val name: String = context.getString(R.string.local_source)
override val id: Long = ID
override val lang: String = "other"
override fun toString() = name
override val supportsLatest: Boolean = true
// Browse related
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val baseDirs = getBaseDirectories(context)
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
var mangaDirs = baseDirs
.asSequence()
.mapNotNull { it.listFiles()?.toList() }
.flatten()
.filter { it.isDirectory }
.filterNot { it.name.startsWith('.') }
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val baseDirsFiles = getBaseDirectoriesFiles(context)
var mangaDirs = baseDirsFiles
// Filter out files that are hidden and is not a folder
.filter { it.isDirectory && !it.name.startsWith('.') }
.distinctBy { it.name }
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
when (state?.index) {
0 -> {
mangaDirs = if (state.ascending) {
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name }))
} else {
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it.name }))
}
}
1 -> {
mangaDirs = if (state.ascending) {
mangaDirs.sortedBy(File::lastModified)
} else {
mangaDirs.sortedByDescending(File::lastModified)
}
val lastModifiedLimit = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
// Filter by query or last modified
mangaDirs = mangaDirs.filter {
if (lastModifiedLimit == 0L) {
it.name.contains(query, ignoreCase = true)
} else {
it.lastModified() >= lastModifiedLimit
}
}
filters.forEach { filter ->
when (filter) {
is OrderBy -> {
when (filter.state!!.index) {
0 -> {
mangaDirs = if (filter.state!!.ascending) {
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER) { it.name })
} else {
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER) { it.name })
}
}
1 -> {
mangaDirs = if (filter.state!!.ascending) {
mangaDirs.sortedBy(File::lastModified)
} else {
mangaDirs.sortedByDescending(File::lastModified)
}
}
}
}
else -> { /* Do nothing */ }
}
}
// Transform mangaDirs to list of SManga
val mangas = mangaDirs.map { mangaDir ->
SManga.create().apply {
title = mangaDir.name
url = mangaDir.name
// Try to find the cover
for (dir in baseDirs) {
val cover = getCoverFile(File("${dir.absolutePath}/$url"))
if (cover != null && cover.exists()) {
thumbnail_url = cover.absolutePath
break
}
val cover = getCoverFile(mangaDir.name, baseDirsFiles)
if (cover != null && cover.exists()) {
thumbnail_url = cover.absolutePath
}
}
}
val sManga = this
val mangaInfo = this.toMangaInfo()
runBlocking {
val chapters = getChapterList(mangaInfo)
if (chapters.isNotEmpty()) {
val chapter = chapters.last().toSChapter()
val format = getFormat(chapter)
if (format is Format.Epub) {
EpubFile(format.file).use { epub ->
epub.fillMangaMetadata(sManga)
}
}
// Fetch chapters of all the manga
mangas.forEach { manga ->
val mangaInfo = manga.toMangaInfo()
runBlocking {
val chapters = getChapterList(mangaInfo)
if (chapters.isNotEmpty()) {
val chapter = chapters.last().toSChapter()
val format = getFormat(chapter)
// Copy the cover from the first chapter found.
if (thumbnail_url == null) {
try {
val dest = updateCover(chapter, sManga)
thumbnail_url = dest?.absolutePath
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
}
if (format is Format.Epub) {
EpubFile(format.file).use { epub ->
epub.fillMangaMetadata(manga)
}
}
// Copy the cover from the first chapter found if not available
if (manga.thumbnail_url == null) {
updateCover(chapter, manga)
}
}
}
}
@ -168,38 +144,44 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
return Observable.just(MangasPage(mangas.toList(), false))
}
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
// Manga details related
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val localDetails = getBaseDirectories(context)
.asSequence()
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
.flatten()
var mangaInfo = manga
val baseDirsFile = getBaseDirectoriesFiles(context)
val coverFile = getCoverFile(manga.key, baseDirsFile)
coverFile?.let {
mangaInfo = mangaInfo.copy(cover = it.absolutePath)
}
val localDetails = getMangaDirsFiles(manga.key, baseDirsFile)
.firstOrNull { it.extension.equals("json", ignoreCase = true) }
return if (localDetails != null) {
if (localDetails != null) {
val obj = json.decodeFromStream<JsonObject>(localDetails.inputStream())
manga.copy(
title = obj["title"]?.jsonPrimitive?.contentOrNull ?: manga.title,
author = obj["author"]?.jsonPrimitive?.contentOrNull ?: manga.author,
artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: manga.artist,
description = obj["description"]?.jsonPrimitive?.contentOrNull ?: manga.description,
genres = obj["genre"]?.jsonArray?.map { it.jsonPrimitive.content } ?: manga.genres,
status = obj["status"]?.jsonPrimitive?.intOrNull ?: manga.status,
mangaInfo = mangaInfo.copy(
title = obj["title"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.title,
author = obj["author"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.author,
artist = obj["artist"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.artist,
description = obj["description"]?.jsonPrimitive?.contentOrNull ?: mangaInfo.description,
genres = obj["genre"]?.jsonArray?.map { it.jsonPrimitive.content } ?: mangaInfo.genres,
status = obj["status"]?.jsonPrimitive?.intOrNull ?: mangaInfo.status,
)
} else {
manga
}
return mangaInfo
}
// Chapters
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
val sManga = manga.toSManga()
val chapters = getBaseDirectories(context)
.asSequence()
.mapNotNull { File(it, manga.key).listFiles()?.toList() }
.flatten()
val baseDirsFile = getBaseDirectoriesFiles(context)
return getMangaDirsFiles(manga.key, baseDirsFile)
// Only keep supported formats
.filter { it.isDirectory || isSupportedFile(it.extension) }
.map { chapterFile ->
SChapter.create().apply {
@ -211,14 +193,14 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
}
date_upload = chapterFile.lastModified()
ChapterRecognition.parseChapterNumber(this, sManga)
val format = getFormat(chapterFile)
if (format is Format.Epub) {
EpubFile(format.file).use { epub ->
epub.fillChapterMetadata(this)
}
}
ChapterRecognition.parseChapterNumber(this, sManga)
}
}
.map { it.toChapterInfo() }
@ -227,12 +209,24 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
}
.toList()
return chapters
}
override suspend fun getPageList(chapter: ChapterInfo) = throw Exception("Unused")
// Filters
override fun getFilterList() = FilterList(OrderBy(context))
private val POPULAR_FILTERS = FilterList(OrderBy(context))
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
private class OrderBy(context: Context) : Filter.Sort(
context.getString(R.string.local_filter_order_by),
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
Selection(0, true),
)
// Unused stuff
override suspend fun getPageList(chapter: ChapterInfo) = throw UnsupportedOperationException("Unused")
// Miscellaneous
private fun isSupportedFile(extension: String): Boolean {
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
}
@ -296,25 +290,89 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
}
}
}
.also { coverCache.clearMemoryCache() }
}
override fun getFilterList() = POPULAR_FILTERS
private val POPULAR_FILTERS = FilterList(OrderBy(context))
private val LATEST_FILTERS = FilterList(OrderBy(context).apply { state = Filter.Sort.Selection(1, false) })
private class OrderBy(context: Context) : Filter.Sort(
context.getString(R.string.local_filter_order_by),
arrayOf(context.getString(R.string.title), context.getString(R.string.date)),
Selection(0, true),
)
sealed class Format {
data class Directory(val file: File) : Format()
data class Zip(val file: File) : Format()
data class Rar(val file: File) : Format()
data class Epub(val file: File) : Format()
}
companion object {
const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
private const val DEFAULT_COVER_NAME = "cover.jpg"
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
private fun getBaseDirectories(context: Context): Sequence<File> {
val localFolder = context.getString(R.string.app_name) + File.separator + "local"
return DiskUtil.getExternalStorages(context)
.map { File(it.absolutePath, localFolder) }
.asSequence()
}
private fun getBaseDirectoriesFiles(context: Context): Sequence<File> {
return getBaseDirectories(context)
// Get all the files inside all baseDir
.flatMap { it.listFiles().orEmpty().toList() }
}
private fun getMangaDir(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
return baseDirsFile
// Get the first mangaDir or null
.firstOrNull { it.isDirectory && it.name == mangaUrl }
}
private fun getMangaDirsFiles(mangaUrl: String, baseDirsFile: Sequence<File>): Sequence<File> {
return baseDirsFile
// Filter out ones that are not related to the manga and is not a directory
.filter { it.isDirectory && it.name == mangaUrl }
// Get all the files inside the filtered folders
.flatMap { it.listFiles().orEmpty().toList() }
}
private fun getCoverFile(mangaUrl: String, baseDirsFile: Sequence<File>): File? {
return getMangaDirsFiles(mangaUrl, baseDirsFile)
// Get all file whose names start with 'cover'
.filter { it.isFile && it.nameWithoutExtension.equals("cover", ignoreCase = true) }
// Get the first actual image
.firstOrNull {
ImageUtil.isImage(it.name) { it.inputStream() }
}
}
fun updateCover(context: Context, manga: SManga, inputStream: InputStream): File? {
val baseDirsFiles = getBaseDirectoriesFiles(context)
val mangaDir = getMangaDir(manga.url, baseDirsFiles)
if (mangaDir == null) {
inputStream.close()
return null
}
var coverFile = getCoverFile(manga.url, baseDirsFiles)
if (coverFile == null) {
coverFile = File(mangaDir.absolutePath, DEFAULT_COVER_NAME)
}
// It might not exist at this point
coverFile.parentFile?.mkdirs()
inputStream.use { input ->
coverFile.outputStream().use { output ->
input.copyTo(output)
}
}
// Create a .nomedia file
DiskUtil.createNoMediaFile(UniFile.fromFile(mangaDir), context)
manga.thumbnail_url = coverFile.absolutePath
return coverFile
}
}
}
private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")

View File

@ -371,6 +371,6 @@ abstract class HttpSource : CatalogueSource {
override fun getFilterList() = FilterList()
companion object {
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/102.0.5005.124 Safari/537.36 Edg/102.0.1245.44"
}
}

View File

@ -97,7 +97,7 @@ abstract class DialogController : Controller {
/**
* Dismiss the dialog and pop this controller
*/
private fun dismissDialog() {
fun dismissDialog() {
if (dismissed) {
return
}

View File

@ -20,6 +20,9 @@ interface ThemingDelegate {
PreferenceValues.AppTheme.GREEN_APPLE -> {
resIds += R.style.Theme_Tachiyomi_GreenApple
}
PreferenceValues.AppTheme.LAVENDER -> {
resIds += R.style.Theme_Tachiyomi_Lavender
}
PreferenceValues.AppTheme.MIDNIGHT_DUSK -> {
resIds += R.style.Theme_Tachiyomi_MidnightDusk
}

View File

@ -129,7 +129,10 @@ class SearchController(
}
(targetController as? SearchController)?.copyManga(manga, newManga)
}
.setNeutralButton(android.R.string.cancel, null)
.setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
dismissDialog()
router.pushController(MangaController(newManga).withFadeTransaction())
}
.create()
}
}

View File

@ -42,10 +42,10 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
else -> throw Exception("Unknown state")
},
)?.apply {
val color = if (filter.state == Filter.TriState.STATE_INCLUDE) {
view.context.getResourceColor(R.attr.colorAccent)
} else {
val color = if (filter.state == Filter.TriState.STATE_IGNORE) {
view.context.getResourceColor(R.attr.colorOnBackground, 0.38f)
} else {
view.context.getResourceColor(R.attr.colorPrimary)
}
setTint(color)

View File

@ -89,7 +89,7 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
view.popupMenu(
menuRes = R.menu.download_single,
initMenu = {
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition != 0
findItem(R.id.move_to_top).isVisible = bindingAdapterPosition > 1
findItem(R.id.move_to_bottom).isVisible =
bindingAdapterPosition != adapter.itemCount - 1
},

View File

@ -8,7 +8,6 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import androidx.appcompat.view.ActionMode
import androidx.core.view.doOnAttach
import androidx.core.view.isVisible
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
@ -304,8 +303,10 @@ class LibraryController(
onTabsSettingsChanged(firstLaunch = true)
// Delay the scroll position to allow the view to be properly measured.
view.doOnAttach {
(activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true)
view.post {
if (isAttached) {
(activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true)
}
}
// Send the manga map to child fragments after the adapter is updated.

View File

@ -901,7 +901,7 @@ class MangaController :
chaptersHeader.setNumChapters(chapters.size)
val adapter = chaptersAdapter ?: return
adapter.updateDataSet(presenter.cleanChapterNames(chapters))
adapter.updateDataSet(chapters)
if (selectedChapters.isNotEmpty()) {
adapter.clearSelection() // we need to start from a clean state, index may have changed

View File

@ -431,17 +431,6 @@ class MangaPresenter(
}
}
fun cleanChapterNames(chapters: List<ChapterItem>): List<ChapterItem> {
chapters.forEach {
it.name = it.name
.trim()
.removePrefix(manga.title)
.trim(*CHAPTER_TRIM_CHARS)
}
return chapters
}
/**
* Updates the UI after applying the filters.
*/
@ -863,38 +852,3 @@ class MangaPresenter(
// Track sheet - end
}
private val CHAPTER_TRIM_CHARS = arrayOf(
// Whitespace
' ',
'\u0009',
'\u000A',
'\u000B',
'\u000C',
'\u000D',
'\u0020',
'\u0085',
'\u00A0',
'\u1680',
'\u2000',
'\u2001',
'\u2002',
'\u2003',
'\u2004',
'\u2005',
'\u2006',
'\u2007',
'\u2008',
'\u2009',
'\u200A',
'\u2028',
'\u2029',
'\u202F',
'\u205F',
'\u3000',
// Separators
'-',
'_',
',',
':',
).toCharArray()

View File

@ -6,6 +6,7 @@ import androidx.core.text.buildSpannedString
import androidx.core.text.color
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.ChaptersItemBinding
import eu.kanade.tachiyomi.source.LocalSource
@ -35,6 +36,8 @@ class ChapterHolder(
itemView.context.getString(R.string.display_mode_chapter, number)
}
else -> chapter.name
// TODO: show cleaned name consistently around the app
// else -> cleanChapterName(chapter, manga)
}
// Set correct text color
@ -80,4 +83,47 @@ class ChapterHolder(
binding.download.isVisible = item.manga.source != LocalSource.ID
binding.download.setState(item.status, item.progress)
}
private fun cleanChapterName(chapter: Chapter, manga: Manga): String {
return chapter.name
.trim()
.removePrefix(manga.title)
.trim(*CHAPTER_TRIM_CHARS)
}
}
private val CHAPTER_TRIM_CHARS = arrayOf(
// Whitespace
' ',
'\u0009',
'\u000A',
'\u000B',
'\u000C',
'\u000D',
'\u0020',
'\u0085',
'\u00A0',
'\u1680',
'\u2000',
'\u2001',
'\u2002',
'\u2003',
'\u2004',
'\u2005',
'\u2006',
'\u2007',
'\u2008',
'\u2009',
'\u200A',
'\u2028',
'\u2029',
'\u202F',
'\u205F',
'\u3000',
// Separators
'-',
'_',
',',
':',
).toCharArray()

View File

@ -121,6 +121,7 @@ class AboutController : SettingsController(), NoAppBarElevationController {
is AppUpdateResult.NoNewUpdate -> {
activity?.toast(R.string.update_check_no_new_updates)
}
else -> {}
}
} catch (error: Exception) {
activity?.toast(error.message)

View File

@ -2,35 +2,58 @@ package eu.kanade.tachiyomi.ui.more
import android.app.Dialog
import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.core.os.bundleOf
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.AppUpdateService
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import io.noties.markwon.Markwon
class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
constructor(update: AppUpdateResult.NewUpdate) : this(
bundleOf(BODY_KEY to update.release.info, URL_KEY to update.release.getDownloadLink()),
bundleOf(
BODY_KEY to update.release.info,
RELEASE_URL_KEY to update.release.releaseLink,
DOWNLOAD_URL_KEY to update.release.getDownloadLink(),
),
)
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val releaseBody = args.getString(BODY_KEY)!!
.replace("""---(\R|.)*Checksums(\R|.)*""".toRegex(), "")
val info = Markwon.create(activity!!).toMarkdown(releaseBody)
return MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.update_check_notification_update_available)
.setMessage(args.getString(BODY_KEY) ?: "")
.setMessage(info)
.setPositiveButton(R.string.update_check_confirm) { _, _ ->
val appContext = applicationContext
if (appContext != null) {
applicationContext?.let { context ->
// Start download
val url = args.getString(URL_KEY) ?: ""
AppUpdateService.start(appContext, url)
val url = args.getString(DOWNLOAD_URL_KEY)!!
AppUpdateService.start(context, url)
}
}
.setNegativeButton(R.string.update_check_ignore, null)
.setNeutralButton(R.string.update_check_open) { _, _ ->
openInBrowser(args.getString(RELEASE_URL_KEY)!!)
}
.create()
}
override fun onAttach(view: View) {
super.onAttach(view)
// Make links in Markdown text clickable
(dialog?.findViewById(android.R.id.message) as? TextView)?.movementMethod =
LinkMovementMethod.getInstance()
}
}
private const val BODY_KEY = "NewUpdateDialogController.body"
private const val URL_KEY = "NewUpdateDialogController.key"
private const val RELEASE_URL_KEY = "NewUpdateDialogController.release_url"
private const val DOWNLOAD_URL_KEY = "NewUpdateDialogController.download_url"

View File

@ -226,6 +226,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
super.onSaveInstanceState(outState)
}
override fun onPause() {
presenter.saveProgress()
super.onPause()
}
/**
* Set menu visibility again on activity resume to apply immersive mode again if needed.
* Helps with rotations.

View File

@ -4,7 +4,6 @@ import android.app.Application
import android.net.Uri
import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.History
@ -22,6 +21,7 @@ import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader
import eu.kanade.tachiyomi.ui.reader.loader.HttpPageLoader
import eu.kanade.tachiyomi.ui.reader.model.InsertPage
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
@ -345,6 +345,14 @@ class ReaderPresenter(
* that the user doesn't have to wait too long to continue reading.
*/
private fun preload(chapter: ReaderChapter) {
if (chapter.pageLoader is HttpPageLoader) {
val manga = manga ?: return
val isDownloaded = downloadManager.isChapterDownloaded(chapter.chapter, manga)
if (isDownloaded) {
chapter.state = ReaderChapter.State.Wait
}
}
if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) {
return
}
@ -456,6 +464,10 @@ class ReaderPresenter(
}
}
fun saveProgress() {
getCurrentChapter()?.let { onChapterChanged(it) }
}
/**
* Called from the activity to preload the given [chapter].
*/
@ -662,20 +674,22 @@ class ReaderPresenter(
Observable
.fromCallable {
if (manga.isLocal()) {
val context = Injekt.get<Application>()
LocalSource.updateCover(context, manga, stream())
manga.updateCoverLastModified(db)
R.string.cover_updated
SetAsCoverResult.Success
} else {
if (manga.favorite) {
coverCache.setCustomCoverToCache(manga, stream())
stream().use {
if (manga.isLocal()) {
val context = Injekt.get<Application>()
LocalSource.updateCover(context, manga, it)
manga.updateCoverLastModified(db)
coverCache.clearMemoryCache()
SetAsCoverResult.Success
} else {
SetAsCoverResult.AddToLibraryFirst
if (manga.favorite) {
coverCache.setCustomCoverToCache(manga, it)
manga.updateCoverLastModified(db)
coverCache.clearMemoryCache()
SetAsCoverResult.Success
} else {
SetAsCoverResult.AddToLibraryFirst
}
}
}
}

View File

@ -26,7 +26,6 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
is ChapterTransition.Prev -> bindPrevChapterTransition(transition)
is ChapterTransition.Next -> bindNextChapterTransition(transition)
}
missingChapterWarning(transition)
}

View File

@ -66,9 +66,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
set(value) {
field = value
if (value) {
awaitingIdleViewerChapters?.let {
setChaptersInternal(it)
awaitingIdleViewerChapters?.let { viewerChapters ->
setChaptersInternal(viewerChapters)
awaitingIdleViewerChapters = null
if (viewerChapters.currChapter.pages?.size == 1) {
adapter.nextTransition?.to?.let {
activity.requestPreloadChapter(it)
}
}
}
}
}

View File

@ -46,7 +46,7 @@ class WebtoonTransitionHolder(
layout.orientation = LinearLayout.VERTICAL
layout.gravity = Gravity.CENTER
val paddingVertical = 48.dpToPx
val paddingVertical = 128.dpToPx
val paddingHorizontal = 32.dpToPx
layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)

View File

@ -103,6 +103,12 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
activity.requestPreloadChapter(firstItem.to)
}
}
val lastIndex = layoutManager.findLastEndVisibleItemPosition()
val lastItem = adapter.items.getOrNull(lastIndex)
if (lastItem is ChapterTransition.Next && lastItem.to == null) {
activity.showMenu()
}
}
},
)
@ -216,9 +222,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
if (toChapter != null) {
logcat { "Request preload destination chapter because we're on the transition" }
activity.requestPreloadChapter(toChapter)
} else if (transition is ChapterTransition.Next) {
// No more chapters, show menu because the user is probably going to close the reader
activity.showMenu()
}
}
@ -245,7 +248,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
logcat { "moveToPage" }
val position = adapter.items.indexOf(page)
if (position != -1) {
recycler.scrollToPosition(position)
layoutManager.scrollToPositionWithOffset(position, 0)
if (layoutManager.findLastEndVisibleItemPosition() == -1) {
onScrolled(pos = position)
}

View File

@ -4,10 +4,11 @@ import android.annotation.SuppressLint
import android.content.ActivityNotFoundException
import android.content.Intent
import android.provider.Settings
import android.webkit.WebStorage
import android.webkit.WebView
import androidx.core.net.toUri
import androidx.preference.PreferenceScreen
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -15,9 +16,13 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.PREF_DOH_360
import eu.kanade.tachiyomi.network.PREF_DOH_ADGUARD
import eu.kanade.tachiyomi.network.PREF_DOH_ALIDNS
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.network.PREF_DOH_DNSPOD
import eu.kanade.tachiyomi.network.PREF_DOH_GOOGLE
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
@ -38,11 +43,16 @@ import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.util.system.isPackageInstalled
import eu.kanade.tachiyomi.util.system.logcat
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast
import logcat.LogPriority
import rikka.sui.Sui
import uy.kohesive.injekt.injectLazy
import java.io.File
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsAdvancedController : SettingsController() {
@ -55,7 +65,7 @@ class SettingsAdvancedController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = screen.apply {
titleRes = R.string.pref_category_advanced
if (BuildConfig.FLAVOR != "dev") {
if (isDevFlavor.not()) {
switchPreference {
key = "acra.enable"
titleRes = R.string.pref_enable_acra
@ -78,7 +88,7 @@ class SettingsAdvancedController : SettingsController() {
key = Keys.verboseLogging
titleRes = R.string.pref_verbose_logging
summaryRes = R.string.pref_verbose_logging_summary
defaultValue = false
defaultValue = isDevFlavor
onChange {
activity?.toast(R.string.requires_app_restart)
@ -161,6 +171,12 @@ class SettingsAdvancedController : SettingsController() {
activity?.toast(R.string.cookies_cleared)
}
}
preference {
key = "pref_clear_webview_data"
titleRes = R.string.pref_clear_webview_data
onClick { clearWebViewData() }
}
intListPreference {
key = Keys.dohProvider
titleRes = R.string.pref_dns_over_https
@ -170,6 +186,10 @@ class SettingsAdvancedController : SettingsController() {
"Google",
"AdGuard",
"Quad9",
"AliDNS",
"DNSPod",
"360",
"Quad 101",
)
entryValues = arrayOf(
"-1",
@ -177,6 +197,10 @@ class SettingsAdvancedController : SettingsController() {
PREF_DOH_GOOGLE.toString(),
PREF_DOH_ADGUARD.toString(),
PREF_DOH_QUAD9.toString(),
PREF_DOH_ALIDNS.toString(),
PREF_DOH_DNSPOD.toString(),
PREF_DOH_360.toString(),
PREF_DOH_QUAD101.toString(),
)
defaultValue = "-1"
summary = "%s"
@ -274,10 +298,29 @@ class SettingsAdvancedController : SettingsController() {
resources?.getString(R.string.used_cache, chapterCache.readableSize)
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
withUIContext { activity?.toast(R.string.cache_delete_error) }
}
}
}
private fun clearWebViewData() {
if (activity == null) return
try {
val webview = WebView(activity!!)
webview.setDefaultSettings()
webview.clearCache(true)
webview.clearFormData()
webview.clearHistory()
webview.clearSslPreferences()
WebStorage.getInstance().deleteAllData()
activity?.applicationInfo?.dataDir?.let { File("$it/app_webview/").deleteRecursively() }
activity?.toast(R.string.webview_data_deleted)
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
activity?.toast(R.string.cache_delete_error)
}
}
}
private const val CLEAR_CACHE_KEY = "pref_clear_cache_key"

View File

@ -125,20 +125,20 @@ class SettingsDownloadController : SettingsController() {
titleRes = R.string.pref_category_auto_download
switchPreference {
bindTo(preferences.downloadNew())
bindTo(preferences.downloadNewChapter())
titleRes = R.string.pref_download_new
}
preference {
bindTo(preferences.downloadNewCategories())
bindTo(preferences.downloadNewChapterCategories())
titleRes = R.string.categories
onClick {
DownloadCategoriesDialog().showDialog(router)
}
visibleIf(preferences.downloadNew()) { it }
visibleIf(preferences.downloadNewChapter()) { it }
fun updateSummary() {
val selectedCategories = preferences.downloadNewCategories().get()
val selectedCategories = preferences.downloadNewChapterCategories().get()
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
.sortedBy { it.order }
val includedItemsText = if (selectedCategories.isEmpty()) {
@ -147,7 +147,7 @@ class SettingsDownloadController : SettingsController() {
selectedCategories.joinToString { it.name }
}
val excludedCategories = preferences.downloadNewCategoriesExclude().get()
val excludedCategories = preferences.downloadNewChapterCategoriesExclude().get()
.mapNotNull { id -> categories.find { it.id == id.toInt() } }
.sortedBy { it.order }
val excludedItemsText = if (excludedCategories.isEmpty()) {
@ -163,10 +163,10 @@ class SettingsDownloadController : SettingsController() {
}
}
preferences.downloadNewCategories().asFlow()
preferences.downloadNewChapterCategories().asFlow()
.onEach { updateSummary() }
.launchIn(viewScope)
preferences.downloadNewCategoriesExclude().asFlow()
preferences.downloadNewChapterCategoriesExclude().asFlow()
.onEach { updateSummary() }
.launchIn(viewScope)
}
@ -254,8 +254,8 @@ class SettingsDownloadController : SettingsController() {
var selected = categories
.map {
when (it.id.toString()) {
in preferences.downloadNewCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
in preferences.downloadNewCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
in preferences.downloadNewChapterCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
in preferences.downloadNewChapterCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
else -> QuadStateTextView.State.UNCHECKED.ordinal
}
}
@ -282,8 +282,8 @@ class SettingsDownloadController : SettingsController() {
.map { categories[it].id.toString() }
.toSet()
preferences.downloadNewCategories().set(included)
preferences.downloadNewCategoriesExclude().set(excluded)
preferences.downloadNewChapterCategories().set(included)
preferences.downloadNewChapterCategoriesExclude().set(excluded)
}
.setNegativeButton(android.R.string.cancel, null)
.create()

View File

@ -11,12 +11,7 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
import eu.kanade.tachiyomi.data.preference.MANGA_HAS_UNREAD
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
import eu.kanade.tachiyomi.data.preference.MANGA_NON_READ
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.*
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.databinding.PrefLibraryColumnsBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController
@ -159,8 +154,8 @@ class SettingsLibraryController : SettingsController() {
multiSelectListPreference {
bindTo(preferences.libraryUpdateDeviceRestriction())
titleRes = R.string.pref_library_update_restriction
entriesRes = arrayOf(R.string.connected_to_wifi, R.string.charging)
entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_CHARGING)
entriesRes = arrayOf(R.string.connected_to_wifi, R.string.network_not_metered, R.string.charging, R.string.battery_not_low)
entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_NETWORK_NOT_METERED, DEVICE_CHARGING, DEVICE_BATTERY_NOT_LOW)
visibleIf(preferences.libraryUpdateInterval()) { it > 0 }
@ -176,7 +171,9 @@ class SettingsLibraryController : SettingsController() {
.map {
when (it) {
DEVICE_ONLY_ON_WIFI -> context.getString(R.string.connected_to_wifi)
DEVICE_NETWORK_NOT_METERED -> context.getString(R.string.network_not_metered)
DEVICE_CHARGING -> context.getString(R.string.charging)
DEVICE_BATTERY_NOT_LOW -> context.getString(R.string.battery_not_low)
else -> it
}
}

View File

@ -66,7 +66,7 @@ class ClearDatabaseController :
adapter = FlexibleAdapter<ClearDatabaseSourceItem>(null, this, true)
binding.recycler.adapter = adapter
binding.recycler.layoutManager = LinearLayoutManager(activity)
binding.recycler.layoutManager = LinearLayoutManager(activity!!)
binding.recycler.setHasFixedSize(true)
adapter?.fastScroller = binding.fastScroller
recycler = binding.recycler

View File

@ -56,14 +56,14 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
if (!favorite) return false
// Boolean to determine if user wants to automatically download new chapters.
val downloadNew = prefs.downloadNew().get()
if (!downloadNew) return false
val downloadNewChapter = prefs.downloadNewChapter().get()
if (!downloadNewChapter) return false
val categoriesToDownload = prefs.downloadNewCategories().get().map(String::toInt)
val categoriesToExclude = prefs.downloadNewCategoriesExclude().get().map(String::toInt)
val includedCategories = prefs.downloadNewChapterCategories().get().map { it.toInt() }
val excludedCategories = prefs.downloadNewChapterCategoriesExclude().get().map { it.toInt() }
// Default: download from all categories
if (categoriesToDownload.isEmpty() && categoriesToExclude.isEmpty()) return true
// Default: Download from all categories
if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true
// Get all categories, else default category (0)
val categoriesForManga =
@ -72,8 +72,11 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
.takeUnless { it.isEmpty() } ?: listOf(0)
// In excluded category
if (categoriesForManga.intersect(categoriesToExclude).isNotEmpty()) return false
if (categoriesForManga.any { it in excludedCategories }) return false
// Included category not selected
if (includedCategories.isEmpty()) return true
// In included category
return categoriesForManga.intersect(categoriesToDownload).isNotEmpty()
return categoriesForManga.any { it in includedCategories }
}

View File

@ -11,6 +11,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.Date
import java.util.TreeSet
import kotlin.math.max
/**
* Helper method for syncing the list of chapters from the source with the ones from the database.
@ -59,6 +60,9 @@ fun syncChaptersWithSource(
}
}
var maxTimestamp = 0L // in previous chapters to add
val rightNow = Date().time
for (sourceChapter in sourceChapters) {
// This forces metadata update for the main viewable things in the chapter list.
if (source is HttpSource) {
@ -72,7 +76,9 @@ fun syncChaptersWithSource(
// Add the chapter if not in db already, or update if the metadata changed.
if (dbChapter == null) {
if (sourceChapter.date_upload == 0L) {
sourceChapter.date_upload = Date().time
sourceChapter.date_upload = if (maxTimestamp == 0L) rightNow else maxTimestamp
} else {
maxTimestamp = max(maxTimestamp, sourceChapter.date_upload)
}
toAdd.add(sourceChapter)
} else {
@ -97,6 +103,7 @@ fun syncChaptersWithSource(
return Pair(emptyList(), emptyList())
}
// Keep it a List instead of a Set. See #6372.
val readded = mutableListOf<Chapter>()
db.inTransaction {
@ -154,6 +161,7 @@ fun syncChaptersWithSource(
db.updateLastUpdated(manga).executeAsBlocking()
}
@Suppress("ConvertArgumentToSet")
return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList())
}

View File

@ -0,0 +1,6 @@
package eu.kanade.tachiyomi.util.system
import eu.kanade.tachiyomi.BuildConfig
val isDevFlavor: Boolean
get() = BuildConfig.FLAVOR == "dev"

View File

@ -87,7 +87,11 @@ fun Context.copyToClipboard(label: String, content: String) {
val clipboard = getSystemService<ClipboardManager>()!!
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50)))
// Android 13 and higher shows a visual confirmation of copied contents
// https://developer.android.com/about/versions/13/features/copy-paste
if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.S_V2) {
toast(getString(R.string.copied_to_clipboard, content.truncateCenter(50)))
}
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
toast(R.string.clipboard_copy_error)

View File

@ -10,6 +10,7 @@ import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable
import android.os.Build
import android.webkit.MimeTypeMap
import androidx.core.graphics.alpha
import androidx.core.graphics.applyCanvas
import androidx.core.graphics.blue
@ -56,6 +57,12 @@ object ImageUtil {
return null
}
fun getExtensionFromMimeType(mime: String?): String {
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime)
?: SUPPLEMENTARY_MIMETYPE_MAPPING[mime]
?: "jpg"
}
fun isAnimatedAndSupported(stream: InputStream): Boolean {
try {
val type = getImageType(stream) ?: return false
@ -392,4 +399,10 @@ object ImageUtil {
private fun Int.isWhite(): Boolean =
red + blue + green > 740
// Android doesn't include some mappings
private val SUPPLEMENTARY_MIMETYPE_MAPPING = mapOf(
// https://issuetracker.google.com/issues/182703810
"image/jxl" to "jxl",
)
}

View File

@ -73,7 +73,7 @@ open class ExtendedNavigationView @JvmOverloads constructor(
* @param context any context.
* @param resId the vector resource to load and tint
*/
fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorAccent): Drawable {
fun tintVector(context: Context, resId: Int, @AttrRes colorAttrRes: Int = R.attr.colorPrimary): Drawable {
return AppCompatResources.getDrawable(context, resId)!!.apply {
setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal))
}

View File

@ -29,7 +29,7 @@ class QuadStateTextView @JvmOverloads constructor(context: Context, attrs: Attri
val tint = if (state == State.UNCHECKED) {
context.getThemeColor(R.attr.colorControlNormal)
} else {
context.getThemeColor(R.attr.colorAccent)
context.getThemeColor(R.attr.colorPrimary)
}
if (tint != 0) {
TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(tint))

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="?attr/colorPrimary" android:state_enabled="true"/>
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorOnSurface"/>
</selector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:alpha="0.24" android:color="?attr/colorPrimary" android:state_enabled="true"/>
<item android:alpha="@dimen/material_emphasis_disabled" android:color="?attr/colorOnSurface"/>
</selector>

View File

@ -1,8 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item
android:width="36dp"
android:height="36dp"
android:drawable="@drawable/ic_tachi"
android:gravity="center" />
</layer-list>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108"
android:viewportHeight="108">
<path
android:pathData="M50.61,39.254C50.65,40.029 50.691,41.036 50.719,41.513L50.773,42.371L44.96,42.331C41.772,42.303 39.049,42.235 38.926,42.166C38.695,42.058 38.682,42.248 38.682,44.944L38.682,47.831L38.995,47.75C39.893,47.491 43.53,47.395 53.796,47.395C64.064,47.395 67.698,47.491 68.611,47.75L68.911,47.831L68.911,44.944C68.911,42.248 68.897,42.058 68.678,42.166C68.542,42.235 65.819,42.303 62.634,42.331L56.819,42.371L56.873,41.513C56.9,41.036 56.941,40.029 56.981,39.254L57.037,37.864L50.555,37.864L50.61,39.254ZM44.645,49.329C43.23,49.861 42.086,50.377 42.086,50.459C42.086,50.555 42.276,51.044 42.509,51.563C43.597,53.973 45.312,59.937 45.79,62.945C45.87,63.53 46.008,64.008 46.089,64.008C46.171,64.008 47.505,63.572 49.072,63.041C51.1,62.333 51.889,62.006 51.889,61.856C51.889,61.42 49.467,53.551 48.322,50.282C47.805,48.839 47.586,48.349 47.41,48.362C47.287,48.362 46.049,48.798 44.645,49.329Z"
android:fillColor="#000000"/>
<path
android:pathData="M59.406,49.397C58.957,52.257 57.282,57.785 55.484,62.333L54.545,64.688L37.456,64.688L37.456,70.136L70.544,70.136L70.544,64.688L65.493,64.688C61.503,64.688 60.454,64.648 60.509,64.511C60.55,64.43 60.876,63.708 61.23,62.918C61.571,62.128 62.252,60.426 62.728,59.12C63.641,56.614 65.643,50.364 65.643,49.997C65.643,49.819 65.016,49.614 62.756,49.057C61.163,48.662 59.788,48.349 59.706,48.349C59.624,48.349 59.488,48.825 59.406,49.397Z"
android:fillColor="#000000"/>
</vector>

View File

@ -17,7 +17,7 @@
android:layout_height="wrap_content"
android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@id/side_nav"
app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar
@ -35,7 +35,7 @@
android:background="?attr/colorTertiary"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@id/side_nav"
app:layout_constraintTop_toBottomOf="@+id/appbar"
tools:visibility="visible">
@ -56,7 +56,7 @@
android:background="?attr/colorPrimary"
android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintStart_toEndOf="@id/side_nav"
app:layout_constraintTop_toBottomOf="@+id/downloaded_only"
tools:visibility="visible">
@ -73,11 +73,10 @@
<com.google.android.material.navigationrail.NavigationRailView
android:id="@+id/side_nav"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:elevation="0dp"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_height="match_parent"
android:paddingTop="?attr/actionBarSize"
app:elevation="1dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/incognito_mode"
app:menu="@menu/main_nav" />
<com.google.android.material.tabs.TabLayout

View File

@ -95,9 +95,10 @@
<TextView
android:id="@+id/left_page_text"
android:layout_width="32dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:minWidth="32dp"
android:textColor="?attr/colorOnSurface"
android:textSize="15sp"
tools:text="1" />
@ -116,9 +117,10 @@
<TextView
android:id="@+id/right_page_text"
android:layout_width="32dp"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:gravity="center"
android:minWidth="32dp"
android:textColor="?attr/colorOnSurface"
android:textSize="15sp"
tools:text="15" />

View File

@ -11,7 +11,6 @@
android:id="@+id/upper_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textAppearance="?attr/textAppearanceTitleMedium"
tools:text="Top" />
@ -19,7 +18,7 @@
android:id="@+id/warning"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:layout_marginTop="16dp"
android:gravity="center_vertical">
<ImageView
@ -44,6 +43,7 @@
android:id="@+id/lower_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="?attr/textAppearanceTitleMedium"
tools:text="Bottom" />

View File

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Colors for Lavender theme
~
~ Color scheme by Osyx
~
~ Key colors:
~ Primary #A177FF
~ Secondary #A177FF
~ Tertiary #5E25E1
~ Neutral #111129
-->
<resources>
<color name="lavender_primary">#A177FF</color>
<color name="lavender_onPrimary">#111129</color>
<color name="lavender_primaryContainer">#A177FF</color>
<color name="lavender_onPrimaryContainer">#111129</color>
<color name="lavender_secondary">#A177FF</color>
<color name="lavender_onSecondary">#111129</color>
<color name="lavender_secondaryContainer">#A177FF</color>
<color name="lavender_onSecondaryContainer">#111129</color>
<color name="lavender_tertiary">#5E25E1</color>
<color name="lavender_onTertiary">#E8E8E8</color>
<color name="lavender_tertiaryContainer">#111129</color>
<color name="lavender_onTertiaryContainer">#DEE8FF</color>
<color name="lavender_background">#111129</color>
<color name="lavender_onBackground">#DEE8FF</color>
<color name="lavender_surface">#111129</color>
<color name="lavender_onSurface">#DEE8FF</color>
<color name="lavender_surfaceVariant">#2CB6B6B6</color>
<color name="lavender_onSurfaceVariant">#E8E8E8</color>
<color name="lavender_outline">#A8905FFF</color>
<color name="lavender_inverseOnSurface">#DEE8FF</color>
<color name="lavender_inverseSurface">#221247</color>
<color name="lavender_primaryInverse">#A177FF</color>
<color name="lavender_elevationOverlay">@color/lavender_primary</color>
</resources>

View File

@ -0,0 +1,36 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
~ Colors for Lavender theme
~ Original color scheme by CrepeTF
~
~ Key colors:
~ Primary #7B46AF
~ Secondary #7B46AF
~ Tertiary #EDE2FF
~ Neutral #EDE2FF
-->
<resources>
<color name="lavender_primary">#7B46AF</color>
<color name="lavender_onPrimary">#EDE2FF</color>
<color name="lavender_primaryContainer">#7B46AF</color>
<color name="lavender_onPrimaryContainer">#EDE2FF</color>
<color name="lavender_secondary">#7B46AF</color>
<color name="lavender_onSecondary">#EDE2FF</color>
<color name="lavender_secondaryContainer">#7B46AF</color>
<color name="lavender_onSecondaryContainer">#EDE2FF</color>
<color name="lavender_tertiary">#EDE2FF</color>
<color name="lavender_onTertiary">#7B46AF</color>
<color name="lavender_tertiaryContainer">#EDE2FF</color>
<color name="lavender_onTertiaryContainer">#7B46AF</color>
<color name="lavender_background">#EDE2FF</color>
<color name="lavender_onBackground">#1B1B22</color>
<color name="lavender_surface">#EDE2FF</color>
<color name="lavender_onSurface">#1B1B22</color>
<color name="lavender_surfaceVariant">#B9B0CC</color>
<color name="lavender_onSurfaceVariant">#D849454E</color>
<color name="lavender_outline">#7B46AF</color>
<color name="lavender_inverseOnSurface">#F3EFF4</color>
<color name="lavender_inverseSurface">#313033</color>
<color name="lavender_primaryInverse">#D6BAFF</color>
<color name="lavender_elevationOverlay">@color/lavender_primary</color>
</resources>

View File

@ -157,6 +157,7 @@
<string name="pref_app_theme">App theme</string>
<string name="theme_monet">Dynamic</string>
<string name="theme_greenapple">Green Apple</string>
<string name="theme_lavender">Lavender</string>
<string name="theme_midnightdusk">Midnight Dusk</string>
<string name="theme_strawberrydaiquiri">Strawberry Daiquiri</string>
<string name="theme_tako">Tako</string>
@ -222,7 +223,9 @@
<string name="update_weekly">Weekly</string>
<string name="pref_library_update_restriction">Automatic updates device restrictions</string>
<string name="connected_to_wifi">Only on Wi-Fi</string>
<string name="network_not_metered">Only on unmetered Network</string>
<string name="charging">Charging</string>
<string name="battery_not_low">Battery not low</string>
<string name="restrictions">Restrictions: %s</string>
<string name="pref_library_update_manga_restriction">Skip updating titles</string>
@ -283,8 +286,8 @@
<!-- Reader section -->
<string name="pref_fullscreen">Fullscreen</string>
<string name="pref_show_navigation_mode">Show navigation layout overlay</string>
<string name="pref_show_navigation_mode_summary">Show tap zones when reader is opened</string>
<string name="pref_show_navigation_mode">Show tap zones overlay</string>
<string name="pref_show_navigation_mode_summary">Briefly show when reader is opened</string>
<string name="pref_dual_page_split">Dual page split</string>
<string name="pref_dual_page_invert">Invert dual page split placement</string>
<string name="pref_dual_page_invert_summary">If the placement of the dual page split doesn\'t match reading direction</string>
@ -295,7 +298,7 @@
<string name="pref_show_reading_mode">Show reading mode</string>
<string name="pref_show_reading_mode_summary">Briefly show current mode when reader is opened</string>
<string name="pref_true_color">32-bit color</string>
<string name="pref_true_color_summary">Reduces banding, but impacts performance</string>
<string name="pref_true_color_summary">Reduces banding, but may impact performance</string>
<string name="pref_crop_borders">Crop borders</string>
<string name="on">On</string>
<string name="off">Off</string>
@ -315,7 +318,7 @@
<string name="pref_reader_navigation">Navigation</string>
<string name="pref_read_with_volume_keys">Volume keys</string>
<string name="pref_read_with_volume_keys_inverted">Invert volume keys</string>
<string name="pref_read_with_tapping_inverted">Invert tapping</string>
<string name="pref_read_with_tapping_inverted">Invert tap zones</string>
<string name="tapping_inverted_none">None</string>
<string name="tapping_inverted_horizontal">Horizontal</string>
<string name="tapping_inverted_vertical">Vertical</string>
@ -345,7 +348,7 @@
<string name="webtoon_viewer">Webtoon</string>
<string name="vertical_plus_viewer">Continuous vertical</string>
<string name="pager_viewer">Paged</string>
<string name="pref_viewer_nav">Navigation layout</string>
<string name="pref_viewer_nav">Tap zones</string>
<string name="pref_image_scale_type">Scale type</string>
<string name="scale_type_fit_screen">Fit screen</string>
<string name="scale_type_stretch">Stretch</string>
@ -451,6 +454,7 @@
<string name="backup_choice">What do you want to backup?</string>
<string name="creating_backup">Creating backup</string>
<string name="creating_backup_error">Backup failed</string>
<string name="empty_backup_error">No library entries to back up</string>
<string name="restore_miui_warning">Backup/restore may not function properly if MIUI Optimization is disabled.</string>
<string name="restore_in_progress">Restore is already in progress</string>
<string name="restoring_backup">Restoring backup</string>
@ -468,7 +472,7 @@
<string name="pref_clear_chapter_cache">Clear chapter cache</string>
<string name="used_cache">Used: %1$s</string>
<string name="cache_deleted">Cache cleared. %1$d files have been deleted</string>
<string name="cache_delete_error">An error occurred while clearing cache</string>
<string name="cache_delete_error">Error occurred while clearing</string>
<string name="pref_auto_clear_chapter_cache">Clear chapter cache on app close</string>
<string name="pref_clear_database">Clear database</string>
<string name="pref_clear_database_summary">Delete history for manga that are not saved in your library</string>
@ -476,6 +480,8 @@
<string name="clear_database_confirmation">Are you sure? Read chapters and progress of non-library manga will be lost</string>
<string name="clear_database_completed">Entries deleted</string>
<string name="database_clean">Database clean</string>
<string name="pref_clear_webview_data">Clear WebView data</string>
<string name="webview_data_deleted">WebView data cleared</string>
<string name="pref_refresh_library_covers">Refresh library manga covers</string>
<string name="pref_refresh_library_tracking">Refresh tracking</string>
<string name="pref_refresh_library_tracking_summary">Updates status, score and last chapter read from the tracking services</string>
@ -717,12 +723,12 @@
<!-- Downloads activity and service -->
<string name="download_queue_error">Couldn\'t download chapters. You can try again in the downloads section</string>
<string name="download_insufficient_space">Couldn\'t download chapters due to low storage space</string>
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking Tachiyomi</string>
<string name="download_queue_size_warning">Warning: large bulk downloads may lead to sources becoming slower and/or blocking Tachiyomi. Tap to learn more.</string>
<!-- Library update service notifications -->
<string name="notification_check_updates">Checking for new chapters</string>
<string name="notification_updating">Updating library… (%1$d/%2$d)</string>
<string name="notification_size_warning">Large updates harm sources and may lead to slower updates and also increased battery usage</string>
<string name="notification_size_warning">Large updates harm sources and may lead to slower updates and also increased battery usage. Tap to learn more.</string>
<string name="notification_new_chapters">New chapters found</string>
<plurals name="notification_new_chapters_summary">
<item quantity="one">For %d title</item>
@ -756,7 +762,7 @@
<!--UpdateCheck-->
<string name="update_check_confirm">Download</string>
<string name="update_check_ignore">Ignore</string>
<string name="update_check_open">Open on GitHub</string>
<string name="update_check_eol">This Android version is no longer supported</string>
<string name="update_check_no_new_updates">No new updates available</string>
<string name="update_check_look_for_updates">Searching for updates…</string>

View File

@ -192,6 +192,8 @@
<style name="Widget.Tachiyomi.Slider" parent="Widget.Material3.Slider">
<item name="labelBehavior">gone</item>
<item name="tickVisible">false</item>
<item name="trackColorInactive">@color/slider_inactive_track</item>
<item name="trackColorActive">@color/slider_active_track</item>
</style>

View File

@ -121,6 +121,34 @@
<item name="colorPrimaryInverse">@color/greenapple_primaryInverse</item>
</style>
<!--== Lavender Theme ==-->
<style name="Theme.Tachiyomi.Lavender">
<!-- Theme Colors -->
<item name="colorPrimary">@color/lavender_primary</item>
<item name="colorOnPrimary">@color/lavender_onPrimary</item>
<item name="colorPrimaryContainer">@color/lavender_primaryContainer</item>
<item name="colorOnPrimaryContainer">@color/lavender_onPrimaryContainer</item>
<item name="colorSecondary">@color/lavender_secondary</item>
<item name="colorOnSecondary">@color/lavender_onSecondary</item>
<item name="colorSecondaryContainer">@color/lavender_secondaryContainer</item>
<item name="colorOnSecondaryContainer">@color/lavender_onSecondaryContainer</item>
<item name="colorTertiary">@color/lavender_tertiary</item>
<item name="colorOnTertiary">@color/lavender_onTertiary</item>
<item name="colorTertiaryContainer">@color/lavender_tertiaryContainer</item>
<item name="colorOnTertiaryContainer">@color/lavender_onTertiaryContainer</item>
<item name="android:colorBackground">@color/lavender_background</item>
<item name="colorOnBackground">@color/lavender_onBackground</item>
<item name="colorSurface">@color/lavender_surface</item>
<item name="colorOnSurface">@color/lavender_onSurface</item>
<item name="colorSurfaceVariant">@color/lavender_surfaceVariant</item>
<item name="colorOnSurfaceVariant">@color/lavender_onSurfaceVariant</item>
<item name="colorOutline">@color/lavender_outline</item>
<item name="colorOnSurfaceInverse">@color/lavender_inverseOnSurface</item>
<item name="colorSurfaceInverse">@color/lavender_inverseSurface</item>
<item name="colorPrimaryInverse">@color/lavender_primaryInverse</item>
<item name="elevationOverlayColor">@color/lavender_elevationOverlay</item>
</style>
<!--== Midnight Dusk Theme ==-->
<style name="Theme.Tachiyomi.MidnightDusk">
<!-- Theme Colors -->

View File

@ -1,5 +1,5 @@
object AndroidConfig {
const val compileSdk = 31
const val compileSdk = 32
const val minSdk = 23
const val targetSdk = 29
const val ndk = "22.1.7171670"

View File

@ -1,16 +1,16 @@
[versions]
agp_version = "7.1.3"
lifecycle_version = "2.5.0-alpha06"
lifecycle_version = "2.5.0"
[libraries]
annotation = "androidx.annotation:annotation:1.4.0-alpha02"
appcompat = "androidx.appcompat:appcompat:1.4.1"
annotation = "androidx.annotation:annotation:1.4.0"
appcompat = "androidx.appcompat:appcompat:1.4.2"
biometricktx = "androidx.biometric:biometric-ktx:1.2.0-alpha04"
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.3"
constraintlayout = "androidx.constraintlayout:constraintlayout:2.1.4"
coordinatorlayout = "androidx.coordinatorlayout:coordinatorlayout:1.2.0"
corektx = "androidx.core:core-ktx:1.8.0-alpha07"
corektx = "androidx.core:core-ktx:1.8.0"
splashscreen = "androidx.core:core-splashscreen:1.0.0-alpha02"
recyclerview = "androidx.recyclerview:recyclerview:1.3.0-alpha02"
recyclerview = "androidx.recyclerview:recyclerview:1.3.0-beta01"
swiperefreshlayout = "androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01"
viewpager = "androidx.viewpager:viewpager:1.1.0-alpha01"
@ -26,5 +26,5 @@ lifecycle = ["lifecycle-common", "lifecycle-process", "lifecycle-runtimektx"]
workmanager = ["work-runtime", "guava"]
[plugins]
application = { id = "com.android.application", version.ref="agp_version"}
library = { id = "com.android.library", version.ref="agp_version"}
application = { id = "com.android.application", version.ref = "agp_version" }
library = { id = "com.android.library", version.ref = "agp_version" }

View File

@ -1,9 +1,9 @@
[versions]
aboutlib_version = "8.9.4"
okhttp_version = "4.9.3"
okhttp_version = "4.10.0"
nucleus_version = "3.0.0"
coil_version = "2.0.0-rc03"
conductor_version = "3.1.2"
conductor_version = "3.1.5"
flowbinding_version = "1.2.0"
shizuku_version = "12.1.0"
robolectric_version = "3.1.4"
@ -22,7 +22,7 @@ flowreactivenetwork = "ru.beryukhov:flowreactivenetwork:1.0.4"
okhttp-core = { module = "com.squareup.okhttp3:okhttp", version.ref = "okhttp_version" }
okhttp-logging = { module = "com.squareup.okhttp3:logging-interceptor", version.ref = "okhttp_version" }
okhttp-dnsoverhttps = { module = "com.squareup.okhttp3:okhttp-dnsoverhttps", version.ref = "okhttp_version" }
okio = "com.squareup.okio:okio:3.0.0"
okio = "com.squareup.okio:okio:3.2.0"
conscrypt-android = "org.conscrypt:conscrypt-android:2.5.2"
@ -34,13 +34,13 @@ jsoup = "org.jsoup:jsoup:1.14.3"
disklrucache = "com.jakewharton:disklrucache:2.0.2"
unifile = "com.github.tachiyomiorg:unifile:17bec43"
junrar = "com.github.junrar:junrar:7.4.0"
junrar = "com.github.junrar:junrar:7.5.2"
sqlitektx = "androidx.sqlite:sqlite-ktx:2.3.0-alpha02"
sqlite-android = "com.github.requery:sqlite-android:3.36.0"
preferencektx = "androidx.preference:preference-ktx:1.2.0"
flowpreferences = "com.fredporciuncula:flow-preferences:1.6.0"
flowpreferences = "com.fredporciuncula:flow-preferences:1.7.0"
nucleus-core = { module = "info.android15.nucleus:nucleus", version.ref = "nucleus_version" }
nucleus-supportv7 = { module = "info.android15.nucleus:nucleus-support-v7", version.ref = "nucleus_version" }
@ -55,6 +55,8 @@ image-decoder = "com.github.tachiyomiorg:image-decoder:7481a4a"
natural-comparator = "com.github.gpanther:java-nat-sort:natural-comparator-1.1"
markwon = "io.noties.markwon:core:4.6.2"
material = "com.google.android.material:material:1.7.0-alpha01"
androidprocessbutton = "com.github.dmytrodanylyk.android-process-button:library:1.0.4"
flexible-adapter-core = "com.github.arkon.FlexibleAdapter:flexible-adapter:c8013533"
@ -76,8 +78,8 @@ flowbinding-viewpager = { module = "io.github.reactivecircus.flowbinding:flowbin
logcat = "com.squareup.logcat:logcat:0.1"
acra-http = "ch.acra:acra-http:5.9.1"
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:20.0.2"
acra-http = "ch.acra:acra-http:5.9.5"
firebase-analytics = "com.google.firebase:firebase-analytics-ktx:21.0.0"
aboutlibraries-core = { module = "com.mikepenz:aboutlibraries-core", version.ref = "aboutlib_version" }
aboutlibraries-gradle = { module = "com.mikepenz.aboutlibraries.plugin:aboutlibraries-plugin", version.ref = "aboutlib_version" }