Compare commits

...

44 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
59 changed files with 710 additions and 314 deletions

View File

@ -3,7 +3,7 @@
I acknowledge that:
- I have updated:
- To the latest version of the app (stable is v0.13.4)
- 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.4"
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.4](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.4](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 = 80
versionName = "0.13.4"
versionCode = 81
versionName = "0.13.5"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")

View File

@ -55,7 +55,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
backup = Backup(
backupManga(databaseManga, flags),
backupCategories(),
backupCategories(flags),
emptyList(),
backupExtensionInfo(databaseManga),
)
@ -133,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()
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

@ -204,7 +204,7 @@ 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)
@ -278,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)

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

@ -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,129 +39,103 @@ 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 }))
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 {
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it.name }))
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 (state.ascending) {
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"))
val cover = getCoverFile(mangaDir.name, baseDirsFiles)
if (cover != null && cover.exists()) {
thumbnail_url = cover.absolutePath
break
}
}
}
val sManga = this
val mangaInfo = this.toMangaInfo()
// 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)
if (format is Format.Epub) {
EpubFile(format.file).use { epub ->
epub.fillMangaMetadata(sManga)
epub.fillMangaMetadata(manga)
}
}
// 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)
}
}
// 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,9 +303,11 @@ class LibraryController(
onTabsSettingsChanged(firstLaunch = true)
// Delay the scroll position to allow the view to be properly measured.
view.doOnAttach {
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.
libraryMangaRelay.call(LibraryMangaEvent(mangaMap))

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,15 +674,16 @@ class ReaderPresenter(
Observable
.fromCallable {
stream().use {
if (manga.isLocal()) {
val context = Injekt.get<Application>()
LocalSource.updateCover(context, manga, stream())
LocalSource.updateCover(context, manga, it)
manga.updateCoverLastModified(db)
R.string.cover_updated
coverCache.clearMemoryCache()
SetAsCoverResult.Success
} else {
if (manga.favorite) {
coverCache.setCustomCoverToCache(manga, stream())
coverCache.setCustomCoverToCache(manga, it)
manga.updateCoverLastModified(db)
coverCache.clearMemoryCache()
SetAsCoverResult.Success
@ -679,6 +692,7 @@ class ReaderPresenter(
}
}
}
}
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribeFirst(

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

@ -16,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
@ -48,6 +52,7 @@ 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() {
@ -143,12 +148,6 @@ class SettingsAdvancedController : SettingsController() {
titleRes = R.string.pref_auto_clear_chapter_cache
defaultValue = false
}
preference {
key = "pref_clear_webview_data"
titleRes = R.string.pref_clear_webview_data
onClick { clearWebViewData() }
}
preference {
key = "pref_clear_database"
titleRes = R.string.pref_clear_database
@ -172,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
@ -181,6 +186,10 @@ class SettingsAdvancedController : SettingsController() {
"Google",
"AdGuard",
"Quad9",
"AliDNS",
"DNSPod",
"360",
"Quad 101",
)
entryValues = arrayOf(
"-1",
@ -188,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"
@ -301,6 +314,7 @@ class SettingsAdvancedController : SettingsController() {
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)

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

@ -87,7 +87,11 @@ fun Context.copyToClipboard(label: String, content: String) {
val clipboard = getSystemService<ClipboardManager>()!!
clipboard.setPrimaryClip(ClipData.newPlainText(label, content))
// 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

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

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"

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.5.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" }
@ -78,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.3"
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" }