Compare commits

...

86 Commits

Author SHA1 Message Date
Jobobby04 1d24bae841 Release v1.8.4 2022-07-13 12:02:59 -04:00
Jobobby04 5901509fbf Cherry picking fixes 2022-07-13 11:48:48 -04:00
Jobobby04 a8b07e0e05 Fix trash group by tracking code
(cherry picked from commit 4de4992e4e)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryPresenter.kt
2022-07-13 11:48:26 -04:00
Jobobby04 808efd3968 Simplify autoscroll
(cherry picked from commit 1a17f87945)
2022-07-13 11:41:01 -04:00
Jobobby04 cedbbb05e4 Only enable autoscroll when app is active
(cherry picked from commit ac586560f0)
2022-07-13 11:40:52 -04:00
Jobobby04 84d22c11ee Use image decoder for double pages
(cherry picked from commit 4ddc696fb5)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt
2022-07-13 11:40:44 -04:00
Jobobby04 4cf068283b Add virtual visibility tag for E-Hentai
(cherry picked from commit 27733aba02)
2022-07-13 11:39:42 -04:00
Jobobby04 e5fd460bb0 Minor cleanup of metadata
(cherry picked from commit 5b7539ac3e)
2022-07-13 11:37:35 -04:00
Jobobby04 6d3095b503 Improve migration sheet layout
(cherry picked from commit fef7808bb4)
2022-07-13 11:36:53 -04:00
Saud-97 fcbe9590d3 New: Migrating titles maintains custom covers (#7196)
* New: Migrating titles maintains custom covers #7189

* Added Custom Covers to MigrationFlags.kt, strings.xml

* Reworded covers --> cover

* Updated logic to show/hide Migration flags titles depending on manga.

(cherry picked from commit 5ea03fad87)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/MigrationFlags.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/browse/migration/search/SearchPresenter.kt
(cherry picked from commit ed39b61ee9)
2022-07-13 11:36:43 -04:00
Jobobby04 f7e5df2b6d Fix auto-downloading for merged manga
(cherry picked from commit 8f868c0813)

# Conflicts:
#	app/src/main/sqldelight/data/merged.sq
2022-07-13 11:35:50 -04:00
Jobobby04 c58554ec75 Minor cleanup
(cherry picked from commit b71b9ab551)
2022-07-13 11:34:51 -04:00
Jobobby04 cdf2cf8a2d Fix mangadex blocked uploaders
(cherry picked from commit f0f8a2a0a2)
2022-07-13 11:34:01 -04:00
Jobobby04 0922d3c288 Fix on hiatus status category
(cherry picked from commit 0df61a9f28)
2022-07-13 11:33:52 -04:00
Jobobby04 505a8288be Merged source stop ddos
(cherry picked from commit 0c7ceb059e)
2022-07-13 11:33:42 -04:00
Jobobby04 b3baaa18d2 Use a file suppress deprecation
(cherry picked from commit 1eafc6ebd8)
2022-07-13 11:32:26 -04:00
Jobobby04 62e2b301c5 Cherry picking fix 2022-07-13 11:30:56 -04:00
Jobobby04 8b11357eff Separate EHTags into multiple files
(cherry picked from commit c51e8c7ab4)
2022-07-13 11:30:23 -04:00
Jobobby04 5bf4d5e434 Update tag action, fix preview changelog
(cherry picked from commit f03711e2f7)
2022-07-13 11:29:58 -04:00
arkon 45569947c4 Bump dependencies
(cherry picked from commit 1dc4a52f61)

# Conflicts:
#	gradle/libs.versions.toml
2022-07-13 11:25:27 -04:00
Jobobby04 e9d25e9d32 Fix cherry picking errors 2022-07-13 11:23:48 -04:00
arkon a03ed54c64 Update default user agent string
(cherry picked from commit 7d3fe0ed43)
(cherry picked from commit d71bf4e6bc)
2022-07-13 11:20:39 -04:00
arkon cc499a7c07 Add MIME type mapping for image/jxl (fixes #7117)
(cherry picked from commit 591df8abcc)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt
(cherry picked from commit bbdab4a703)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt
2022-07-13 11:20:31 -04:00
arkon 0ca0a8f74f Increase height of transition view in webtoon viewers (fixes #7242)
(cherry picked from commit 46734c525f)
(cherry picked from commit adae68a294)
2022-07-13 11:19:52 -04:00
stevenyomi 184aa4e211 Extension API: change fallback source and logic (#7400)
* Extension API: change fallback source and logic

* remove ghproxy

(cherry picked from commit 284445c364)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
(cherry picked from commit ef20995e7d)
2022-07-13 11:19:43 -04:00
stevenyomi 8b7b4e05d2 RateLimitInterceptor: ignore canceled calls (#7389)
* RateLimitInterceptor: ignore canceled calls

* SpecificHostRateLimit: ignore canceled calls

(cherry picked from commit 5b8cd68cf3)
(cherry picked from commit af82ef436b)
2022-07-13 11:19:36 -04:00
Osyx 501dedf845 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)
(cherry picked from commit bd6f778de2)
2022-07-13 11:19:26 -04:00
arkon c6896d87d6 Use primary color for excluded tristate filter icon (fixes #7360)
(cherry picked from commit 3ca1ce4636)
(cherry picked from commit d2e40a0749)
2022-07-13 11:19:19 -04:00
jobobby04 9af0d40479 Fix downloader crash related to UnmeteredSource (#7365)
Fix crash when starting a download with chaqpters from a UnmeteredSource

(cherry picked from commit 470a576441)
(cherry picked from commit 1e53ad97db)
2022-07-13 11:19:12 -04:00
arkon 1ed182853a Fix accented UI elements in library sheet being different colors
(cherry picked from commit cd5bcc3673)
(cherry picked from commit eefdeb3c3f)
2022-07-13 11:19:01 -04:00
arkon 1ef9717443 Fix wrapped long page numbers in reader (closes #7300)
(cherry picked from commit 6bc484617e)
(cherry picked from commit 5edb36ea75)
2022-07-13 11:18:52 -04:00
arkon afb80a23fc Don't show clipboard copy confirmation toast on Android 13 or above
(cherry picked from commit 40f5d26945)
(cherry picked from commit adbf52a347)
2022-07-13 11:18:41 -04:00
kasperskier 2bc380a9a3 Add more DoH providers (#7256)
* Add more DoH providers

* Fix IPs

(cherry picked from commit 18ea6c4f65)
(cherry picked from commit d957f2fa8b)
2022-07-13 11:18:33 -04:00
kasperskier acc4d4a320 ChapterSourceSync: set default timestamp to max timestamp (#7197)
(cherry picked from commit dd5da56695)
(cherry picked from commit 1d00dee9b7)
2022-07-13 11:18:27 -04:00
Chris ac8e5cf78c 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)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsLibraryController.kt
(cherry picked from commit dd8dc8fbe9)
2022-07-13 11:18:19 -04:00
FourTOne5 9464ae04aa Local Source - qol, cleanup and cover related fixes (#7166)
* Local Source - qol, cleanup and cover related fixes

* Review Changes

(cherry picked from commit ad17eb1386)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
(cherry picked from commit 6fd79f4838)
2022-07-13 11:18:12 -04:00
CVIUS 1c61d37171 Fix reader menu appearing then disappearing in webtoon viewer when there is no next chapter (#7115)
(cherry picked from commit 6580f5771f)
(cherry picked from commit c0362faaf8)
2022-07-13 11:17:55 -04:00
CVIUS b64a2cf816 Fix webtoon viewer showing transition view when going to next/prev chapter using next/prev button (#7133)
(cherry picked from commit b21bcc2d45)
(cherry picked from commit 31ac3aece2)
2022-07-13 11:17:47 -04:00
kasperskier 9820e1097d Change jsDelivr CDN URL to Fastly (#7156)
(cherry picked from commit 7b242bf118)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
(cherry picked from commit bbdbaa1de6)
2022-07-13 11:17:37 -04:00
arkon 153022df0a Use jsDelivr as fallback when GitHub can't be reached for extensions (closes #5517)
Re-implementation of 24bb2f02dc

(cherry picked from commit d61bfd7caf)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/extension/api/ExtensionGithubApi.kt
(cherry picked from commit 4458f74f6c)

# Conflicts:
#	app/src/main/java/eu/kanade/presentation/extension/ExtensionScreen.kt
2022-07-13 11:17:30 -04:00
CVIUS 9e31806e5c Save reader progress when activity is paused (#7121)
(cherry picked from commit f1ab34e27c)
(cherry picked from commit 9322624886)
2022-07-13 11:13:28 -04:00
CVIUS 3ec11cb81f Fix category tabs incorrect scroll position (#7120)
(cherry picked from commit 6d655ff757)
(cherry picked from commit 58db04d8dd)
2022-07-13 11:13:19 -04:00
nzoba 960d67ec26 Add switch to DownloadPageLoader when chapter is downloaded (#7119)
(cherry picked from commit 63627c81eb)
(cherry picked from commit f7a57d2ddd)
2022-07-13 11:13:08 -04:00
CVIUS 832107b932 Fix "Move to top" showing at the most top item in download queue (#7109)
(cherry picked from commit b26daf8824)
(cherry picked from commit 054e6b839e)
2022-07-13 11:12:59 -04:00
Jobobby04 a575770be0 Update build workflow actions
(cherry picked from commit c1c934011f)
2022-07-13 11:12:50 -04:00
nicki a7979b8323 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)
(cherry picked from commit 81bdc19075)
2022-07-13 11:12:36 -04:00
CVIUS e7cd7c06fa Use theme primary color for slider track (#7102)
(cherry picked from commit bc053580ad)
(cherry picked from commit ea9ea11eaf)
2022-07-13 11:12:25 -04:00
nicki 4cee1b3583 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)
(cherry picked from commit 1269d71d1a)
2022-07-13 11:12:16 -04:00
arkon dfa9b7462f Rename "navigation layout" to "tap zones"
(cherry picked from commit c49d862fc5)
(cherry picked from commit ec9d55e9e8)
2022-07-13 11:12:06 -04:00
FourTOne5 b456e38cc5 Fix removing manga from library reverts during global update (#7063)
* Fix removing manga from library reverts during global update

* Review Changes

* Review changes 2
# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt

(cherry picked from commit f966940d15)
2022-07-13 11:11:58 -04:00
FourTOne5 b8e0b86df8 Add -r flag to ShizukuInstaller createCommand (#7080)
(cherry picked from commit 3865384ccc)
2022-07-13 11:11:48 -04:00
arkon c48f4770ee Fix Android 13 icon sizing
(cherry picked from commit 3a4f107ab7)

# Conflicts:
#	app/build.gradle.kts
2022-07-13 11:10:51 -04:00
arkon 5191d7abb1 Add links to website FAQ for library update and download warning notifications
(cherry picked from commit 70698e6494)
(cherry picked from commit b846bc2044)
2022-07-13 11:09:50 -04:00
FourTOne5 9da8a09cb4 Download new chapters when only excluded categories is selected (#6984)
(cherry picked from commit 06bec0ad54)
(cherry picked from commit 7ed22e5d90)
2022-07-13 11:09:40 -04:00
arkon 98d5173507 Fix skipped library entries and size warning notifications using same ID
(cherry picked from commit 91ed3a4a5f)
(cherry picked from commit da739dfc07)
2022-07-13 11:09:32 -04:00
arkon ff9fbc5265 Fix update warning notifications being cut off (fixes #6983)
(cherry picked from commit 20145f7a12)
(cherry picked from commit 92af7291d5)
2022-07-13 11:09:23 -04:00
arkon c721b90dc3 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)
(cherry picked from commit f22ff7d3f0)
2022-07-13 11:09:14 -04:00
arkon 77ebecd87d Add battery not low restriction for global updates (closes #6980)
(cherry picked from commit 3feea71146)
(cherry picked from commit 4804dcf644)
2022-07-13 11:08:56 -04:00
ItsLogic 518f2c1faa Fix chapter transition setting for one page chapters (#6998)
(cherry picked from commit 5e32b8e49f)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt
(cherry picked from commit 6df5497dc6)
2022-07-13 11:08:37 -04:00
arkon 33f4c0ad08 Delete entire app_webview folder when clearing WebView data
(cherry picked from commit 6e95fde4ec)
(cherry picked from commit 1d0520e716)
2022-07-13 11:08:28 -04:00
arkon 8d0bfcd55e Move clear webview data action to network group
(cherry picked from commit bf0bb5aa88)
(cherry picked from commit 93b7881505)
2022-07-13 11:08:19 -04:00
Jobobby04 263c0fae8c Release v1.8.3 2022-04-22 19:39:42 -04:00
Howard Wu 7756f25312 Add Simplified Chinese translation (#584)
* Add Simplified Chinese translation

Work In Program
Part 1

* Add more translate

* Add more translate

* Add more translate

* Add more translate

* Fix

* Minor changes

* Fix some strings

* Fix some strings
2022-04-22 19:38:51 -04:00
Jobobby04 6a0b523e86 Revert history Compose/SQLDelight changes 2022-04-22 19:27:15 -04:00
arkon 070e2d94c7 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.

(cherry picked from commit c0214103a9)
2022-04-22 19:23:45 -04:00
arkon 743482dfd2 Add advanced setting to clear WebView data
(cherry picked from commit 2b76a97989)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
2022-04-22 19:23:37 -04:00
Andreas f6b7f9e29f Enable verbose logging in dev flavor by default (#6979)
(cherry picked from commit 9d77052d9c)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedController.kt
2022-04-22 19:22:12 -04:00
Andreas 5c9f98bff1 Add indexes to creational tables (#6974)
(cherry picked from commit b4981058a2)
2022-04-22 19:21:09 -04:00
arkon d375d7d8c8 Lift Compose theme to abstract controller
(cherry picked from commit 032aa64195)
2022-04-22 19:21:01 -04:00
arkon a88bcb0fa2 Simplify history item description building
(cherry picked from commit 7c8e8317a8)
2022-04-22 19:20:54 -04:00
arkon 5512c6eb79 Add abstract ComposeController
(cherry picked from commit eb1cfc4cd4)
2022-04-22 19:20:46 -04:00
arkon 97e4b0e248 Add placeholder color for Compose manga covers
(cherry picked from commit f1e5cccee7)
2022-04-22 19:20:39 -04:00
arkon 99a94150ea Default auto backups to 2
(cherry picked from commit bc2ed763bd)
2022-04-22 19:20:32 -04:00
Jobobby04 26b30adf4a Migrate saved search and feed saved search to SQLDelight 2022-04-22 19:19:50 -04:00
Jobobby04 4a115785eb Add SY specific queries to sqldelight files 2022-04-22 19:16:48 -04:00
Andreas a8cb77cc7e 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>
(cherry picked from commit b1f46ed830)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/database/DatabaseHelper.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/DbOpenCallback.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/tables/CategoryTable.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/tables/MangaTable.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/setting/database/ClearDatabasePresenter.kt
#	build.gradle.kts
2022-04-22 10:08:31 -04:00
arkon c44c37383d Make links in new update dialog clickable
Co-authored-by: Jays2Kings <Jays2Kings@users.noreply.github.com>
(cherry picked from commit 6c1565a7d4)
2022-04-21 17:07:12 -04:00
arkon 8e72394910 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.

(cherry picked from commit 2ca6b655ad)
2022-04-21 17:06:57 -04:00
arkon e5349a3d33 Update junrar
(cherry picked from commit a83a481ac8)
2022-04-21 17:06:50 -04:00
arkon e6aa6f02e4 Move chapter name cleaning logic to holder (fixes #6955)
(cherry picked from commit 65a8b63b3b)
2022-04-21 17:06:39 -04:00
Andreas 231c75df65 Fix AppBar not unlifting when scrolling using ComposeView (#6952)
(cherry picked from commit b20ca36db9)
2022-04-21 17:06:31 -04:00
arkon 08c2bfd263 Show better error message when empty backup creation is attempted (closes #6941)
(cherry picked from commit 189f92d7e8)
2022-04-21 17:06:25 -04:00
arkon 33bdf011b4 Increase default OkHttp call timeout to 2 minutes
Which is still stupidly high, but maybe it'll be lenient enough for certain people.

(cherry picked from commit cdd4ec6233)
2022-04-21 17:06:18 -04:00
arkon 26deb46219 Show parsed Markdown for new version info (closes #6940)
(cherry picked from commit ef1bb4e800)
2022-04-21 17:06:11 -04:00
Andreas 45bfd5f72c 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

(cherry picked from commit c475acd1ea)

# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/database/queries/HistoryQueries.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
#	settings.gradle.kts
2022-04-21 17:06:03 -04:00
CrepeTF 32d81eb1fa Add elevation to navigation rails (#6947)
Co-authored-by: CrepeTF <trungnguyen02@outlookcom>
(cherry picked from commit 7d50d7ff52)
2022-04-21 17:01:34 -04:00
110 changed files with 26495 additions and 25125 deletions
+1 -1
View File
@@ -3,7 +3,7 @@
I acknowledge that: I acknowledge that:
- I have updated: - I have updated:
- To the latest version of the app (stable is v1.8.2) - To the latest version of the app (stable is v1.8.4)
- All extensions - All extensions
- I have tried the troubleshooting guide: https://tachiyomi.org/help/guides/troubleshooting-problems/ - 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 - If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
+2 -2
View File
@@ -53,7 +53,7 @@ body:
label: Tachiyomi version label: Tachiyomi version
description: You can find your Tachiyomi version in **More → About**. description: You can find your Tachiyomi version in **More → About**.
placeholder: | placeholder: |
Example: "1.8.2" Example: "1.8.4"
validations: validations:
required: true required: true
@@ -98,7 +98,7 @@ body:
required: true required: true
- label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/). - label: I have tried the [troubleshooting guide](https://tachiyomi.org/help/guides/troubleshooting/).
required: true required: true
- label: I have updated the app to version **[1.8.2](https://github.com/jobobby04/tachiyomisy/releases/latest)**. - label: I have updated the app to version **[1.8.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
required: true required: true
- label: I have updated all installed extensions. - label: I have updated all installed extensions.
required: true required: true
+1 -1
View File
@@ -33,7 +33,7 @@ body:
required: true 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). - 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 required: true
- label: I have updated the app to version **[1.8.2](https://github.com/jobobby04/tachiyomisy/releases/latest)**. - label: I have updated the app to version **[1.8.4](https://github.com/jobobby04/tachiyomisy/releases/latest)**.
required: true required: true
- label: I will fill out all of the requested information in this form. - label: I will fill out all of the requested information in this form.
required: true required: true
@@ -27,7 +27,7 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: TAG - Bump version and push tag - name: TAG - Bump version and push tag
uses: anothrNick/github-tag-action@1.17.2 uses: anothrNick/github-tag-action@1.39.0
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
WITH_V: true WITH_V: true
@@ -32,9 +32,10 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up JDK 11 - name: Set up JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v3
with: with:
java-version: 11 java-version: 11
distribution: adopt
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: | run: |
+2 -1
View File
@@ -28,9 +28,10 @@ jobs:
uses: actions/checkout@v2 uses: actions/checkout@v2
- name: Set up JDK 11 - name: Set up JDK 11
uses: actions/setup-java@v1 uses: actions/setup-java@v3
with: with:
java-version: 11 java-version: 11
distribution: adopt
- name: Copy CI gradle.properties - name: Copy CI gradle.properties
run: | run: |
+3 -2
View File
@@ -25,8 +25,8 @@ android {
applicationId = "eu.kanade.tachiyomi.sy" applicationId = "eu.kanade.tachiyomi.sy"
minSdk = AndroidConfig.minSdk minSdk = AndroidConfig.minSdk
targetSdk = AndroidConfig.targetSdk targetSdk = AndroidConfig.targetSdk
versionCode = 33 versionCode = 35
versionName = "1.8.2" versionName = "1.8.4"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -202,6 +202,7 @@ dependencies {
exclude(group = "androidx.viewpager", module = "viewpager") exclude(group = "androidx.viewpager", module = "viewpager")
} }
implementation(libs.insetter) implementation(libs.insetter)
implementation(libs.markwon)
// Conductor // Conductor
implementation(libs.bundles.conductor) implementation(libs.bundles.conductor)
@@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.backup.full
import android.content.Context import android.content.Context
import android.net.Uri import android.net.Uri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager 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
import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK import eu.kanade.tachiyomi.data.backup.BackupConst.BACKUP_CATEGORY_MASK
@@ -74,7 +75,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
backup = Backup( backup = Backup(
backupManga(databaseManga, flags), backupManga(databaseManga, flags),
backupCategories(), backupCategories(flags),
emptyList(), emptyList(),
backupExtensionInfo(databaseManga), backupExtensionInfo(databaseManga),
backupSavedSearches(), backupSavedSearches(),
@@ -111,6 +112,10 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
} }
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!) val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
if (byteArray.isEmpty()) {
throw IllegalStateException(context.getString(R.string.empty_backup_error))
}
file.openOutputStream().also { file.openOutputStream().also {
// Force overwrite old file // Force overwrite old file
(it as? FileOutputStream)?.channel?.truncate(0) (it as? FileOutputStream)?.channel?.truncate(0)
@@ -149,10 +154,15 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
* *
* @return list of [BackupCategory] to be backed up * @return list of [BackupCategory] to be backed up
*/ */
private fun backupCategories(): List<BackupCategory> { private fun backupCategories(options: Int): List<BackupCategory> {
return databaseHelper.getCategories() // Check if user wants category information in backup
.executeAsBlocking() return if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
.map { BackupCategory.copyFrom(it) } databaseHelper.getCategories()
.executeAsBlocking()
.map { BackupCategory.copyFrom(it) }
} else {
emptyList()
}
} }
// SY --> // SY -->
@@ -25,6 +25,19 @@ fun getMergedMangaQuery() =
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID} ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
""" """
/**
* Query to get the manga merged into a merged manga
*/
fun getMergedMangaForDownloadingQuery() =
"""
SELECT ${Manga.TABLE}.*
FROM (
SELECT ${Merged.COL_MANGA_ID} FROM ${Merged.TABLE} WHERE ${Merged.COL_MERGE_ID} = ? AND ${Merged.COL_DOWNLOAD_CHAPTERS} = 1
) AS M
JOIN ${Manga.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = M.${Merged.COL_MANGA_ID}
"""
/** /**
* Query to get all the manga that are merged into other manga * Query to get all the manga that are merged into other manga
*/ */
@@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.app.PendingIntent
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory import android.graphics.BitmapFactory
import androidx.core.app.NotificationCompat 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. * @param timeout duration after which to automatically dismiss the notification.
* Only works on Android 8+. * Only works on Android 8+.
*/ */
fun onWarning(reason: String, timeout: Long? = null) { fun onWarning(reason: String, timeout: Long? = null, contentIntent: PendingIntent? = null) {
with(errorNotificationBuilder) { with(errorNotificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title)) setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(reason) setStyle(NotificationCompat.BigTextStyle().bigText(reason))
setSmallIcon(R.drawable.ic_warning_white_24dp) setSmallIcon(R.drawable.ic_warning_white_24dp)
setAutoCancel(true) setAutoCancel(true)
clearActions() clearActions()
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
timeout?.let { setTimeoutAfter(it) } timeout?.let { setTimeoutAfter(it) }
contentIntent?.let { setContentIntent(it) }
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
} }
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.webkit.MimeTypeMap
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay 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.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue 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.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.UnmeteredSource
@@ -278,7 +279,8 @@ class Downloader(
val maxDownloadsFromSource = queue val maxDownloadsFromSource = queue
.groupBy { it.source } .groupBy { it.source }
.filterKeys { it !is UnmeteredSource } .filterKeys { it !is UnmeteredSource }
.maxOf { it.value.size } .maxOfOrNull { it.value.size }
?: 0
if ( if (
queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD || queuedDownloads > DOWNLOADS_QUEUED_WARNING_THRESHOLD ||
maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD maxDownloadsFromSource > CHAPTERS_PER_SOURCE_QUEUE_WARNING_THRESHOLD
@@ -287,6 +289,7 @@ class Downloader(
notifier.onWarning( notifier.onWarning(
context.getString(R.string.download_queue_size_warning), context.getString(R.string.download_queue_size_warning),
WARNING_NOTIF_TIMEOUT_MS, WARNING_NOTIF_TIMEOUT_MS,
NotificationHandler.openUrl(context, LibraryUpdateNotifier.HELP_WARNING_URL),
) )
} }
} }
@@ -474,7 +477,7 @@ class Downloader(
// Else read magic numbers. // Else read magic numbers.
?: ImageUtil.findImageType { file.openInputStream() }?.mime ?: ImageUtil.findImageType { file.openInputStream() }?.mime
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg" return ImageUtil.getExtensionFromMimeType(mime)
} }
/** /**
@@ -8,9 +8,7 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
import androidx.work.Worker import androidx.work.Worker
import androidx.work.WorkerParameters import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING import eu.kanade.tachiyomi.data.preference.*
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.system.isConnectedToWifi import eu.kanade.tachiyomi.util.system.isConnectedToWifi
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@@ -21,8 +19,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
override fun doWork(): Result { override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
if (requiresWifiConnection(preferences) && !context.isConnectedToWifi()) { val restrictions = preferences.libraryUpdateDeviceRestriction().get()
Result.failure() if ((DEVICE_ONLY_ON_WIFI in restrictions) && !context.isConnectedToWifi()) {
return Result.failure()
} }
return if (LibraryUpdateService.start(context)) { return if (LibraryUpdateService.start(context)) {
@@ -41,8 +40,9 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
if (interval > 0) { if (interval > 0) {
val restrictions = preferences.libraryUpdateDeviceRestriction().get() val restrictions = preferences.libraryUpdateDeviceRestriction().get()
val constraints = Constraints.Builder() 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) .setRequiresCharging(DEVICE_CHARGING in restrictions)
.setRequiresBatteryNotLow(DEVICE_BATTERY_NOT_LOW in restrictions)
.build() .build()
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>( val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
@@ -60,10 +60,5 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
WorkManager.getInstance(context).cancelAllWorkByTag(TAG) WorkManager.getInstance(context).cancelAllWorkByTag(TAG)
} }
} }
fun requiresWifiConnection(preferences: PreferencesHelper): Boolean {
val restrictions = preferences.libraryUpdateDeviceRestriction().get()
return DEVICE_ONLY_ON_WIFI in restrictions
}
} }
} }
@@ -94,9 +94,10 @@ class LibraryUpdateNotifier(private val context: Context) {
fun showQueueSizeWarningNotification() { fun showQueueSizeWarningNotification() {
val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) { val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_LIBRARY_PROGRESS) {
setContentTitle(context.getString(R.string.label_warning)) 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) setSmallIcon(R.drawable.ic_warning_white_24dp)
setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS) setTimeoutAfter(Downloader.WARNING_NOTIF_TIMEOUT_MS)
setContentIntent(NotificationHandler.openUrl(context, HELP_WARNING_URL))
} }
context.notificationManager.notify( context.notificationManager.notify(
@@ -341,6 +342,10 @@ class LibraryUpdateNotifier(private val context: Context) {
} }
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) 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 private const val NOTIF_MAX_CHAPTERS = 5
@@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.EnhancedTrackService import eu.kanade.tachiyomi.data.track.EnhancedTrackService
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.TrackStatus
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.UnmeteredSource import eu.kanade.tachiyomi.source.UnmeteredSource
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
@@ -207,6 +208,8 @@ class LibraryUpdateService(
*/ */
override fun onDestroy() { override fun onDestroy() {
updateJob?.cancel() updateJob?.cancel()
// Despite what Android Studio
// states this can be null
ioScope?.cancel() ioScope?.cancel()
if (wakeLock.isHeld) { if (wakeLock.isHeld) {
wakeLock.release() wakeLock.release()
@@ -272,8 +275,7 @@ class LibraryUpdateService(
/** /**
* Adds list of manga to be updated. * Adds list of manga to be updated.
* *
* @param category the ID of the category to update, or -1 if no category specified. * @param categoryId the ID of the category to update, or -1 if no category specified.
* @param target the target to update.
*/ */
fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?) { fun addMangaToQueue(categoryId: Int, group: Int, groupExtra: String?) {
val libraryManga = db.getLibraryMangas().executeAsBlocking() val libraryManga = db.getLibraryMangas().executeAsBlocking()
@@ -308,17 +310,13 @@ class LibraryUpdateService(
when (group) { when (group) {
LibraryGroup.BY_TRACK_STATUS -> { LibraryGroup.BY_TRACK_STATUS -> {
val trackingExtra = groupExtra?.toIntOrNull() ?: -1 val trackingExtra = groupExtra?.toIntOrNull() ?: -1
val loggedServices = trackManager.services.filter { it.isLogged }
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id } val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
val statuses = loggedServices.associate {
it.id to it.getStatusList().associateWith(it::getStatus)
}
libraryManga.filter { manga -> libraryManga.filter { manga ->
val status = tracks[manga.id]?.firstNotNullOfOrNull { track -> val status = tracks[manga.id]?.firstNotNullOfOrNull { track ->
statuses[track.sync_id]?.get(track.status) TrackStatus.parseTrackerStatus(track.sync_id, track.status)
} ?: "not tracked" } ?: TrackStatus.OTHER
(trackManager.trackMap[status] ?: TrackManager.OTHER) == trackingExtra status.int == trackingExtra
} }
} }
LibraryGroup.BY_SOURCE -> { LibraryGroup.BY_SOURCE -> {
@@ -357,12 +355,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. * to do heavy operations or network calls here.
* For each manga it calls [updateManga] and updates the notification showing the current * For each manga it calls [updateManga] and updates the notification showing the current
* progress. * progress.
* *
* @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update. * @return an observable delivering the progress of each update.
*/ */
suspend fun updateChapterList() { suspend fun updateChapterList() {
@@ -389,35 +386,38 @@ class LibraryUpdateService(
return@async return@async
} }
// Don't continue to update if manga not in library
db.getManga(manga.id!!).executeAsBlocking() ?: return@forEach
withUpdateNotification( withUpdateNotification(
currentlyUpdatingManga, currentlyUpdatingManga,
progressCount, progressCount,
manga, manga,
) { manga -> ) { mangaWithNotif ->
try { try {
when { when {
MANGA_NON_COMPLETED in restrictions && manga.status == SManga.COMPLETED -> { MANGA_NON_COMPLETED in restrictions && mangaWithNotif.status == SManga.COMPLETED ->
skippedUpdates.add(manga to getString(R.string.skipped_reason_completed)) skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_completed))
}
MANGA_HAS_UNREAD in restrictions && manga.unreadCount != 0 -> { MANGA_HAS_UNREAD in restrictions && mangaWithNotif.unreadCount != 0 ->
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_caught_up)) skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_caught_up))
}
MANGA_NON_READ in restrictions && manga.totalChapters > 0 && !manga.hasStarted -> { MANGA_NON_READ in restrictions && mangaWithNotif.totalChapters > 0 && !mangaWithNotif.hasStarted ->
skippedUpdates.add(manga to getString(R.string.skipped_reason_not_started)) skippedUpdates.add(mangaWithNotif to getString(R.string.skipped_reason_not_started))
}
else -> { else -> {
// Convert to the manga that contains new chapters // Convert to the manga that contains new chapters
val (newChapters, _) = updateManga(manga, loggedServices) val (newChapters, _) = updateManga(mangaWithNotif, loggedServices)
if (newChapters.isNotEmpty()) { if (newChapters.isNotEmpty()) {
if (manga.shouldDownloadNewChapters(db, preferences)) { if (mangaWithNotif.shouldDownloadNewChapters(db, preferences)) {
downloadChapters(manga, newChapters) downloadChapters(mangaWithNotif, newChapters)
hasDownloads.set(true) hasDownloads.set(true)
} }
// Convert to the manga that contains new chapters // Convert to the manga that contains new chapters
newUpdates.add( newUpdates.add(
manga to newChapters.sortedByDescending { ch -> ch.source_order } mangaWithNotif to newChapters.sortedByDescending { ch -> ch.source_order }
.toTypedArray(), .toTypedArray(),
) )
} }
@@ -436,11 +436,11 @@ class LibraryUpdateService(
e.message e.message
} }
} }
failedUpdates.add(manga to errorMessage) failedUpdates.add(mangaWithNotif to errorMessage)
} }
if (preferences.autoUpdateTrackers()) { if (preferences.autoUpdateTrackers()) {
updateTrackings(manga, loggedServices) updateTrackings(mangaWithNotif, loggedServices)
} }
} }
} }
@@ -477,13 +477,22 @@ class LibraryUpdateService(
// We don't want to start downloading while the library is updating, because websites // We don't want to start downloading while the library is updating, because websites
// may don't like it and they could ban the user. // may don't like it and they could ban the user.
// SY --> // SY -->
val chapterFilter = if (manga.source == MERGED_SOURCE_ID) { if (manga.source == MERGED_SOURCE_ID) {
db.getMergedMangaReferences(manga.id!!).executeAsBlocking() val downloadingManga = db.getMergedMangasForDownloading(manga.id!!).executeAsBlocking()
.filterNot { it.downloadChapters } .associateBy { it.id!! }
.mapNotNull { it.mangaId } + manga.id!! chapters.groupBy { it.manga_id }
} else emptyList() .forEach {
downloadManager.downloadChapters(
downloadingManga[it.key] ?: return@forEach,
chapters,
false,
)
}
return
}
// SY <-- // SY <--
downloadManager.downloadChapters(manga, /* SY --> */ chapters.filterNot { it.manga_id in chapterFilter } /* SY <-- */, false) downloadManager.downloadChapters(manga, chapters, false)
} }
/** /**
@@ -495,6 +504,7 @@ class LibraryUpdateService(
suspend fun updateManga(manga: Manga, loggedServices: List<TrackService>): Pair<List<Chapter>, List<Chapter>> { suspend fun updateManga(manga: Manga, loggedServices: List<TrackService>): Pair<List<Chapter>, List<Chapter>> {
val source = sourceManager.getOrStub(manga.source).getMainSource() val source = sourceManager.getOrStub(manga.source).getMainSource()
var networkSManga: SManga? = null
// Update manga details metadata // Update manga details metadata
if (preferences.autoUpdateMetadata()) { if (preferences.autoUpdateMetadata()) {
val updatedManga = source.getMangaDetails(manga.toMangaInfo()) val updatedManga = source.getMangaDetails(manga.toMangaInfo())
@@ -506,8 +516,7 @@ class LibraryUpdateService(
sManga.thumbnail_url = manga.thumbnail_url sManga.thumbnail_url = manga.thumbnail_url
} }
manga.copyFrom(sManga) networkSManga = sManga
db.insertManga(manga).executeAsBlocking()
} }
// SY --> // SY -->
@@ -532,7 +541,20 @@ class LibraryUpdateService(
val chapters = source.getChapterList(manga.toMangaInfo()) val chapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() } .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() { private suspend fun updateCovers() {
@@ -555,16 +577,16 @@ class LibraryUpdateService(
currentlyUpdatingManga, currentlyUpdatingManga,
progressCount, progressCount,
manga, manga,
) { manga -> ) { mangaWithNotif ->
sourceManager.get(manga.source)?.let { source -> sourceManager.get(mangaWithNotif.source)?.let { source ->
try { try {
val networkManga = val networkManga =
source.getMangaDetails(manga.toMangaInfo()) source.getMangaDetails(mangaWithNotif.toMangaInfo())
val sManga = networkManga.toSManga() val sManga = networkManga.toSManga()
manga.prepUpdateCover(coverCache, sManga, true) mangaWithNotif.prepUpdateCover(coverCache, sManga, true)
sManga.thumbnail_url?.let { sManga.thumbnail_url?.let {
manga.thumbnail_url = it mangaWithNotif.thumbnail_url = it
db.insertManga(manga).executeAsBlocking() db.insertManga(mangaWithNotif).executeAsBlocking()
} }
} catch (e: Throwable) { } catch (e: Throwable) {
// Ignore errors and continue // Ignore errors and continue
@@ -30,7 +30,7 @@ object Notifications {
const val CHANNEL_LIBRARY_ERROR = "library_errors_channel" const val CHANNEL_LIBRARY_ERROR = "library_errors_channel"
const val ID_LIBRARY_ERROR = -102 const val ID_LIBRARY_ERROR = -102
const val CHANNEL_LIBRARY_SKIPPED = "library_skipped_channel" 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. * Notification channel and ids used by the downloader.
@@ -3,7 +3,9 @@ package eu.kanade.tachiyomi.data.preference
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
const val DEVICE_ONLY_ON_WIFI = "wifi" const val DEVICE_ONLY_ON_WIFI = "wifi"
const val DEVICE_NETWORK_NOT_METERED = "network_not_metered"
const val DEVICE_CHARGING = "ac" const val DEVICE_CHARGING = "ac"
const val DEVICE_BATTERY_NOT_LOW = "battery_not_low"
const val MANGA_NON_COMPLETED = "manga_ongoing" const val MANGA_NON_COMPLETED = "manga_ongoing"
const val MANGA_HAS_UNREAD = "manga_fully_read" const val MANGA_HAS_UNREAD = "manga_fully_read"
@@ -28,13 +30,14 @@ object PreferenceValues {
enum class AppTheme(val titleResId: Int?) { enum class AppTheme(val titleResId: Int?) {
DEFAULT(R.string.label_default), DEFAULT(R.string.label_default),
MONET(R.string.theme_monet), MONET(R.string.theme_monet),
GREEN_APPLE(R.string.theme_greenapple),
LAVENDER(R.string.theme_lavender),
MIDNIGHT_DUSK(R.string.theme_midnightdusk), MIDNIGHT_DUSK(R.string.theme_midnightdusk),
STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri), STRAWBERRY_DAIQUIRI(R.string.theme_strawberrydaiquiri),
YOTSUBA(R.string.theme_yotsuba),
TAKO(R.string.theme_tako), TAKO(R.string.theme_tako),
GREEN_APPLE(R.string.theme_greenapple),
TEALTURQUOISE(R.string.theme_tealturquoise), TEALTURQUOISE(R.string.theme_tealturquoise),
YINYANG(R.string.theme_yinyang), YINYANG(R.string.theme_yinyang),
YOTSUBA(R.string.theme_yotsuba),
// Deprecated // Deprecated
DARK_BLUE(null), DARK_BLUE(null),
@@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.ui.reader.setting.ReaderBottomButton
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerConfig
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isDevFlavor
import eu.kanade.tachiyomi.widget.ExtendedNavigationView import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import java.io.File import java.io.File
import java.text.DateFormat import java.text.DateFormat
@@ -212,11 +213,11 @@ class PreferencesHelper(val context: Context) {
fun downloadOnlyOverWifi() = prefs.getBoolean(Keys.downloadOnlyOverWifi, true) 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 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) fun backupInterval() = flowPrefs.getInt("backup_interval", 0)
@@ -288,10 +289,10 @@ class PreferencesHelper(val context: Context) {
fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet()) 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 downloadNewChapterCategories() = flowPrefs.getStringSet("download_new_categories", emptySet())
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet()) fun downloadNewChapterCategoriesExclude() = flowPrefs.getStringSet("download_new_categories_exclude", emptySet())
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1) fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
@@ -330,7 +331,7 @@ class PreferencesHelper(val context: Context) {
if (DeviceUtil.isMiui) Values.ExtensionInstaller.LEGACY else Values.ExtensionInstaller.PACKAGEINSTALLER, 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) fun autoClearChapterCache() = prefs.getBoolean(Keys.autoClearChapterCache, false)
@@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.data.track package eu.kanade.tachiyomi.data.track
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
@@ -23,16 +22,6 @@ class TrackManager(context: Context) {
// SY --> Mangadex from Neko // SY --> Mangadex from Neko
const val MDLIST = 60 const val MDLIST = 60
// SY <-- // SY <--
// SY -->
const val READING = 1
const val REPEATING = 2
const val PLAN_TO_READ = 3
const val PAUSED = 4
const val COMPLETED = 5
const val DROPPED = 6
const val OTHER = 7
// SY <--
} }
val mdList = MdList(context, MDLIST) val mdList = MdList(context, MDLIST)
@@ -54,17 +43,4 @@ class TrackManager(context: Context) {
fun getService(id: Int) = services.find { it.id == id } fun getService(id: Int) = services.find { it.id == id }
fun hasLoggedServices() = services.any { it.isLogged } fun hasLoggedServices() = services.any { it.isLogged }
// SY -->
val trackMap by lazy {
mapOf(
context.getString(R.string.reading) to READING,
context.getString(R.string.repeating) to REPEATING,
context.getString(R.string.plan_to_read) to PLAN_TO_READ,
context.getString(R.string.paused) to PAUSED,
context.getString(R.string.completed) to COMPLETED,
context.getString(R.string.dropped) to DROPPED,
)
}
// SY <--
} }
@@ -0,0 +1,101 @@
package eu.kanade.tachiyomi.data.track
import androidx.annotation.StringRes
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu
import eu.kanade.tachiyomi.data.track.komga.Komga
import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
import exh.md.utils.FollowStatus
enum class TrackStatus(val int: Int, @StringRes val res: Int) {
READING(1, R.string.reading),
REPEATING(2, R.string.repeating),
PLAN_TO_READ(3, R.string.plan_to_read),
PAUSED(4, R.string.on_hold),
COMPLETED(5, R.string.completed),
DROPPED(6, R.string.dropped),
OTHER(7, R.string.not_tracked);
companion object {
fun parseTrackerStatus(tracker: Int, status: Int): TrackStatus? {
return when (tracker) {
TrackManager.MDLIST -> {
when (FollowStatus.fromInt(status)) {
FollowStatus.UNFOLLOWED -> null
FollowStatus.READING -> READING
FollowStatus.COMPLETED -> COMPLETED
FollowStatus.ON_HOLD -> PAUSED
FollowStatus.PLAN_TO_READ -> PLAN_TO_READ
FollowStatus.DROPPED -> DROPPED
FollowStatus.RE_READING -> REPEATING
}
}
TrackManager.MYANIMELIST -> {
when (status) {
MyAnimeList.READING -> READING
MyAnimeList.COMPLETED -> COMPLETED
MyAnimeList.ON_HOLD -> PAUSED
MyAnimeList.PLAN_TO_READ -> PLAN_TO_READ
MyAnimeList.DROPPED -> DROPPED
MyAnimeList.REREADING -> REPEATING
else -> null
}
}
TrackManager.ANILIST -> {
when (status) {
Anilist.READING -> READING
Anilist.COMPLETED -> COMPLETED
Anilist.ON_HOLD -> PAUSED
Anilist.PLAN_TO_READ -> PLAN_TO_READ
Anilist.DROPPED -> DROPPED
Anilist.REREADING -> REPEATING
else -> null
}
}
TrackManager.KITSU -> {
when (status) {
Kitsu.READING -> READING
Kitsu.COMPLETED -> COMPLETED
Kitsu.ON_HOLD -> PAUSED
Kitsu.PLAN_TO_READ -> PLAN_TO_READ
Kitsu.DROPPED -> DROPPED
else -> null
}
}
TrackManager.SHIKIMORI -> {
when (status) {
Shikimori.READING -> READING
Shikimori.COMPLETED -> COMPLETED
Shikimori.ON_HOLD -> PAUSED
Shikimori.PLAN_TO_READ -> PLAN_TO_READ
Shikimori.DROPPED -> DROPPED
Shikimori.REREADING -> REPEATING
else -> null
}
}
TrackManager.BANGUMI -> {
when (status) {
Bangumi.READING -> READING
Bangumi.COMPLETED -> COMPLETED
Bangumi.ON_HOLD -> PAUSED
Bangumi.PLAN_TO_READ -> PLAN_TO_READ
Bangumi.DROPPED -> DROPPED
else -> null
}
}
TrackManager.KOMGA -> {
when (status) {
Komga.READING -> READING
Komga.COMPLETED -> COMPLETED
Komga.UNREAD -> null
else -> null
}
}
else -> null
}
}
}
}
@@ -55,12 +55,13 @@ class AppUpdateChecker {
} }
// SY --> // SY -->
private fun isNewVersionSY(versionTag: String) = (versionTag != BuildConfig.VERSION_NAME && (syDebugVersion == "0")) || ((syDebugVersion != "0") && versionTag != syDebugVersion) private fun isNewVersionSY(versionTag: String) = (versionTag != BuildConfig.VERSION_NAME && syDebugVersion == "0") || (syDebugVersion != "0" && versionTag != syDebugVersion)
// SY <-- // SY <--
private fun isNewVersion(versionTag: String): Boolean { private fun isNewVersion(versionTag: String): Boolean {
// Removes prefixes like "r" or "v" // Removes prefixes like "r" or "v"
val newVersion = versionTag.replace("[^\\d.]".toRegex(), "") val newVersion = versionTag.replace("[^\\d.]".toRegex(), "")
val oldVersion = BuildConfig.VERSION_NAME.replace("[^\\d.]".toRegex(), "")
return if (BuildConfig.DEBUG) { return if (BuildConfig.DEBUG) {
// Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo // Preview builds: based on releases in "tachiyomiorg/tachiyomi-preview" repo
@@ -69,7 +70,15 @@ class AppUpdateChecker {
} else { } else {
// Release builds: based on releases in "tachiyomiorg/tachiyomi" repo // Release builds: based on releases in "tachiyomiorg/tachiyomi" repo
// tagged as something like "v0.1.2" // 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
} }
} }
} }
@@ -11,8 +11,10 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.await import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.parseAs import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import eu.kanade.tachiyomi.util.system.logcat
import exh.source.BlacklistedSources import exh.source.BlacklistedSources
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import logcat.LogPriority
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.util.Date import java.util.Date
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -22,21 +24,41 @@ internal class ExtensionGithubApi {
private val networkService: NetworkHelper by injectLazy() private val networkService: NetworkHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private var requiresFallbackSource = false
suspend fun findExtensions(): List<Extension.Available> { suspend fun findExtensions(): List<Extension.Available> {
return withIOContext { return withIOContext {
val extensions = networkService.client val githubResponse = if (requiresFallbackSource) null else try {
.newCall(GET("${REPO_URL_PREFIX}index.min.json")) networkService.client
.await() .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>>() .parseAs<List<ExtensionJsonObject>>()
.toExtensions() /* SY --> */ + preferences.extensionRepos() .toExtensions() /* SY --> */ + preferences.extensionRepos()
.get() .get()
.flatMap { repoPath -> .flatMap { repoPath ->
val url = "$BASE_URL$repoPath/repo/" val url = if (requiresFallbackSource) {
"$FALLBACK_BASE_URL$repoPath@repo/"
} else {
"$BASE_URL$repoPath/repo/"
}
networkService.client networkService.client
.newCall(GET("${url}index.min.json")) .newCall(GET("${url}index.min.json"))
.await() .await()
.parseAs<List<ExtensionJsonObject>>() .parseAs<List<ExtensionJsonObject>>()
.toExtensions(url) .toExtensions(url, repoSource = true)
} }
// SY <-- // SY <--
@@ -85,7 +107,12 @@ internal class ExtensionGithubApi {
return extensionsWithUpdate return extensionsWithUpdate
} }
private fun List<ExtensionJsonObject>.toExtensions(/* SY --> */ repoUrl: String = REPO_URL_PREFIX /* SY <-- */): List<Extension.Available> { private fun List<ExtensionJsonObject>.toExtensions(
// SY -->
repoUrl: String = getUrlPrefix(),
repoSource: Boolean = false,
// SY <--
): List<Extension.Available> {
return this return this
.filter { .filter {
val libVersion = it.version.substringBeforeLast('.').toDouble() val libVersion = it.version.substringBeforeLast('.').toDouble()
@@ -106,6 +133,7 @@ internal class ExtensionGithubApi {
iconUrl = "${/* SY --> */ repoUrl /* SY <-- */}icon/${it.apk.replace(".apk", ".png")}", iconUrl = "${/* SY --> */ repoUrl /* SY <-- */}icon/${it.apk.replace(".apk", ".png")}",
// SY --> // SY -->
repoUrl = repoUrl, repoUrl = repoUrl,
isRepoSource = repoSource,
// SY <-- // SY <--
) )
} }
@@ -125,6 +153,14 @@ internal class ExtensionGithubApi {
return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" // SY <-- return /* SY --> */ "${extension.repoUrl}/apk/${extension.apkName}" // SY <--
} }
private fun getUrlPrefix(): String {
return if (requiresFallbackSource) {
FALLBACK_REPO_URL_PREFIX
} else {
REPO_URL_PREFIX
}
}
// SY --> // SY -->
private fun Extension.isBlacklisted( private fun Extension.isBlacklisted(
blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get(), blacklistEnabled: Boolean = preferences.enableSourceBlacklist().get(),
@@ -134,8 +170,10 @@ internal class ExtensionGithubApi {
// SY <-- // SY <--
} }
const val BASE_URL = "https://raw.githubusercontent.com/" private const val BASE_URL = "https://raw.githubusercontent.com/"
const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/" private const val REPO_URL_PREFIX = "${BASE_URL}tachiyomiorg/tachiyomi-extensions/repo/"
private const val FALLBACK_BASE_URL = "https://gcore.jsdelivr.net/gh/"
private const val FALLBACK_REPO_URL_PREFIX = "${FALLBACK_BASE_URL}tachiyomiorg/tachiyomi-extensions@repo/"
@Serializable @Serializable
private data class ExtensionJsonObject( private data class ExtensionJsonObject(
@@ -52,9 +52,9 @@ class ShizukuInstaller(private val service: Service) : Installer(service) {
val size = service.getUriSize(entry.uri) ?: throw IllegalStateException() val size = service.getUriSize(entry.uri) ?: throw IllegalStateException()
service.contentResolver.openInputStream(entry.uri)!!.use { service.contentResolver.openInputStream(entry.uri)!!.use {
val createCommand = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { 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 { } else {
"pm install-create -i ${service.packageName} -S $size" "pm install-create -r -i ${service.packageName} -S $size"
} }
val createResult = exec(createCommand) val createResult = exec(createCommand)
sessionId = SESSION_ID_REGEX.find(createResult.out)?.value sessionId = SESSION_ID_REGEX.find(createResult.out)?.value
@@ -48,6 +48,7 @@ sealed class Extension {
val iconUrl: String, val iconUrl: String,
// SY --> // SY -->
val repoUrl: String, val repoUrl: String,
val isRepoSource: Boolean,
// SY <-- // SY <--
) : Extension() ) : Extension()
@@ -13,6 +13,10 @@ const val PREF_DOH_CLOUDFLARE = 1
const val PREF_DOH_GOOGLE = 2 const val PREF_DOH_GOOGLE = 2
const val PREF_DOH_ADGUARD = 3 const val PREF_DOH_ADGUARD = 3
const val PREF_DOH_QUAD9 = 4 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( fun OkHttpClient.Builder.dohCloudflare() = dns(
DnsOverHttps.Builder().client(build()) DnsOverHttps.Builder().client(build())
@@ -68,3 +72,51 @@ fun OkHttpClient.Builder.dohQuad9() = dns(
) )
.build(), .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(),
)
@@ -30,7 +30,7 @@ open /* SY <-- */ class NetworkHelper(context: Context) {
.cookieJar(cookieManager) .cookieJar(cookieManager)
.connectTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.readTimeout(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 // .fastFallback(true) // TODO: re-enable when OkHttp 5 is stabler
.addInterceptor(UserAgentInterceptor()) .addInterceptor(UserAgentInterceptor())
@@ -46,6 +46,10 @@ open /* SY <-- */ class NetworkHelper(context: Context) {
PREF_DOH_GOOGLE -> builder.dohGoogle() PREF_DOH_GOOGLE -> builder.dohGoogle()
PREF_DOH_ADGUARD -> builder.dohAdGuard() PREF_DOH_ADGUARD -> builder.dohAdGuard()
PREF_DOH_QUAD9 -> builder.dohQuad9() 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 return builder
@@ -4,6 +4,7 @@ import android.os.SystemClock
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
@@ -36,6 +37,11 @@ private class RateLimitInterceptor(
private val rateLimitMillis = unit.toMillis(period) private val rateLimitMillis = unit.toMillis(period)
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
// Ignore canceled calls, otherwise they would jam the queue
if (chain.call().isCanceled()) {
throw IOException()
}
synchronized(requestQueue) { synchronized(requestQueue) {
val now = SystemClock.elapsedRealtime() val now = SystemClock.elapsedRealtime()
val waitTime = if (requestQueue.size < permits) { 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) { if (requestQueue.size == permits) {
requestQueue.removeAt(0) requestQueue.removeAt(0)
} }
@@ -5,6 +5,7 @@ import okhttp3.HttpUrl
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Response import okhttp3.Response
import java.io.IOException
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
/** /**
@@ -41,9 +42,13 @@ class SpecificHostRateLimitInterceptor(
private val host = httpUrl.host private val host = httpUrl.host
override fun intercept(chain: Interceptor.Chain): Response { 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()) return chain.proceed(chain.request())
} }
synchronized(requestQueue) { synchronized(requestQueue) {
val now = SystemClock.elapsedRealtime() val now = SystemClock.elapsedRealtime()
val waitTime = if (requestQueue.size < permits) { val waitTime = if (requestQueue.size < permits) {
@@ -59,6 +64,11 @@ class SpecificHostRateLimitInterceptor(
} }
} }
// Final check
if (chain.call().isCanceled()) {
throw IOException()
}
if (requestQueue.size == permits) { if (requestQueue.size == permits) {
requestQueue.removeAt(0) requestQueue.removeAt(0)
} }
@@ -2,7 +2,9 @@ package eu.kanade.tachiyomi.source
import android.content.Context import android.content.Context
import com.github.junrar.Archive import com.github.junrar.Archive
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@@ -18,16 +20,16 @@ import eu.kanade.tachiyomi.util.lang.compareToCaseInsensitiveNaturalOrder
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.EpubFile import eu.kanade.tachiyomi.util.storage.EpubFile
import eu.kanade.tachiyomi.util.system.ImageUtil import eu.kanade.tachiyomi.util.system.ImageUtil
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import kotlinx.serialization.Serializable import kotlinx.serialization.Serializable
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import kotlinx.serialization.json.decodeFromStream import kotlinx.serialization.json.decodeFromStream
import kotlinx.serialization.json.encodeToStream import kotlinx.serialization.json.encodeToStream
import logcat.LogPriority
import rx.Observable import rx.Observable
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
import tachiyomi.source.model.MangaInfo import tachiyomi.source.model.MangaInfo
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
@@ -35,50 +37,10 @@ import java.io.InputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.ZipFile import java.util.zip.ZipFile
class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSource { class LocalSource(
private val context: Context,
companion object { private val coverCache: CoverCache = Injekt.get(),
const val ID = 0L ) : CatalogueSource, UnmeteredSource {
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) }
}
}
private val json: Json by injectLazy() private val json: Json by injectLazy()
@@ -86,86 +48,100 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
// SY <-- // SY <--
override val id = ID override val name: String = context.getString(R.string.local_source)
override val name = context.getString(R.string.local_source)
override val lang = "other" override val id: Long = ID
override val supportsLatest = true
override val lang: String = "other"
override fun toString() = name override fun toString() = name
override val supportsLatest: Boolean = true
// Browse related
override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS) override fun fetchPopularManga(page: Int) = fetchSearchManga(page, "", POPULAR_FILTERS)
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> { override fun fetchSearchManga(page: Int, query: String, filters: FilterList): Observable<MangasPage> {
val baseDirs = getBaseDirectories(context) val baseDirsFiles = getBaseDirectoriesFiles(context)
// SY --> // SY -->
val allowLocalSourceHiddenFolders = preferences.allowLocalSourceHiddenFolders().get() val allowLocalSourceHiddenFolders = preferences.allowLocalSourceHiddenFolders().get()
// SY <-- // SY <--
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L var mangaDirs = baseDirsFiles
var mangaDirs = baseDirs // Filter out files that are hidden and is not a folder
.asSequence() .filter { it.isDirectory && /* SY --> */ (!it.name.startsWith('.') || allowLocalSourceHiddenFolders) /* SY <-- */ }
.mapNotNull { it.listFiles()?.toList() }
.flatten()
.filter { it.isDirectory }
.filterNot { it.name.startsWith('.') /* SY --> */ && !allowLocalSourceHiddenFolders /* SY <-- */ }
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
.distinctBy { it.name } .distinctBy { it.name }
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state val lastModifiedLimit = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
when (state?.index) { // Filter by query or last modified
0 -> { mangaDirs = mangaDirs.filter {
mangaDirs = if (state.ascending) { if (lastModifiedLimit == 0L) {
mangaDirs.sortedWith(compareBy(String.CASE_INSENSITIVE_ORDER, { it.name })) it.name.contains(query, ignoreCase = true)
} else { } else {
mangaDirs.sortedWith(compareByDescending(String.CASE_INSENSITIVE_ORDER, { it.name })) it.lastModified() >= lastModifiedLimit
}
}
1 -> {
mangaDirs = if (state.ascending) {
mangaDirs.sortedBy(File::lastModified)
} else {
mangaDirs.sortedByDescending(File::lastModified)
}
} }
} }
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 -> val mangas = mangaDirs.map { mangaDir ->
SManga.create().apply { SManga.create().apply {
title = mangaDir.name title = mangaDir.name
url = mangaDir.name url = mangaDir.name
// Try to find the cover // Try to find the cover
for (dir in baseDirs) { val cover = getCoverFile(mangaDir.name, baseDirsFiles)
val cover = getCoverFile(File("${dir.absolutePath}/$url")) if (cover != null && cover.exists()) {
if (cover != null && cover.exists()) { thumbnail_url = cover.absolutePath
thumbnail_url = cover.absolutePath
break
}
} }
}
}
val sManga = this // Fetch chapters of all the manga
val mangaInfo = this.toMangaInfo() mangas.forEach { manga ->
runBlocking { val mangaInfo = manga.toMangaInfo()
val chapters = getChapterList(mangaInfo) runBlocking {
if (chapters.isNotEmpty()) { val chapters = getChapterList(mangaInfo)
val chapter = chapters.last().toSChapter() if (chapters.isNotEmpty()) {
val format = getFormat(chapter) val chapter = chapters.last().toSChapter()
if (format is Format.Epub) { val format = getFormat(chapter)
EpubFile(format.file).use { epub ->
epub.fillMangaMetadata(sManga)
}
}
// Copy the cover from the first chapter found. if (format is Format.Epub) {
if (thumbnail_url == null) { EpubFile(format.file).use { epub ->
try { epub.fillMangaMetadata(manga)
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)
}
} }
} }
} }
@@ -200,38 +176,44 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
) )
// SY <-- // SY <--
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS) // Manga details related
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
val localDetails = getBaseDirectories(context) var mangaInfo = manga
.asSequence()
.mapNotNull { File(it, manga.key).listFiles()?.toList() } val baseDirsFile = getBaseDirectoriesFiles(context)
.flatten()
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) } .firstOrNull { it.extension.equals("json", ignoreCase = true) }
return if (localDetails != null) { if (localDetails != null) {
val mangaJson = json.decodeFromStream<MangaJson>(localDetails.inputStream()) val mangaJson = json.decodeFromStream<MangaJson>(localDetails.inputStream())
manga.copy( mangaInfo = mangaInfo.copy(
title = mangaJson.title ?: manga.title, title = mangaJson.title ?: mangaInfo.title,
author = mangaJson.author ?: manga.author, author = mangaJson.author ?: mangaInfo.author,
artist = mangaJson.artist ?: manga.artist, artist = mangaJson.artist ?: mangaInfo.artist,
description = mangaJson.description ?: manga.description, description = mangaJson.description ?: mangaInfo.description,
genres = mangaJson.genre ?: manga.genres, genres = mangaJson.genre ?: mangaInfo.genres,
status = mangaJson.status ?: manga.status, status = mangaJson.status ?: mangaInfo.status,
) )
} else {
manga
} }
return mangaInfo
} }
// Chapters
override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> { override suspend fun getChapterList(manga: MangaInfo): List<ChapterInfo> {
val sManga = manga.toSManga() val sManga = manga.toSManga()
val chapters = getBaseDirectories(context) val baseDirsFile = getBaseDirectoriesFiles(context)
.asSequence() return getMangaDirsFiles(manga.key, baseDirsFile)
.mapNotNull { File(it, manga.key).listFiles()?.toList() } // Only keep supported formats
.flatten()
.filter { it.isDirectory || isSupportedFile(it.extension) } .filter { it.isDirectory || isSupportedFile(it.extension) }
.map { chapterFile -> .map { chapterFile ->
SChapter.create().apply { SChapter.create().apply {
@@ -243,14 +225,14 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
} }
date_upload = chapterFile.lastModified() date_upload = chapterFile.lastModified()
ChapterRecognition.parseChapterNumber(this, sManga)
val format = getFormat(chapterFile) val format = getFormat(chapterFile)
if (format is Format.Epub) { if (format is Format.Epub) {
EpubFile(format.file).use { epub -> EpubFile(format.file).use { epub ->
epub.fillChapterMetadata(this) epub.fillChapterMetadata(this)
} }
} }
ChapterRecognition.parseChapterNumber(this, sManga)
} }
} }
.map { it.toChapterInfo() } .map { it.toChapterInfo() }
@@ -259,12 +241,24 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c if (c == 0) c2.name.compareToCaseInsensitiveNaturalOrder(c1.name) else c
} }
.toList() .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 { private fun isSupportedFile(extension: String): Boolean {
return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES return extension.lowercase() in SUPPORTED_ARCHIVE_TYPES
} }
@@ -328,25 +322,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 { sealed class Format {
data class Directory(val file: File) : Format() data class Directory(val file: File) : Format()
data class Zip(val file: File) : Format() data class Zip(val file: File) : Format()
data class Rar(val file: File) : Format() data class Rar(val file: File) : Format()
data class Epub(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") private val SUPPORTED_ARCHIVE_TYPES = listOf("zip", "cbz", "rar", "cbr", "epub")
@@ -419,6 +419,6 @@ abstract class HttpSource : CatalogueSource {
// EXH <-- // EXH <--
companion object { 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"
} }
} }
@@ -37,6 +37,7 @@ import exh.metadata.metadata.EHentaiSearchMetadata
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_GENRE_NAMESPACE
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_META_NAMESPACE import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_META_NAMESPACE
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_UPLOADER_NAMESPACE import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_UPLOADER_NAMESPACE
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.EH_VISIBILITY_NAMESPACE
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_LIGHT
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_NORMAL
import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK import exh.metadata.metadata.EHentaiSearchMetadata.Companion.TAG_TYPE_WEAK
@@ -688,6 +689,9 @@ class EHentai(
uploader?.let { uploader?.let {
tags += RaisedTag(EH_UPLOADER_NAMESPACE, it, TAG_TYPE_VIRTUAL) tags += RaisedTag(EH_UPLOADER_NAMESPACE, it, TAG_TYPE_VIRTUAL)
} }
visible?.let {
tags += RaisedTag(EH_VISIBILITY_NAMESPACE, it.substringAfter('(').substringBeforeLast(')'), TAG_TYPE_VIRTUAL)
}
} }
} }
} }
@@ -96,7 +96,7 @@ class MangaDex(delegate: HttpSource, val context: Context) :
private fun dataSaver() = sourcePreferences.getBoolean(getDataSaverPreferenceKey(mdLang.lang), false) private fun dataSaver() = sourcePreferences.getBoolean(getDataSaverPreferenceKey(mdLang.lang), false)
private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false) private fun usePort443Only() = sourcePreferences.getBoolean(getStandardHttpsPreferenceKey(mdLang.lang), false)
private fun blockedGroups() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty() private fun blockedGroups() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty()
private fun blockedUploaders() = sourcePreferences.getString(getBlockedGroupsPrefKey(mdLang.lang), "").orEmpty() private fun blockedUploaders() = sourcePreferences.getString(getBlockedUploaderPrefKey(mdLang.lang), "").orEmpty()
private val mangadexService by lazy { private val mangadexService by lazy {
MangaDexService(client) MangaDexService(client)
@@ -21,11 +21,12 @@ import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import exh.log.xLogW import exh.log.xLogW
import exh.merged.sql.models.MergedMangaReference import exh.merged.sql.models.MergedMangaReference
import exh.source.MERGED_SOURCE_ID import exh.source.MERGED_SOURCE_ID
import exh.util.executeOnIO
import kotlinx.coroutines.CancellationException import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.async import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope import kotlinx.coroutines.supervisorScope
import kotlinx.coroutines.sync.Semaphore
import kotlinx.coroutines.sync.withPermit
import okhttp3.Response import okhttp3.Response
import rx.Observable import rx.Observable
import tachiyomi.source.model.ChapterInfo import tachiyomi.source.model.ChapterInfo
@@ -63,18 +64,27 @@ class MergedSource : HttpSource() {
override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo { override suspend fun getMangaDetails(manga: MangaInfo): MangaInfo {
return withIOContext { return withIOContext {
val mergedManga = db.getManga(manga.key, id).executeAsBlocking() ?: throw Exception("merged manga not in db") val mergedManga = db.getManga(manga.key, id).executeAsBlocking()
val mangaReferences = db.getMergedMangaReferences(mergedManga.id ?: throw Exception("merged manga id is null")).executeOnIO() ?: throw Exception("merged manga not in db")
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, info unavailable, merge is likely corrupted") val mangaReferences = db.getMergedMangaReferences(mergedManga.id!!).executeAsBlocking()
if (mangaReferences.size == 1 && .apply {
run { if (isEmpty()) {
val mangaReference = mangaReferences.firstOrNull() throw IllegalArgumentException(
mangaReference == null || mangaReference.mangaSourceId == MERGED_SOURCE_ID "Manga references are empty, info unavailable, merge is likely corrupted",
)
}
if (size == 1 && first().mangaSourceId == MERGED_SOURCE_ID) {
throw IllegalArgumentException(
"Manga references contain only the merged reference, merge is likely corrupted",
)
}
} }
) throw IllegalArgumentException("Manga references contain only the merged reference, merge is likely corrupted")
val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga } ?: mangaReferences.firstOrNull { it.mangaId != it.mergeId } val mangaInfoReference = mangaReferences.firstOrNull { it.isInfoManga }
val dbManga = mangaInfoReference?.let { db.getManga(it.mangaUrl, it.mangaSourceId).executeOnIO()?.toMangaInfo() } ?: mangaReferences.firstOrNull { it.mangaId != it.mergeId }
val dbManga = mangaInfoReference?.run {
db.getManga(mangaUrl, mangaSourceId).executeAsBlocking()?.toMangaInfo()
}
(dbManga ?: mergedManga.toMangaInfo()).copy( (dbManga ?: mergedManga.toMangaInfo()).copy(
key = manga.key, key = manga.key,
) )
@@ -143,41 +153,50 @@ class MergedSource : HttpSource() {
suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Pair<List<Chapter>, List<Chapter>> { suspend fun fetchChaptersAndSync(manga: Manga, downloadChapters: Boolean = true): Pair<List<Chapter>, List<Chapter>> {
val mangaReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking() val mangaReferences = db.getMergedMangaReferences(manga.id!!).executeAsBlocking()
if (mangaReferences.isEmpty()) throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted") if (mangaReferences.isEmpty()) {
throw IllegalArgumentException("Manga references are empty, chapters unavailable, merge is likely corrupted")
}
val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences) val ifDownloadNewChapters = downloadChapters && manga.shouldDownloadNewChapters(db, preferences)
val semaphore = Semaphore(5)
var exception: Exception? = null var exception: Exception? = null
return supervisorScope { return supervisorScope {
mangaReferences mangaReferences
.map { .groupBy(MergedMangaReference::mangaSourceId)
.minus(MERGED_SOURCE_ID)
.map { (_, values) ->
async { async {
try { semaphore.withPermit {
if (it.mangaSourceId == MERGED_SOURCE_ID) return@async null values.map {
val (source, loadedManga, reference) = try {
it.load(db, sourceManager) val (source, loadedManga, reference) =
if (loadedManga != null && reference.getChapterUpdates) { it.load(db, sourceManager)
val chapterList = source.getChapterList(loadedManga.toMangaInfo()) if (loadedManga != null && reference.getChapterUpdates) {
.map { it.toSChapter() } val chapterList = source.getChapterList(loadedManga.toMangaInfo())
val results = .map(ChapterInfo::toSChapter)
syncChaptersWithSource(db, chapterList, loadedManga, source) val results =
if (ifDownloadNewChapters && reference.downloadChapters) { syncChaptersWithSource(db, chapterList, loadedManga, source)
downloadManager.downloadChapters( if (ifDownloadNewChapters && reference.downloadChapters) {
loadedManga, downloadManager.downloadChapters(
results.first, loadedManga,
) results.first,
)
}
results
} else {
null
}
} catch (e: Exception) {
if (e is CancellationException) throw e
exception = e
null
} }
results
} else {
null
} }
} catch (e: Exception) {
if (e is CancellationException) throw e
exception = e
null
} }
} }
} }
.awaitAll() .awaitAll()
.flatten()
.let { pairs -> .let { pairs ->
pairs.flatMap { it?.first.orEmpty() } to pairs.flatMap { it?.second.orEmpty() } pairs.flatMap { it?.first.orEmpty() } to pairs.flatMap { it?.second.orEmpty() }
} }
@@ -187,7 +206,7 @@ class MergedSource : HttpSource() {
} }
suspend fun MergedMangaReference.load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource { suspend fun MergedMangaReference.load(db: DatabaseHelper, sourceManager: SourceManager): LoadedMangaSource {
var manga = db.getManga(mangaUrl, mangaSourceId).executeOnIO() var manga = db.getManga(mangaUrl, mangaSourceId).executeAsBlocking()
val source = sourceManager.getOrStub(manga?.source ?: mangaSourceId) val source = sourceManager.getOrStub(manga?.source ?: mangaSourceId)
if (manga == null) { if (manga == null) {
manga = Manga.create(mangaSourceId).apply { manga = Manga.create(mangaSourceId).apply {
@@ -20,6 +20,9 @@ interface ThemingDelegate {
PreferenceValues.AppTheme.GREEN_APPLE -> { PreferenceValues.AppTheme.GREEN_APPLE -> {
resIds += R.style.Theme_Tachiyomi_GreenApple resIds += R.style.Theme_Tachiyomi_GreenApple
} }
PreferenceValues.AppTheme.LAVENDER -> {
resIds += R.style.Theme_Tachiyomi_Lavender
}
PreferenceValues.AppTheme.MIDNIGHT_DUSK -> { PreferenceValues.AppTheme.MIDNIGHT_DUSK -> {
resIds += R.style.Theme_Tachiyomi_MidnightDusk resIds += R.style.Theme_Tachiyomi_MidnightDusk
} }
@@ -8,7 +8,6 @@ import coil.load
import eu.davidea.viewholders.FlexibleViewHolder import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ExtensionItemBinding import eu.kanade.tachiyomi.databinding.ExtensionItemBinding
import eu.kanade.tachiyomi.extension.api.REPO_URL_PREFIX
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
@@ -57,15 +56,14 @@ class ExtensionHolder(view: View, val adapter: ExtensionAdapter) :
// SY --> // SY -->
private fun String.plusRepo(extension: Extension): String { private fun String.plusRepo(extension: Extension): String {
return if (extension is Extension.Available) { return if (extension is Extension.Available) {
when (extension.repoUrl) { if (!extension.isRepoSource) {
REPO_URL_PREFIX -> this this
else -> { } else {
if (isEmpty()) { if (isEmpty()) {
this this
} else { } else {
this + "" "$this"
} + itemView.context.getString(R.string.repo_source) } + itemView.context.getString(R.string.repo_source)
}
} }
} else this } else this
} }
@@ -1,21 +1,12 @@
package eu.kanade.tachiyomi.ui.browse.migration package eu.kanade.tachiyomi.ui.browse.migration
import eu.kanade.tachiyomi.R
object MigrationFlags { object MigrationFlags {
const val CHAPTERS = 0b0001 const val CHAPTERS = 0b00001
const val CATEGORIES = 0b0010 const val CATEGORIES = 0b00010
const val TRACK = 0b0100 const val TRACK = 0b00100
const val EXTRA = 0b1000 const val CUSTOM_COVER = 0b01000
const val EXTRA = 0b10000
private const val CHAPTERS2 = 0x1
private const val CATEGORIES2 = 0x2
private const val TRACK2 = 0x4
val titles get() = arrayOf(R.string.chapters, R.string.categories, R.string.track, R.string.log_extra)
val flags get() = arrayOf(CHAPTERS, CATEGORIES, TRACK, EXTRA)
fun hasChapters(value: Int): Boolean { fun hasChapters(value: Int): Boolean {
return value and CHAPTERS != 0 return value and CHAPTERS != 0
@@ -29,15 +20,11 @@ object MigrationFlags {
return value and TRACK != 0 return value and TRACK != 0
} }
fun hasCustomCover(value: Int): Boolean {
return value and CUSTOM_COVER != 0
}
fun hasExtra(value: Int): Boolean { fun hasExtra(value: Int): Boolean {
return value and EXTRA != 0 return value and EXTRA != 0
} }
fun getEnabledFlagsPositions(value: Int): List<Int> {
return flags.mapIndexedNotNull { index, flag -> if (value and flag != 0) index else null }
}
fun getFlagsFromPositions(positions: Array<Int>): Int {
return positions.fold(0, { accumulated, position -> accumulated or (1 shl position) })
}
} }
@@ -62,11 +62,13 @@ class MigrationBottomSheetDialog(private val activity: Activity, private val lis
binding.migChapters.isChecked = MigrationFlags.hasChapters(flags) binding.migChapters.isChecked = MigrationFlags.hasChapters(flags)
binding.migCategories.isChecked = MigrationFlags.hasCategories(flags) binding.migCategories.isChecked = MigrationFlags.hasCategories(flags)
binding.migTracking.isChecked = MigrationFlags.hasTracks(flags) binding.migTracking.isChecked = MigrationFlags.hasTracks(flags)
binding.migCustomCover.isChecked = MigrationFlags.hasCustomCover(flags)
binding.migExtra.isChecked = MigrationFlags.hasExtra(flags) binding.migExtra.isChecked = MigrationFlags.hasExtra(flags)
binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags() } binding.migChapters.setOnCheckedChangeListener { _, _ -> setFlags() }
binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags() } binding.migCategories.setOnCheckedChangeListener { _, _ -> setFlags() }
binding.migTracking.setOnCheckedChangeListener { _, _ -> setFlags() } binding.migTracking.setOnCheckedChangeListener { _, _ -> setFlags() }
binding.migCustomCover.setOnCheckedChangeListener { _, _ -> setFlags() }
binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags() } binding.migExtra.setOnCheckedChangeListener { _, _ -> setFlags() }
binding.useSmartSearch.bindToPreference(preferences.smartMigration()) binding.useSmartSearch.bindToPreference(preferences.smartMigration())
@@ -93,6 +95,7 @@ class MigrationBottomSheetDialog(private val activity: Activity, private val lis
if (binding.migChapters.isChecked) flags = flags or MigrationFlags.CHAPTERS if (binding.migChapters.isChecked) flags = flags or MigrationFlags.CHAPTERS
if (binding.migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES if (binding.migCategories.isChecked) flags = flags or MigrationFlags.CATEGORIES
if (binding.migTracking.isChecked) flags = flags or MigrationFlags.TRACK if (binding.migTracking.isChecked) flags = flags or MigrationFlags.TRACK
if (binding.migCustomCover.isChecked) flags = flags or MigrationFlags.CUSTOM_COVER
if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA if (binding.migExtra.isChecked) flags = flags or MigrationFlags.EXTRA
preferences.migrateFlags().set(flags) preferences.migrateFlags().set(flags)
} }
@@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.migration.advanced.process
import android.view.MenuItem import android.view.MenuItem
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.History import eu.kanade.tachiyomi.data.database.models.History
@@ -9,6 +10,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.util.hasCustomCover
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withIOContext import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
@@ -19,6 +21,7 @@ class MigrationProcessAdapter(
) : FlexibleAdapter<MigrationProcessItem>(null, controller, true) { ) : FlexibleAdapter<MigrationProcessItem>(null, controller, true) {
private val db: DatabaseHelper by injectLazy() private val db: DatabaseHelper by injectLazy()
private val preferences: PreferencesHelper by injectLazy() private val preferences: PreferencesHelper by injectLazy()
private val coverCache: CoverCache by injectLazy()
var items: List<MigrationProcessItem> = emptyList() var items: List<MigrationProcessItem> = emptyList()
@@ -148,11 +151,17 @@ class MigrationProcessAdapter(
// Update track // Update track
if (MigrationFlags.hasTracks(flags)) { if (MigrationFlags.hasTracks(flags)) {
val tracks = db.getTracks(prevManga).executeAsBlocking() val tracks = db.getTracks(prevManga).executeAsBlocking()
for (track in tracks) { if (tracks.isNotEmpty()) {
track.id = null tracks.forEach { track ->
track.manga_id = manga.id!! track.id = null
track.manga_id = manga.id!!
}
db.insertTracks(tracks).executeAsBlocking()
} }
db.insertTracks(tracks).executeAsBlocking() }
// Update custom cover
if (MigrationFlags.hasCustomCover(flags) && prevManga.hasCustomCover(coverCache)) {
coverCache.setCustomCoverToCache(manga, coverCache.getCustomCoverFile(prevManga).inputStream())
} }
// Update extras // Update extras
if (MigrationFlags.hasExtra(flags)) { if (MigrationFlags.hasExtra(flags)) {
@@ -42,10 +42,10 @@ open class TriStateItem(val filter: Filter.TriState) : AbstractFlexibleItem<TriS
else -> throw Exception("Unknown state") else -> throw Exception("Unknown state")
}, },
)?.apply { )?.apply {
val color = if (filter.state == Filter.TriState.STATE_INCLUDE) { val color = if (filter.state == Filter.TriState.STATE_IGNORE) {
view.context.getResourceColor(R.attr.colorAccent)
} else {
view.context.getResourceColor(R.attr.colorOnBackground, 0.38f) view.context.getResourceColor(R.attr.colorOnBackground, 0.38f)
} else {
view.context.getResourceColor(R.attr.colorPrimary)
} }
setTint(color) setTint(color)
@@ -89,7 +89,7 @@ class DownloadHolder(private val view: View, val adapter: DownloadAdapter) :
view.popupMenu( view.popupMenu(
menuRes = R.menu.download_single, menuRes = R.menu.download_single,
initMenu = { 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 = findItem(R.id.move_to_bottom).isVisible =
bindingAdapterPosition != adapter.itemCount - 1 bindingAdapterPosition != adapter.itemCount - 1
}, },
@@ -10,7 +10,6 @@ import android.view.View
import android.view.WindowManager import android.view.WindowManager
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.appcompat.view.ActionMode import androidx.appcompat.view.ActionMode
import androidx.core.view.doOnAttach
import androidx.core.view.isVisible import androidx.core.view.isVisible
import com.bluelinelabs.conductor.ControllerChangeHandler import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType import com.bluelinelabs.conductor.ControllerChangeType
@@ -345,8 +344,10 @@ class LibraryController(
onTabsSettingsChanged(firstLaunch = true) onTabsSettingsChanged(firstLaunch = true)
// Delay the scroll position to allow the view to be properly measured. // Delay the scroll position to allow the view to be properly measured.
view.doOnAttach { view.post {
(activity as? MainActivity)?.binding?.tabs?.setScrollPosition(binding.libraryPager.currentItem, 0f, true) 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. // Send the manga map to child fragments after the adapter is updated.
@@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.library.CustomMangaManager import eu.kanade.tachiyomi.data.library.CustomMangaManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackStatus
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
@@ -42,7 +43,6 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.text.Collator import java.text.Collator
import java.util.Collections import java.util.Collections
import java.util.Comparator
import java.util.Locale import java.util.Locale
/** /**
@@ -830,7 +830,7 @@ class LibraryPresenter(
SManga.ONGOING to context.getString(R.string.ongoing), SManga.ONGOING to context.getString(R.string.ongoing),
SManga.LICENSED to context.getString(R.string.licensed), SManga.LICENSED to context.getString(R.string.licensed),
SManga.CANCELLED to context.getString(R.string.cancelled), SManga.CANCELLED to context.getString(R.string.cancelled),
SManga.ON_HIATUS to context.getString(R.string.ongoing), SManga.ON_HIATUS to context.getString(R.string.on_hiatus),
SManga.PUBLISHING_FINISHED to context.getString(R.string.publishing_finished), SManga.PUBLISHING_FINISHED to context.getString(R.string.publishing_finished),
SManga.COMPLETED to context.getString(R.string.completed), SManga.COMPLETED to context.getString(R.string.completed),
SManga.UNKNOWN to context.getString(R.string.unknown), SManga.UNKNOWN to context.getString(R.string.unknown),
@@ -848,15 +848,9 @@ class LibraryPresenter(
.let(grouping::putAll) .let(grouping::putAll)
LibraryGroup.BY_TRACK_STATUS -> { LibraryGroup.BY_TRACK_STATUS -> {
grouping.putAll( grouping.putAll(
listOf( TrackStatus.values()
TrackManager.READING to context.getString(R.string.reading), .map { it.int to context.getString(it.res) }
TrackManager.REPEATING to context.getString(R.string.repeating), .associateBy(Pair<Int, *>::first),
TrackManager.PLAN_TO_READ to context.getString(R.string.plan_to_read),
TrackManager.PAUSED to context.getString(R.string.on_hold),
TrackManager.COMPLETED to context.getString(R.string.completed),
TrackManager.DROPPED to context.getString(R.string.dropped),
TrackManager.OTHER to context.getString(R.string.not_tracked),
).associateBy(Pair<Int, *>::first),
) )
} }
} }
@@ -865,21 +859,12 @@ class LibraryPresenter(
when (groupType) { when (groupType) {
LibraryGroup.BY_TRACK_STATUS -> { LibraryGroup.BY_TRACK_STATUS -> {
val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id } val tracks = db.getTracks().executeAsBlocking().groupBy { it.manga_id }
val statuses = loggedServices.associate {
it.id to it.getStatusList().associateWith(it::getStatus)
}
libraryManga.forEach { libraryItem -> libraryManga.forEach { libraryItem ->
val status = tracks[libraryItem.manga.id]?.firstNotNullOfOrNull { track -> val status = tracks[libraryItem.manga.id]?.firstNotNullOfOrNull { track ->
statuses[track.sync_id]?.get(track.status) TrackStatus.parseTrackerStatus(track.sync_id, track.status)
} ?: "not tracked" } ?: TrackStatus.OTHER
val group = grouping.values.find { (statusInt) ->
statusInt == (trackManager.trackMap[status] ?: TrackManager.OTHER) map.getOrPut(status.int) { mutableListOf() } += libraryItem
}
if (group != null) {
map.getOrPut(group.first) { mutableListOf() } += libraryItem
} else {
map.getOrPut(7) { mutableListOf() } += libraryItem
}
} }
} }
LibraryGroup.BY_SOURCE -> { LibraryGroup.BY_SOURCE -> {
@@ -1118,7 +1118,7 @@ class MangaController :
chaptersHeader.setNumChapters(chapters.size) chaptersHeader.setNumChapters(chapters.size)
val adapter = chaptersAdapter ?: return val adapter = chaptersAdapter ?: return
adapter.updateDataSet(presenter.cleanChapterNames(chapters)) adapter.updateDataSet(chapters)
if (selectedChapters.isNotEmpty()) { if (selectedChapters.isNotEmpty()) {
adapter.clearSelection() // we need to start from a clean state, index may have changed adapter.clearSelection() // we need to start from a clean state, index may have changed
@@ -781,17 +781,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. * Updates the UI after applying the filters.
*/ */
@@ -1281,38 +1270,3 @@ class MangaPresenter(
// Track sheet - end // 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()
@@ -6,6 +6,7 @@ import androidx.core.text.buildSpannedString
import androidx.core.text.color import androidx.core.text.color
import androidx.core.view.isVisible import androidx.core.view.isVisible
import eu.kanade.tachiyomi.R 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.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.ChaptersItemBinding import eu.kanade.tachiyomi.databinding.ChaptersItemBinding
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
@@ -37,6 +38,8 @@ class ChapterHolder(
itemView.context.getString(R.string.display_mode_chapter, number) itemView.context.getString(R.string.display_mode_chapter, number)
} }
else -> chapter.name else -> chapter.name
// TODO: show cleaned name consistently around the app
// else -> cleanChapterName(chapter, manga)
} }
// Set correct text color // Set correct text color
@@ -85,4 +88,47 @@ class ChapterHolder(
binding.download.isVisible = item.manga.source != LocalSource.ID binding.download.isVisible = item.manga.source != LocalSource.ID
binding.download.setState(item.status, item.progress) 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()
@@ -116,6 +116,7 @@ class AboutController : SettingsController(), NoAppBarElevationController {
is AppUpdateResult.NoNewUpdate -> { is AppUpdateResult.NoNewUpdate -> {
activity?.toast(R.string.update_check_no_new_updates) activity?.toast(R.string.update_check_no_new_updates)
} }
else -> {}
} }
} catch (error: Exception) { } catch (error: Exception) {
activity?.toast(error.message) activity?.toast(error.message)
@@ -2,35 +2,58 @@ package eu.kanade.tachiyomi.ui.more
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import android.text.method.LinkMovementMethod
import android.view.View
import android.widget.TextView
import androidx.core.os.bundleOf import androidx.core.os.bundleOf
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.AppUpdateResult import eu.kanade.tachiyomi.data.updater.AppUpdateResult
import eu.kanade.tachiyomi.data.updater.AppUpdateService import eu.kanade.tachiyomi.data.updater.AppUpdateService
import eu.kanade.tachiyomi.ui.base.controller.DialogController 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) { class NewUpdateDialogController(bundle: Bundle? = null) : DialogController(bundle) {
constructor(update: AppUpdateResult.NewUpdate) : this( 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 { 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!!) return MaterialAlertDialogBuilder(activity!!)
.setTitle(R.string.update_check_notification_update_available) .setTitle(R.string.update_check_notification_update_available)
.setMessage(args.getString(BODY_KEY) ?: "") .setMessage(info)
.setPositiveButton(R.string.update_check_confirm) { _, _ -> .setPositiveButton(R.string.update_check_confirm) { _, _ ->
val appContext = applicationContext applicationContext?.let { context ->
if (appContext != null) {
// Start download // Start download
val url = args.getString(URL_KEY) ?: "" val url = args.getString(DOWNLOAD_URL_KEY)!!
AppUpdateService.start(appContext, url) AppUpdateService.start(context, url)
} }
} }
.setNegativeButton(R.string.update_check_ignore, null) .setNeutralButton(R.string.update_check_open) { _, _ ->
openInBrowser(args.getString(RELEASE_URL_KEY)!!)
}
.create() .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 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"
@@ -39,7 +39,9 @@ import androidx.core.view.WindowInsetsControllerCompat
import androidx.core.view.isInvisible import androidx.core.view.isInvisible
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams import androidx.core.view.updateLayoutParams
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.lifecycleScope import androidx.lifecycle.lifecycleScope
import androidx.lifecycle.repeatOnLifecycle
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.dialog.MaterialAlertDialogBuilder import com.google.android.material.dialog.MaterialAlertDialogBuilder
import com.google.android.material.shape.MaterialShapeDrawable import com.google.android.material.shape.MaterialShapeDrawable
@@ -99,16 +101,14 @@ import exh.source.isEhBasedSource
import exh.util.defaultReaderType import exh.util.defaultReaderType
import exh.util.floor import exh.util.floor
import exh.util.mangaType import exh.util.mangaType
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.delay import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.drop import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.flow.merge import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.sample import kotlinx.coroutines.flow.sample
import kotlinx.coroutines.launch
import logcat.LogPriority import logcat.LogPriority
import nucleus.factory.RequiresPresenter import nucleus.factory.RequiresPresenter
import reactivecircus.flowbinding.android.view.clicks import reactivecircus.flowbinding.android.view.clicks
@@ -165,8 +165,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
// SY --> // SY -->
private var ehUtilsVisible = false private var ehUtilsVisible = false
private val autoScrollFlow = MutableSharedFlow<Unit>()
private var autoScrollJob: Job? = null
private val sourceManager: SourceManager by injectLazy() private val sourceManager: SourceManager by injectLazy()
private var lastShiftDoubleState: Boolean? = null private var lastShiftDoubleState: Boolean? = null
@@ -264,19 +262,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
binding.expandEhButton.setImageResource(R.drawable.ic_keyboard_arrow_down_white_32dp) binding.expandEhButton.setImageResource(R.drawable.ic_keyboard_arrow_down_white_32dp)
} }
} }
private fun setupAutoscroll(interval: Double) {
autoScrollJob?.cancel()
if (interval == -1.0) return
val duration = interval.seconds
autoScrollJob = lifecycleScope.launch(Dispatchers.IO) {
while (true) {
delay(duration)
autoScrollFlow.emit(Unit)
}
}
}
// SY <-- // SY <--
/** /**
@@ -291,10 +276,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
readingModeToast?.cancel() readingModeToast?.cancel()
progressDialog?.dismiss() progressDialog?.dismiss()
progressDialog = null progressDialog = null
// SY -->
autoScrollJob?.cancel()
autoScrollJob = null
// SY <--
} }
/** /**
@@ -324,6 +305,11 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
} }
override fun onPause() {
presenter.saveProgress()
super.onPause()
}
/** /**
* Set menu visibility again on activity resume to apply immersive mode again if needed. * Set menu visibility again on activity resume to apply immersive mode again if needed.
* Helps with rotations. * Helps with rotations.
@@ -716,31 +702,34 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
) )
binding.ehAutoscroll.checkedChanges() binding.ehAutoscroll.checkedChanges()
.onEach { .combine(binding.ehAutoscrollFreq.textChanges()) { checked, text ->
setupAutoscroll( checked to text
if (it) {
preferences.autoscrollInterval().get().toDouble()
} else {
-1.0
},
)
} }
.launchIn(lifecycleScope) .mapLatest { (checked, text) ->
val parsed = text.toString().toDoubleOrNull()
binding.ehAutoscrollFreq.textChanges()
.onEach {
val parsed = it.toString().toDoubleOrNull()
if (parsed == null || parsed <= 0 || parsed > 9999) { if (parsed == null || parsed <= 0 || parsed > 9999) {
binding.ehAutoscrollFreq.error = getString(R.string.eh_autoscroll_freq_invalid) binding.ehAutoscrollFreq.error = getString(R.string.eh_autoscroll_freq_invalid)
preferences.autoscrollInterval().set(-1f) preferences.autoscrollInterval().set(-1f)
binding.ehAutoscroll.isEnabled = false binding.ehAutoscroll.isEnabled = false
setupAutoscroll(-1.0)
} else { } else {
binding.ehAutoscrollFreq.error = null binding.ehAutoscrollFreq.error = null
preferences.autoscrollInterval().set(parsed.toFloat()) preferences.autoscrollInterval().set(parsed.toFloat())
binding.ehAutoscroll.isEnabled = true binding.ehAutoscroll.isEnabled = true
setupAutoscroll(if (binding.ehAutoscroll.isChecked) parsed else -1.0) if (checked) {
repeatOnLifecycle(Lifecycle.State.STARTED) {
val interval = parsed.seconds
while (true) {
delay(interval)
viewer.let { v ->
when (v) {
is PagerViewer -> v.moveToNext()
is WebtoonViewer -> v.scrollDown()
}
}
}
}
}
} }
} }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
@@ -844,15 +833,6 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
.show() .show()
} }
.launchIn(lifecycleScope) .launchIn(lifecycleScope)
autoScrollFlow
.onEach {
viewer.let { v ->
if (v is PagerViewer) v.moveToNext()
else if (v is WebtoonViewer) v.scrollDown()
}
}
.launchIn(lifecycleScope)
} }
private fun exhCurrentpage(): ReaderPage? { private fun exhCurrentpage(): ReaderPage? {
@@ -2,12 +2,10 @@ package eu.kanade.tachiyomi.ui.reader
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory
import android.net.Uri import android.net.Uri
import android.os.Bundle import android.os.Bundle
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@@ -29,6 +27,7 @@ import eu.kanade.tachiyomi.source.online.all.MergedSource
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem import eu.kanade.tachiyomi.ui.reader.chapter.ReaderChapterItem
import eu.kanade.tachiyomi.ui.reader.loader.ChapterLoader 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.InsertPage
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
@@ -64,6 +63,7 @@ import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import tachiyomi.decoder.ImageDecoder
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@@ -425,6 +425,14 @@ class ReaderPresenter(
* that the user doesn't have to wait too long to continue reading. * that the user doesn't have to wait too long to continue reading.
*/ */
private fun preload(chapter: ReaderChapter) { 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) { if (chapter.state != ReaderChapter.State.Wait && chapter.state !is ReaderChapter.State.Error) {
return return
} }
@@ -549,6 +557,10 @@ class ReaderPresenter(
} }
} }
fun saveProgress() {
getCurrentChapter()?.let { onChapterChanged(it) }
}
/** /**
* Called from the activity to preload the given [chapter]. * Called from the activity to preload the given [chapter].
*/ */
@@ -761,7 +773,7 @@ class ReaderPresenter(
} }
} }
private suspend fun saveImages( private fun saveImages(
page1: ReaderPage, page1: ReaderPage,
page2: ReaderPage, page2: ReaderPage,
isLTR: Boolean, isLTR: Boolean,
@@ -773,11 +785,8 @@ class ReaderPresenter(
ImageUtil.findImageType(stream1) ?: throw Exception("Not an image") ImageUtil.findImageType(stream1) ?: throw Exception("Not an image")
val stream2 = page2.stream!! val stream2 = page2.stream!!
ImageUtil.findImageType(stream2) ?: throw Exception("Not an image") ImageUtil.findImageType(stream2) ?: throw Exception("Not an image")
val imageBytes = stream1().readBytes() val imageBitmap = ImageDecoder.newInstance(stream1())?.decode()!!
val imageBitmap = BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) val imageBitmap2 = ImageDecoder.newInstance(stream2())?.decode()!!
val imageBytes2 = stream2().readBytes()
val imageBitmap2 = BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size)
val chapter = page1.chapter.chapter val chapter = page1.chapter.chapter
@@ -872,20 +881,22 @@ class ReaderPresenter(
Observable Observable
.fromCallable { .fromCallable {
if (manga.isLocal()) { stream().use {
val context = Injekt.get<Application>() if (manga.isLocal()) {
LocalSource.updateCover(context, manga, stream()) val context = Injekt.get<Application>()
manga.updateCoverLastModified(db) LocalSource.updateCover(context, manga, it)
R.string.cover_updated
SetAsCoverResult.Success
} else {
if (manga.favorite) {
coverCache.setCustomCoverToCache(manga, stream())
manga.updateCoverLastModified(db) manga.updateCoverLastModified(db)
coverCache.clearMemoryCache() coverCache.clearMemoryCache()
SetAsCoverResult.Success SetAsCoverResult.Success
} else { } else {
SetAsCoverResult.AddToLibraryFirst if (manga.favorite) {
coverCache.setCustomCoverToCache(manga, it)
manga.updateCoverLastModified(db)
coverCache.clearMemoryCache()
SetAsCoverResult.Success
} else {
SetAsCoverResult.AddToLibraryFirst
}
} }
} }
} }
@@ -26,7 +26,6 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
is ChapterTransition.Prev -> bindPrevChapterTransition(transition) is ChapterTransition.Prev -> bindPrevChapterTransition(transition)
is ChapterTransition.Next -> bindNextChapterTransition(transition) is ChapterTransition.Next -> bindNextChapterTransition(transition)
} }
missingChapterWarning(transition) missingChapterWarning(transition)
} }
@@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.graphics.BitmapFactory
import android.view.Gravity import android.view.Gravity
import android.view.LayoutInflater import android.view.LayoutInflater
import androidx.core.view.isVisible import androidx.core.view.isVisible
@@ -24,6 +23,7 @@ import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
import tachiyomi.decoder.ImageDecoder
import java.io.ByteArrayInputStream import java.io.ByteArrayInputStream
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -366,13 +366,17 @@ class PagerPageHolder(
} }
val imageBytes = imageStream.readBytes() val imageBytes = imageStream.readBytes()
val imageBitmap = try { val imageBitmap = try {
BitmapFactory.decodeByteArray(imageBytes, 0, imageBytes.size) ImageDecoder.newInstance(imageBytes.inputStream())?.decode()
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
null
}
if (imageBitmap == null) {
imageStream2.close() imageStream2.close()
imageStream.close() imageStream.close()
page.fullPage = true page.fullPage = true
splitDoublePages() splitDoublePages()
logcat(LogPriority.ERROR, e) { "Cannot combine pages" } logcat(LogPriority.ERROR) { "Cannot combine pages" }
return imageBytes.inputStream() return imageBytes.inputStream()
} }
viewer.scope.launchUI { progressIndicator.setProgress(96) } viewer.scope.launchUI { progressIndicator.setProgress(96) }
@@ -389,14 +393,18 @@ class PagerPageHolder(
val imageBytes2 = imageStream2.readBytes() val imageBytes2 = imageStream2.readBytes()
val imageBitmap2 = try { val imageBitmap2 = try {
BitmapFactory.decodeByteArray(imageBytes2, 0, imageBytes2.size) ImageDecoder.newInstance(imageBytes2.inputStream())?.decode()
} catch (e: Exception) { } catch (e: Exception) {
logcat(LogPriority.ERROR, e) { "Cannot combine pages" }
null
}
if (imageBitmap2 == null) {
imageStream2.close() imageStream2.close()
imageStream.close() imageStream.close()
extraPage?.fullPage = true extraPage?.fullPage = true
page.isolatedPage = true page.isolatedPage = true
splitDoublePages() splitDoublePages()
logcat(LogPriority.ERROR, e) { "Cannot combine pages" } logcat(LogPriority.ERROR) { "Cannot combine pages" }
return imageBytes.inputStream() return imageBytes.inputStream()
} }
viewer.scope.launchUI { progressIndicator.setProgress(97) } viewer.scope.launchUI { progressIndicator.setProgress(97) }
@@ -67,9 +67,14 @@ abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
set(value) { set(value) {
field = value field = value
if (value) { if (value) {
awaitingIdleViewerChapters?.let { awaitingIdleViewerChapters?.let { viewerChapters ->
setChaptersDoubleShift(it) setChaptersDoubleShift(viewerChapters)
awaitingIdleViewerChapters = null awaitingIdleViewerChapters = null
if (viewerChapters.currChapter.pages?.size == 1) {
adapter.nextTransition?.to?.let {
activity.requestPreloadChapter(it)
}
}
} }
} }
} }
@@ -46,7 +46,7 @@ class WebtoonTransitionHolder(
layout.orientation = LinearLayout.VERTICAL layout.orientation = LinearLayout.VERTICAL
layout.gravity = Gravity.CENTER layout.gravity = Gravity.CENTER
val paddingVertical = 48.dpToPx val paddingVertical = 128.dpToPx
val paddingHorizontal = 32.dpToPx val paddingHorizontal = 32.dpToPx
layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical) layout.setPadding(paddingHorizontal, paddingVertical, paddingHorizontal, paddingVertical)
@@ -104,6 +104,12 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
activity.requestPreloadChapter(firstItem.to) activity.requestPreloadChapter(firstItem.to)
} }
} }
val lastIndex = layoutManager.findLastEndVisibleItemPosition()
val lastItem = adapter.items.getOrNull(lastIndex)
if (lastItem is ChapterTransition.Next && lastItem.to == null) {
activity.showMenu()
}
} }
}, },
) )
@@ -223,9 +229,6 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
if (toChapter != null) { if (toChapter != null) {
logcat { "Request preload destination chapter because we're on the transition" } logcat { "Request preload destination chapter because we're on the transition" }
activity.requestPreloadChapter(toChapter) 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()
} }
} }
@@ -252,7 +255,7 @@ class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = tr
logcat { "moveToPage" } logcat { "moveToPage" }
val position = adapter.items.indexOf(page) val position = adapter.items.indexOf(page)
if (position != -1) { if (position != -1) {
recycler.scrollToPosition(position) layoutManager.scrollToPositionWithOffset(position, 0)
if (layoutManager.findLastEndVisibleItemPosition() == -1) { if (layoutManager.findLastEndVisibleItemPosition() == -1) {
onScrolled(pos = position) onScrolled(pos = position)
} }
@@ -6,6 +6,8 @@ import android.content.ActivityNotFoundException
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.provider.Settings import android.provider.Settings
import android.webkit.WebStorage
import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import androidx.appcompat.app.AlertDialog import androidx.appcompat.app.AlertDialog
import androidx.core.net.toUri import androidx.core.net.toUri
@@ -20,9 +22,13 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Target
import eu.kanade.tachiyomi.data.preference.PreferenceValues import eu.kanade.tachiyomi.data.preference.PreferenceValues
import eu.kanade.tachiyomi.network.NetworkHelper 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_ADGUARD
import eu.kanade.tachiyomi.network.PREF_DOH_ALIDNS
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE 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_GOOGLE
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD101
import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9 import eu.kanade.tachiyomi.network.PREF_DOH_QUAD9
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES import eu.kanade.tachiyomi.source.SourceManager.Companion.DELEGATED_SOURCES
@@ -49,7 +55,9 @@ import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.isPackageInstalled 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.powerManager
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import exh.debug.SettingsDebugController import exh.debug.SettingsDebugController
import exh.log.EHLogLevel import exh.log.EHLogLevel
@@ -57,10 +65,12 @@ import exh.source.BlacklistedSources
import exh.source.EH_SOURCE_ID import exh.source.EH_SOURCE_ID
import exh.source.EXH_SOURCE_ID import exh.source.EXH_SOURCE_ID
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import logcat.LogPriority
import rikka.sui.Sui import rikka.sui.Sui
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.File
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
class SettingsAdvancedController : SettingsController() { class SettingsAdvancedController : SettingsController() {
@@ -87,7 +97,7 @@ class SettingsAdvancedController : SettingsController() {
key = Keys.verboseLogging key = Keys.verboseLogging
titleRes = R.string.pref_verbose_logging titleRes = R.string.pref_verbose_logging
summaryRes = R.string.pref_verbose_logging_summary summaryRes = R.string.pref_verbose_logging_summary
defaultValue = false defaultValue = isDevFlavor
onChange { onChange {
activity?.toast(R.string.requires_app_restart) activity?.toast(R.string.requires_app_restart)
@@ -170,6 +180,12 @@ class SettingsAdvancedController : SettingsController() {
activity?.toast(R.string.cookies_cleared) activity?.toast(R.string.cookies_cleared)
} }
} }
preference {
key = "pref_clear_webview_data"
titleRes = R.string.pref_clear_webview_data
onClick { clearWebViewData() }
}
intListPreference { intListPreference {
key = Keys.dohProvider key = Keys.dohProvider
titleRes = R.string.pref_dns_over_https titleRes = R.string.pref_dns_over_https
@@ -179,6 +195,10 @@ class SettingsAdvancedController : SettingsController() {
"Google", "Google",
"AdGuard", "AdGuard",
"Quad9", "Quad9",
"AliDNS",
"DNSPod",
"360",
"Quad 101",
) )
entryValues = arrayOf( entryValues = arrayOf(
"-1", "-1",
@@ -186,6 +206,10 @@ class SettingsAdvancedController : SettingsController() {
PREF_DOH_GOOGLE.toString(), PREF_DOH_GOOGLE.toString(),
PREF_DOH_ADGUARD.toString(), PREF_DOH_ADGUARD.toString(),
PREF_DOH_QUAD9.toString(), PREF_DOH_QUAD9.toString(),
PREF_DOH_ALIDNS.toString(),
PREF_DOH_DNSPOD.toString(),
PREF_DOH_360.toString(),
PREF_DOH_QUAD101.toString(),
) )
defaultValue = "-1" defaultValue = "-1"
summary = "%s" summary = "%s"
@@ -486,11 +510,30 @@ class SettingsAdvancedController : SettingsController() {
resources?.getString(R.string.used_cache, chapterCache.readableSize) resources?.getString(R.string.used_cache, chapterCache.readableSize)
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e)
withUIContext { activity?.toast(R.string.cache_delete_error) } 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 companion object { private companion object {
// SY --> // SY -->
private var job: Job? = null private var job: Job? = null
@@ -125,20 +125,20 @@ class SettingsDownloadController : SettingsController() {
titleRes = R.string.pref_category_auto_download titleRes = R.string.pref_category_auto_download
switchPreference { switchPreference {
bindTo(preferences.downloadNew()) bindTo(preferences.downloadNewChapter())
titleRes = R.string.pref_download_new titleRes = R.string.pref_download_new
} }
preference { preference {
bindTo(preferences.downloadNewCategories()) bindTo(preferences.downloadNewChapterCategories())
titleRes = R.string.categories titleRes = R.string.categories
onClick { onClick {
DownloadCategoriesDialog().showDialog(router) DownloadCategoriesDialog().showDialog(router)
} }
visibleIf(preferences.downloadNew()) { it } visibleIf(preferences.downloadNewChapter()) { it }
fun updateSummary() { fun updateSummary() {
val selectedCategories = preferences.downloadNewCategories().get() val selectedCategories = preferences.downloadNewChapterCategories().get()
.mapNotNull { id -> categories.find { it.id == id.toInt() } } .mapNotNull { id -> categories.find { it.id == id.toInt() } }
.sortedBy { it.order } .sortedBy { it.order }
val includedItemsText = if (selectedCategories.isEmpty()) { val includedItemsText = if (selectedCategories.isEmpty()) {
@@ -147,7 +147,7 @@ class SettingsDownloadController : SettingsController() {
selectedCategories.joinToString { it.name } selectedCategories.joinToString { it.name }
} }
val excludedCategories = preferences.downloadNewCategoriesExclude().get() val excludedCategories = preferences.downloadNewChapterCategoriesExclude().get()
.mapNotNull { id -> categories.find { it.id == id.toInt() } } .mapNotNull { id -> categories.find { it.id == id.toInt() } }
.sortedBy { it.order } .sortedBy { it.order }
val excludedItemsText = if (excludedCategories.isEmpty()) { val excludedItemsText = if (excludedCategories.isEmpty()) {
@@ -163,10 +163,10 @@ class SettingsDownloadController : SettingsController() {
} }
} }
preferences.downloadNewCategories().asFlow() preferences.downloadNewChapterCategories().asFlow()
.onEach { updateSummary() } .onEach { updateSummary() }
.launchIn(viewScope) .launchIn(viewScope)
preferences.downloadNewCategoriesExclude().asFlow() preferences.downloadNewChapterCategoriesExclude().asFlow()
.onEach { updateSummary() } .onEach { updateSummary() }
.launchIn(viewScope) .launchIn(viewScope)
} }
@@ -254,8 +254,8 @@ class SettingsDownloadController : SettingsController() {
var selected = categories var selected = categories
.map { .map {
when (it.id.toString()) { when (it.id.toString()) {
in preferences.downloadNewCategories().get() -> QuadStateTextView.State.CHECKED.ordinal in preferences.downloadNewChapterCategories().get() -> QuadStateTextView.State.CHECKED.ordinal
in preferences.downloadNewCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal in preferences.downloadNewChapterCategoriesExclude().get() -> QuadStateTextView.State.INVERSED.ordinal
else -> QuadStateTextView.State.UNCHECKED.ordinal else -> QuadStateTextView.State.UNCHECKED.ordinal
} }
} }
@@ -282,8 +282,8 @@ class SettingsDownloadController : SettingsController() {
.map { categories[it].id.toString() } .map { categories[it].id.toString() }
.toSet() .toSet()
preferences.downloadNewCategories().set(included) preferences.downloadNewChapterCategories().set(included)
preferences.downloadNewCategoriesExclude().set(excluded) preferences.downloadNewChapterCategoriesExclude().set(excluded)
} }
.setNegativeButton(android.R.string.cancel, null) .setNegativeButton(android.R.string.cancel, null)
.create() .create()
@@ -11,7 +11,9 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.DEVICE_BATTERY_NOT_LOW
import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING import eu.kanade.tachiyomi.data.preference.DEVICE_CHARGING
import eu.kanade.tachiyomi.data.preference.DEVICE_NETWORK_NOT_METERED
import eu.kanade.tachiyomi.data.preference.DEVICE_ONLY_ON_WIFI 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_HAS_UNREAD
import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED import eu.kanade.tachiyomi.data.preference.MANGA_NON_COMPLETED
@@ -185,8 +187,8 @@ class SettingsLibraryController : SettingsController() {
multiSelectListPreference { multiSelectListPreference {
bindTo(preferences.libraryUpdateDeviceRestriction()) bindTo(preferences.libraryUpdateDeviceRestriction())
titleRes = R.string.pref_library_update_restriction titleRes = R.string.pref_library_update_restriction
entriesRes = arrayOf(R.string.connected_to_wifi, R.string.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_CHARGING) entryValues = arrayOf(DEVICE_ONLY_ON_WIFI, DEVICE_NETWORK_NOT_METERED, DEVICE_CHARGING, DEVICE_BATTERY_NOT_LOW)
visibleIf(preferences.libraryUpdateInterval()) { it > 0 } visibleIf(preferences.libraryUpdateInterval()) { it > 0 }
@@ -202,7 +204,9 @@ class SettingsLibraryController : SettingsController() {
.map { .map {
when (it) { when (it) {
DEVICE_ONLY_ON_WIFI -> context.getString(R.string.connected_to_wifi) 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_CHARGING -> context.getString(R.string.charging)
DEVICE_BATTERY_NOT_LOW -> context.getString(R.string.battery_not_low)
else -> it else -> it
} }
} }
@@ -66,7 +66,7 @@ class ClearDatabaseController :
adapter = FlexibleAdapter<ClearDatabaseSourceItem>(null, this, true) adapter = FlexibleAdapter<ClearDatabaseSourceItem>(null, this, true)
binding.recycler.adapter = adapter binding.recycler.adapter = adapter
binding.recycler.layoutManager = LinearLayoutManager(activity) binding.recycler.layoutManager = LinearLayoutManager(activity!!)
binding.recycler.setHasFixedSize(true) binding.recycler.setHasFixedSize(true)
adapter?.fastScroller = binding.fastScroller adapter?.fastScroller = binding.fastScroller
recycler = binding.recycler recycler = binding.recycler
@@ -56,14 +56,14 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
if (!favorite) return false if (!favorite) return false
// Boolean to determine if user wants to automatically download new chapters. // Boolean to determine if user wants to automatically download new chapters.
val downloadNew = prefs.downloadNew().get() val downloadNewChapter = prefs.downloadNewChapter().get()
if (!downloadNew) return false if (!downloadNewChapter) return false
val categoriesToDownload = prefs.downloadNewCategories().get().map(String::toInt) val includedCategories = prefs.downloadNewChapterCategories().get().map { it.toInt() }
val categoriesToExclude = prefs.downloadNewCategoriesExclude().get().map(String::toInt) val excludedCategories = prefs.downloadNewChapterCategoriesExclude().get().map { it.toInt() }
// Default: download from all categories // Default: Download from all categories
if (categoriesToDownload.isEmpty() && categoriesToExclude.isEmpty()) return true if (includedCategories.isEmpty() && excludedCategories.isEmpty()) return true
// Get all categories, else default category (0) // Get all categories, else default category (0)
val categoriesForManga = val categoriesForManga =
@@ -72,8 +72,11 @@ fun Manga.shouldDownloadNewChapters(db: DatabaseHelper, prefs: PreferencesHelper
.takeUnless { it.isEmpty() } ?: listOf(0) .takeUnless { it.isEmpty() } ?: listOf(0)
// In excluded category // 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 // In included category
return categoriesForManga.intersect(categoriesToDownload).isNotEmpty() return categoriesForManga.any { it in includedCategories }
} }
@@ -12,6 +12,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import java.util.Date import java.util.Date
import java.util.TreeSet 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. * Helper method for syncing the list of chapters from the source with the ones from the database.
@@ -60,6 +61,9 @@ fun syncChaptersWithSource(
} }
} }
var maxTimestamp = 0L // in previous chapters to add
val rightNow = Date().time
for (sourceChapter in sourceChapters) { for (sourceChapter in sourceChapters) {
// This forces metadata update for the main viewable things in the chapter list. // This forces metadata update for the main viewable things in the chapter list.
if (source is HttpSource) { if (source is HttpSource) {
@@ -73,7 +77,9 @@ fun syncChaptersWithSource(
// Add the chapter if not in db already, or update if the metadata changed. // Add the chapter if not in db already, or update if the metadata changed.
if (dbChapter == null) { if (dbChapter == null) {
if (sourceChapter.date_upload == 0L) { 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) toAdd.add(sourceChapter)
} else { } else {
@@ -98,6 +104,7 @@ fun syncChaptersWithSource(
return Pair(emptyList(), emptyList()) return Pair(emptyList(), emptyList())
} }
// Keep it a List instead of a Set. See #6372.
val readded = mutableListOf<Chapter>() val readded = mutableListOf<Chapter>()
db.inTransaction { db.inTransaction {
@@ -170,6 +177,7 @@ fun syncChaptersWithSource(
db.updateLastUpdated(manga).executeAsBlocking() db.updateLastUpdated(manga).executeAsBlocking()
} }
@Suppress("ConvertArgumentToSet")
return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList()) return Pair(toAdd.subtract(readded).toList(), toDelete.subtract(readded).toList())
} }
@@ -0,0 +1,6 @@
package eu.kanade.tachiyomi.util.system
import eu.kanade.tachiyomi.BuildConfig
val isDevFlavor: Boolean
get() = BuildConfig.FLAVOR == "dev"
@@ -87,7 +87,11 @@ fun Context.copyToClipboard(label: String, content: String) {
val clipboard = getSystemService<ClipboardManager>()!! val clipboard = getSystemService<ClipboardManager>()!!
clipboard.setPrimaryClip(ClipData.newPlainText(label, content)) 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) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) logcat(LogPriority.ERROR, e)
toast(R.string.clipboard_copy_error) toast(R.string.clipboard_copy_error)
@@ -11,6 +11,7 @@ import android.graphics.drawable.ColorDrawable
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.graphics.drawable.GradientDrawable import android.graphics.drawable.GradientDrawable
import android.os.Build import android.os.Build
import android.webkit.MimeTypeMap
import androidx.annotation.ColorInt import androidx.annotation.ColorInt
import androidx.core.graphics.alpha import androidx.core.graphics.alpha
import androidx.core.graphics.applyCanvas import androidx.core.graphics.applyCanvas
@@ -59,6 +60,12 @@ object ImageUtil {
return null return null
} }
fun getExtensionFromMimeType(mime: String?): String {
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime)
?: SUPPLEMENTARY_MIMETYPE_MAPPING[mime]
?: "jpg"
}
fun isAnimatedAndSupported(stream: InputStream): Boolean { fun isAnimatedAndSupported(stream: InputStream): Boolean {
try { try {
val type = getImageType(stream) ?: return false val type = getImageType(stream) ?: return false
@@ -396,6 +403,12 @@ object ImageUtil {
private fun Int.isWhite(): Boolean = private fun Int.isWhite(): Boolean =
red + blue + green > 740 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",
)
fun mergeBitmaps( fun mergeBitmaps(
imageBitmap: Bitmap, imageBitmap: Bitmap,
imageBitmap2: Bitmap, imageBitmap2: Bitmap,
@@ -73,7 +73,7 @@ open class ExtendedNavigationView @JvmOverloads constructor(
* @param context any context. * @param context any context.
* @param resId the vector resource to load and tint * @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 { return AppCompatResources.getDrawable(context, resId)!!.apply {
setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal)) setTint(context.getResourceColor(if (enabled) colorAttrRes else R.attr.colorControlNormal))
} }
@@ -29,7 +29,7 @@ class QuadStateTextView @JvmOverloads constructor(context: Context, attrs: Attri
val tint = if (state == State.UNCHECKED) { val tint = if (state == State.UNCHECKED) {
context.getThemeColor(R.attr.colorControlNormal) context.getThemeColor(R.attr.colorControlNormal)
} else { } else {
context.getThemeColor(R.attr.colorAccent) context.getThemeColor(R.attr.colorPrimary)
} }
if (tint != 0) { if (tint != 0) {
TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(tint)) TextViewCompat.setCompoundDrawableTintList(this, ColorStateList.valueOf(tint))
+2 -1
View File
@@ -1,3 +1,5 @@
@file:Suppress("DEPRECATION")
package exh package exh
import android.content.Context import android.content.Context
@@ -309,7 +311,6 @@ object EXHMigrations {
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0) val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true) val oldSortingDirection = prefs.getBoolean(PreferenceKeys.librarySortingDirection, true)
@Suppress("DEPRECATION")
val newSortingMode = when (oldSortingMode) { val newSortingMode = when (oldSortingMode) {
LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL LibrarySort.ALPHA -> SortModeSetting.ALPHABETICAL
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ LibrarySort.LAST_READ -> SortModeSetting.LAST_READ
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,46 @@
package exh.eh.tags
object Cosplayer : TagList {
override fun getTags1() = listOf(
"cosplayer:akane araragi",
"cosplayer:aleksandra bodler",
"cosplayer:aokotan",
"cosplayer:arty huang",
"cosplayer:atsuki",
"cosplayer:bishoujomom",
"cosplayer:carry key",
"cosplayer:chunmomo",
"cosplayer:gumiho hannya",
"cosplayer:hane ame",
"cosplayer:hinaughtya",
"cosplayer:holly wolf",
"cosplayer:iori moe",
"cosplayer:jill",
"cosplayer:kalinka fox",
"cosplayer:kaya huang",
"cosplayer:kuuko w",
"cosplayer:lenfried",
"cosplayer:miih cosplay",
"cosplayer:mikomin",
"cosplayer:misa daidai",
"cosplayer:momoiro reku",
"cosplayer:momokun",
"cosplayer:nadyasonika",
"cosplayer:nora fawn",
"cosplayer:octokuro",
"cosplayer:oichi",
"cosplayer:rioko",
"cosplayer:rocksy light",
"cosplayer:saku",
"cosplayer:sakurai hinoki",
"cosplayer:shiro kitsune",
"cosplayer:siao ding",
"cosplayer:smoettii",
"cosplayer:valery himera",
"cosplayer:velvet",
"cosplayer:wildhoney423",
"cosplayer:yume",
"cosplayer:yuzupyon",
)
}
+489
View File
@@ -0,0 +1,489 @@
package exh.eh.tags
object Female : TagList {
override fun getTags1() = listOf(
"female:abortion",
"female:absorption",
"female:adventitious vagina",
"female:age progression",
"female:age regression",
"female:ahegao",
"female:albino",
"female:alien girl",
"female:all the way through",
"female:amputee",
"female:anal",
"female:anal birth",
"female:anal intercourse",
"female:anal prolapse",
"female:angel",
"female:animal on furry",
"female:animegao",
"female:anorexic",
"female:apron",
"female:armpit licking",
"female:armpit sex",
"female:asphyxiation",
"female:ass expansion",
"female:assjob",
"female:aunt",
"female:autofellatio",
"female:autopaizuri",
"female:bald",
"female:ball sucking",
"female:balljob",
"female:bandages",
"female:bandaid",
"female:bbw",
"female:bdsm",
"female:bear",
"female:bear girl",
"female:beauty mark",
"female:bee girl",
"female:bestiality",
"female:big areolae",
"female:big ass",
"female:big balls",
"female:big breasts",
"female:big clit",
"female:big nipples",
"female:big penis",
"female:big vagina",
"female:bike shorts",
"female:bikini",
"female:birth",
"female:bisexual",
"female:blackmail",
"female:blind",
"female:blindfold",
"female:blood",
"female:bloomers",
"female:blowjob",
"female:blowjob face",
"female:body modification",
"female:body painting",
"female:body swap",
"female:body writing",
"female:bodystocking",
"female:bodysuit",
"female:bondage",
"female:brain fuck",
"female:breast expansion",
"female:breast feeding",
"female:breast reduction",
"female:bride",
"female:bukkake",
"female:bunny girl",
"female:burping",
"female:business suit",
"female:butler",
"female:cannibalism",
"female:cashier",
"female:catfight",
"female:catgirl",
"female:cbt",
"female:centaur",
"female:cervix penetration",
"female:cervix prolapse",
"female:chastity belt",
"female:cheating",
"female:cheerleader",
"female:chikan",
"female:chinese dress",
"female:chloroform",
"female:christmas",
"female:clamp",
"female:clit growth",
"female:clit stimulation",
"female:clone",
"female:clothed male nude female",
"female:coach",
"female:cock ring",
"female:cockslapping",
"female:collar",
"female:condom",
"female:conjoined",
"female:coprophagia",
"female:corruption",
"female:corset",
"female:cosplaying",
"female:cousin",
"female:cow",
"female:cowgirl",
"female:crab",
"female:crossdressing",
"female:crotch tattoo",
"female:crown",
"female:cum bath",
"female:cum in eye",
"female:cum swap",
"female:cumflation",
"female:cunnilingus",
"female:dark nipples",
"female:dark sclera",
"female:dark skin",
"female:daughter",
"female:deepthroat",
"female:deer",
"female:deer girl",
"female:defloration",
"female:demon girl",
"female:diaper",
"female:dick growth",
"female:dickgirl on dickgirl",
"female:dickgirls only",
"female:dicknipples",
"female:dinosaur",
"female:dog",
"female:dog girl",
"female:doll joints",
"female:donkey",
"female:double anal",
"female:double blowjob",
"female:double penetration",
"female:double vaginal",
"female:dougi",
"female:draenei",
"female:dragon",
"female:drill hair",
"female:drugs",
"female:drunk",
"female:ear fuck",
"female:eel",
"female:eggs",
"female:electric shocks",
"female:elephant",
"female:elf",
"female:emotionless sex",
"female:enema",
"female:exhibitionism",
"female:exposed clothing",
"female:eye penetration",
"female:eye-covering bang",
"female:eyemask",
"female:eyepatch",
"female:facesitting",
"female:facial hair",
"female:fairy",
"female:farting",
"female:females only",
"female:femdom",
"female:fft threesome",
"female:filming",
"female:fingering",
"female:first person perspective",
"female:fish",
"female:fishnets",
"female:fisting",
"female:focus anal",
"female:focus blowjob",
"female:focus paizuri",
"female:food on body",
"female:foot insertion",
"female:foot licking",
"female:footjob",
"female:forniphilia",
"female:fox",
"female:fox girl",
"female:freckles",
"female:frog",
"female:frottage",
"female:fundoshi",
"female:furry",
"female:futanari",
"female:gag",
"female:gaping",
"female:garter belt",
"female:gasmask",
"female:gender change",
"female:gender morph",
"female:ghost",
"female:giantess",
"female:gigantic breasts",
"female:glasses",
"female:glory hole",
"female:gloves",
"female:gokkun",
"female:gothic lolita",
"female:granddaughter",
"female:grandmother",
"female:group",
"female:growth",
"female:guro",
"female:gyaru",
"female:gymshorts",
"female:hair buns",
"female:hairjob",
"female:hairy",
"female:hairy armpits",
"female:handicapped",
"female:handjob",
"female:harem",
"female:harness",
"female:harpy",
"female:headless",
"female:headphones",
"female:heterochromia",
"female:hijab",
"female:hood",
"female:horns",
"female:horse",
"female:horse cock",
"female:horse girl",
"female:hotpants",
"female:huge breasts",
"female:huge penis",
"female:human cattle",
"female:human on furry",
"female:humiliation",
"female:impregnation",
"female:incest",
"female:infantilism",
"female:inflation",
"female:insect",
"female:insect girl",
"female:inverted nipples",
"female:invisible",
"female:kemonomimi",
"female:kigurumi pajama",
"female:kimono",
"female:kissing",
"female:kunoichi",
"female:lab coat",
"female:lactation",
"female:large insertions",
"female:large tattoo",
"female:latex",
"female:layer cake",
"female:leash",
"female:leg lock",
"female:leotard",
"female:lingerie",
"female:lioness",
"female:living clothes",
"female:lizard girl",
"female:lolicon",
"female:long tongue",
"female:low bestiality",
"female:low lolicon",
"female:machine",
"female:maggot",
"female:magical girl",
"female:maid",
"female:makeup",
"female:male on dickgirl",
"female:masked face",
"female:masturbation",
"female:mecha girl",
"female:menstruation",
"female:mermaid",
"female:mesuiki",
"female:metal armor",
"female:midget",
"female:miko",
"female:milf",
"female:military",
"female:milking",
"female:mind break",
"female:mind control",
"female:minigirl",
"female:monkey",
"female:monkey girl",
"female:monster girl",
"female:moral degeneration",
"female:mother",
"female:mouse",
"female:mouse girl",
"female:mouth mask",
"female:multiple arms",
"female:multiple assjob",
"female:multiple breasts",
"female:multiple footjob",
"female:multiple handjob",
"female:multiple orgasms",
"female:multiple paizuri",
"female:multiple penises",
"female:muscle",
"female:muscle growth",
"female:nakadashi",
"female:navel fuck",
"female:nazi",
"female:necrophilia",
"female:netorare",
"female:niece",
"female:nipple birth",
"female:nipple expansion",
"female:nipple fuck",
"female:nose fuck",
"female:nose hook",
"female:nun",
"female:nurse",
"female:octopus",
"female:oil",
"female:old lady",
"female:onahole",
"female:oni",
"female:oppai loli",
"female:orc",
"female:orgasm denial",
"female:paizuri",
"female:pantyhose",
"female:pantyjob",
"female:parasite",
"female:pasties",
"female:penis birth",
"female:petplay",
"female:petrification",
"female:phimosis",
"female:phone sex",
"female:piercing",
"female:pig",
"female:pig girl",
"female:pillory",
"female:pirate",
"female:piss drinking",
"female:pixie cut",
"female:plant girl",
"female:pole dancing",
"female:policewoman",
"female:ponygirl",
"female:ponytail",
"female:possession",
"female:pregnant",
"female:prehensile hair",
"female:prolapse",
"female:prostate massage",
"female:prostitution",
"female:pubic stubble",
"female:public use",
"female:rabbit",
"female:raccoon girl",
"female:race queen",
"female:randoseru",
"female:rape",
"female:real doll",
"female:reptile",
"female:rhinoceros",
"female:rimjob",
"female:robot",
"female:ryona",
"female:saliva",
"female:scar",
"female:scat",
"female:school gym uniform",
"female:school swimsuit",
"female:schoolboy uniform",
"female:schoolgirl uniform",
"female:scrotal lingerie",
"female:selfcest",
"female:sex toys",
"female:shared senses",
"female:shark",
"female:sheep",
"female:sheep girl",
"female:shemale",
"female:shibari",
"female:shimapan",
"female:shrinking",
"female:sister",
"female:skinsuit",
"female:slave",
"female:sleeping",
"female:slime",
"female:slime girl",
"female:slug",
"female:small breasts",
"female:smegma",
"female:smell",
"female:smoking",
"female:snail girl",
"female:snake",
"female:snake girl",
"female:snuff",
"female:sole dickgirl",
"female:sole female",
"female:solo action",
"female:spanking",
"female:speculum",
"female:spider",
"female:spider girl",
"female:squid girl",
"female:squirting",
"female:ssbbw",
"female:stewardess",
"female:stockings",
"female:stomach deformation",
"female:strap-on",
"female:stretching",
"female:stuck in wall",
"female:sumata",
"female:sundress",
"female:sunglasses",
"female:sweating",
"female:swimsuit",
"female:swinging",
"female:syringe",
"female:table masturbation",
"female:tail",
"female:tail plug",
"female:tall girl",
"female:tanlines",
"female:teacher",
"female:tentacles",
"female:thigh high boots",
"female:tiara",
"female:tickling",
"female:tiger",
"female:tights",
"female:toddlercon",
"female:tomboy",
"female:tooth brushing",
"female:torture",
"female:tracksuit",
"female:trampling",
"female:transformation",
"female:tribadism",
"female:triple anal",
"female:triple penetration",
"female:triple vaginal",
"female:ttf threesome",
"female:tube",
"female:turtle",
"female:tutor",
"female:twins",
"female:twintails",
"female:unbirth",
"female:underwater",
"female:unicorn",
"female:unusual insertions",
"female:unusual pupils",
"female:unusual teeth",
"female:urethra insertion",
"female:urination",
"female:vacbed",
"female:vaginal sticker",
"female:vampire",
"female:very long hair",
"female:vomit",
"female:vore",
"female:voyeurism",
"female:vtuber",
"female:waiter",
"female:waitress",
"female:weight gain",
"female:whip",
"female:wings",
"female:witch",
"female:wolf",
"female:wolf girl",
"female:wooden horse",
"female:worm",
"female:wormhole",
"female:wrestling",
"female:x-ray",
"female:yandere",
"female:yuri",
"female:zombie",
)
}
File diff suppressed because it is too large Load Diff
+49
View File
@@ -0,0 +1,49 @@
package exh.eh.tags
object Language : TagList {
override fun getTags1() = listOf(
"language:arabic",
"language:bulgarian",
"language:catalan",
"language:cebuano",
"language:chinese",
"language:cree",
"language:creole",
"language:czech",
"language:danish",
"language:dutch",
"language:english",
"language:finnish",
"language:french",
"language:german",
"language:greek",
"language:hindi",
"language:hungarian",
"language:indonesian",
"language:irish",
"language:italian",
"language:japanese",
"language:korean",
"language:ladino",
"language:lao",
"language:norwegian",
"language:persian",
"language:polish",
"language:portuguese",
"language:rewrite",
"language:romanian",
"language:russian",
"language:sango",
"language:spanish",
"language:speechless",
"language:swedish",
"language:tagalog",
"language:text cleaned",
"language:thai",
"language:tigrinya",
"language:translated",
"language:turkish",
"language:vietnamese",
"language:zulu",
)
}
+381
View File
@@ -0,0 +1,381 @@
package exh.eh.tags
object Male : TagList {
override fun getTags1() = listOf(
"male:abortion",
"male:absorption",
"male:age progression",
"male:age regression",
"male:ahegao",
"male:alien",
"male:amputee",
"male:anal",
"male:anal birth",
"male:anal intercourse",
"male:anal prolapse",
"male:angel",
"male:animal on furry",
"male:animegao",
"male:apparel bukkake",
"male:apron",
"male:armpit licking",
"male:asphyxiation",
"male:bald",
"male:ball sucking",
"male:balls expansion",
"male:bandages",
"male:bat boy",
"male:bbm",
"male:bdsm",
"male:bear",
"male:bear boy",
"male:bestiality",
"male:big ass",
"male:big balls",
"male:big breasts",
"male:big nipples",
"male:big penis",
"male:bike shorts",
"male:bikini",
"male:birth",
"male:bisexual",
"male:blackmail",
"male:blind",
"male:blindfold",
"male:blood",
"male:bloomers",
"male:blowjob",
"male:blowjob face",
"male:body painting",
"male:body swap",
"male:body writing",
"male:bodystocking",
"male:bodysuit",
"male:bondage",
"male:breast feeding",
"male:bride",
"male:brother",
"male:bukkake",
"male:bull",
"male:bunny boy",
"male:burping",
"male:business suit",
"male:butler",
"male:cannibalism",
"male:cashier",
"male:cat",
"male:catboy",
"male:cbt",
"male:cervix prolapse",
"male:chastity belt",
"male:cheating",
"male:cheerleader",
"male:chikan",
"male:chinese dress",
"male:chloroform",
"male:christmas",
"male:clit stimulation",
"male:clone",
"male:clothed female nude male",
"male:coach",
"male:cock ring",
"male:collar",
"male:condom",
"male:coprophagia",
"male:corruption",
"male:corset",
"male:cosplaying",
"male:cousin",
"male:cowman",
"male:crab",
"male:crossdressing",
"male:crown",
"male:cuntboy",
"male:dark nipples",
"male:dark sclera",
"male:dark skin",
"male:deepthroat",
"male:deer",
"male:demon",
"male:dick growth",
"male:dickgirl on male",
"male:dilf",
"male:dinosaur",
"male:dog",
"male:dog boy",
"male:donkey",
"male:double anal",
"male:double blowjob",
"male:double penetration",
"male:dougi",
"male:dragon",
"male:drill hair",
"male:drugs",
"male:drunk",
"male:eel",
"male:eggs",
"male:electric shocks",
"male:elephant",
"male:elf",
"male:emotionless sex",
"male:enema",
"male:exhibitionism",
"male:exposed clothing",
"male:eye penetration",
"male:eye-covering bang",
"male:eyemask",
"male:eyepatch",
"male:facial hair",
"male:fairy",
"male:farting",
"male:father",
"male:feminization",
"male:filming",
"male:first person perspective",
"male:fish",
"male:fishnets",
"male:fisting",
"male:focus paizuri",
"male:food on body",
"male:foot licking",
"male:footjob",
"male:forniphilia",
"male:fox",
"male:fox boy",
"male:freckles",
"male:frog",
"male:frottage",
"male:fundoshi",
"male:furry",
"male:gag",
"male:gaping",
"male:garter belt",
"male:gasmask",
"male:gender change",
"male:gender morph",
"male:ghost",
"male:giant",
"male:glasses",
"male:glory hole",
"male:goblin",
"male:gokkun",
"male:gorilla",
"male:gothic lolita",
"male:grandfather",
"male:group",
"male:growth",
"male:guro",
"male:gyaru-oh",
"male:gymshorts",
"male:hair buns",
"male:hairy",
"male:handjob",
"male:harem",
"male:harpy",
"male:headphones",
"male:horns",
"male:horse",
"male:horse boy",
"male:horse cock",
"male:hotpants",
"male:human on furry",
"male:humiliation",
"male:impregnation",
"male:incest",
"male:infantilism",
"male:inflation",
"male:insect",
"male:insect boy",
"male:invisible",
"male:josou seme",
"male:kemonomimi",
"male:kigurumi pajama",
"male:kimono",
"male:kissing",
"male:lab coat",
"male:large insertions",
"male:large tattoo",
"male:latex",
"male:layer cake",
"male:leotard",
"male:lingerie",
"male:lion",
"male:lizard guy",
"male:long tongue",
"male:low bestiality",
"male:low shotacon",
"male:machine",
"male:maggot",
"male:magical girl",
"male:maid",
"male:makeup",
"male:males only",
"male:masked face",
"male:masturbation",
"male:merman",
"male:mesuiki",
"male:metal armor",
"male:midget",
"male:miko",
"male:military",
"male:mind break",
"male:mind control",
"male:miniguy",
"male:minotaur",
"male:monkey",
"male:monkey boy",
"male:monster",
"male:moral degeneration",
"male:mouse",
"male:mouse boy",
"male:multiple assjob",
"male:multiple footjob",
"male:multiple handjob",
"male:multiple orgasms",
"male:multiple penises",
"male:muscle",
"male:nakadashi",
"male:necrophilia",
"male:netorare",
"male:ninja",
"male:nipple birth",
"male:nose fuck",
"male:nose hook",
"male:nun",
"male:nurse",
"male:octopus",
"male:oil",
"male:old man",
"male:onahole",
"male:orc",
"male:orgasm denial",
"male:otokofutanari",
"male:paizuri",
"male:panther",
"male:pantyhose",
"male:pasties",
"male:pegging",
"male:penis birth",
"male:petplay",
"male:phimosis",
"male:piercing",
"male:pig",
"male:pig man",
"male:pillory",
"male:piss drinking",
"male:plant boy",
"male:pole dancing",
"male:policeman",
"male:possession",
"male:pregnant",
"male:priest",
"male:prolapse",
"male:prostate massage",
"male:prostitution",
"male:pubic stubble",
"male:public use",
"male:rabbit",
"male:randoseru",
"male:rape",
"male:reptile",
"male:rhinoceros",
"male:rimjob",
"male:robot",
"male:ryona",
"male:scar",
"male:scat",
"male:school gym uniform",
"male:school swimsuit",
"male:schoolboy uniform",
"male:schoolgirl uniform",
"male:selfcest",
"male:sex toys",
"male:shared senses",
"male:shark",
"male:shark boy",
"male:sheep",
"male:sheep boy",
"male:shibari",
"male:shimapan",
"male:shotacon",
"male:shrinking",
"male:skinsuit",
"male:slave",
"male:sleeping",
"male:slime",
"male:slime boy",
"male:slug",
"male:small penis",
"male:smegma",
"male:smell",
"male:smoking",
"male:snake",
"male:snake boy",
"male:snuff",
"male:sole male",
"male:solo action",
"male:spanking",
"male:speculum",
"male:spider",
"male:squid boy",
"male:stewardess",
"male:stockings",
"male:stomach deformation",
"male:stretching",
"male:stuck in wall",
"male:sundress",
"male:sunglasses",
"male:sweating",
"male:swimsuit",
"male:swinging",
"male:syringe",
"male:tail",
"male:tail plug",
"male:tall man",
"male:tanlines",
"male:teacher",
"male:tentacles",
"male:thigh high boots",
"male:tiara",
"male:tickling",
"male:tiger",
"male:tights",
"male:toddlercon",
"male:tomgirl",
"male:tooth brushing",
"male:torture",
"male:tracksuit",
"male:trampling",
"male:transformation",
"male:tube",
"male:turtle",
"male:tutor",
"male:twins",
"male:unbirth",
"male:uncle",
"male:unicorn",
"male:unusual pupils",
"male:urethra insertion",
"male:urination",
"male:vampire",
"male:very long hair",
"male:virginity",
"male:vomit",
"male:vore",
"male:voyeurism",
"male:waiter",
"male:waitress",
"male:whip",
"male:wings",
"male:witch",
"male:wolf",
"male:wolf boy",
"male:wooden horse",
"male:worm",
"male:wormhole",
"male:x-ray",
"male:yandere",
"male:yaoi",
)
}
+24
View File
@@ -0,0 +1,24 @@
package exh.eh.tags
object Mixed : TagList {
override fun getTags1() = listOf(
"mixed:animal on animal",
"mixed:body swap",
"mixed:ffm threesome",
"mixed:frottage",
"mixed:group",
"mixed:incest",
"mixed:mmf threesome",
"mixed:mmt threesome",
"mixed:mtf threesome",
"mixed:multimouth blowjob",
"mixed:multiple assjob",
"mixed:multiple footjob",
"mixed:multiple handjob",
"mixed:oyakodon",
"mixed:shimaidon",
"mixed:ttm threesome",
"mixed:twins",
)
}
+59
View File
@@ -0,0 +1,59 @@
package exh.eh.tags
object Other : TagList {
override fun getTags1() = listOf(
"other:3d",
"other:already uploaded",
"other:anaglyph",
"other:animated",
"other:anthology",
"other:artbook",
"other:caption",
"other:comic",
"other:compilation",
"other:dakimakura",
"other:figure",
"other:forbidden content",
"other:full censorship",
"other:full color",
"other:game sprite",
"other:goudoushi",
"other:hardcore",
"other:how to",
"other:incomplete",
"other:missing cover",
"other:mosaic censorship",
"other:multi-work series",
"other:multipanel sequence",
"other:no penetration",
"other:non-h imageset",
"other:non-nude",
"other:novel",
"other:nudity only",
"other:out of order",
"other:paperchild",
"other:poor grammar",
"other:realporn",
"other:redraw",
"other:replaced",
"other:rough translation",
"other:sample",
"other:scanmark",
"other:screenshots",
"other:sketch lines",
"other:stereoscopic",
"other:story arc",
"other:tankoubon",
"other:themeless",
"other:time stop",
"other:uncensored",
"other:variant set",
"other:watermarked",
"other:webtoon",
"other:western cg",
"other:western imageset",
"other:western non-h",
"other:yukkuri",
)
}
File diff suppressed because it is too large Load Diff
+17
View File
@@ -0,0 +1,17 @@
package exh.eh.tags
object ReClass : TagList {
override fun getTags1() = listOf(
"reclass:artistcg",
"reclass:asianporn",
"reclass:cosplay",
"reclass:doujinshi",
"reclass:gamecg",
"reclass:imageset",
"reclass:manga",
"reclass:misc",
"reclass:non-h",
"reclass:western",
)
}
+18
View File
@@ -0,0 +1,18 @@
package exh.eh.tags
interface TagList {
fun getTags1(): List<String>
fun getTags2(): List<String> = emptyList()
fun getTags3(): List<String> = emptyList()
fun getTags4(): List<String> = emptyList()
fun getTags() = listOf(
getTags1(),
getTags2(),
getTags3(),
getTags4(),
)
}
@@ -13,8 +13,8 @@ import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.withTimeoutOrNull import kotlinx.coroutines.withTimeoutOrNull
import kotlin.time.Duration.Companion.seconds import kotlin.time.Duration.Companion.seconds
class MangaDexLoginHelper(val authServiceLazy: Lazy<MangaDexAuthService>, val preferences: PreferencesHelper, val mdList: MdList) { class MangaDexLoginHelper(authServiceLazy: Lazy<MangaDexAuthService>, val preferences: PreferencesHelper, val mdList: MdList) {
val authService by authServiceLazy private val authService by authServiceLazy
suspend fun isAuthenticated(): Boolean { suspend fun isAuthenticated(): Boolean {
return runCatching { authService.checkToken().isAuthenticated } return runCatching { authService.checkToken().isAuthenticated }
.getOrElse { e -> .getOrElse { e ->
@@ -7,12 +7,17 @@ import okhttp3.Authenticator
import okhttp3.Request import okhttp3.Request
import okhttp3.Response import okhttp3.Response
import okhttp3.Route import okhttp3.Route
import java.io.IOException
class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authenticator { class TokenAuthenticator(private val loginHelper: MangaDexLoginHelper) : Authenticator {
override fun authenticate(route: Route?, response: Response): Request? { override fun authenticate(route: Route?, response: Response): Request? {
xLogI("Detected Auth error ${response.code} on ${response.request.url}") xLogI("Detected Auth error ${response.code} on ${response.request.url}")
val token = refreshToken(loginHelper) val token = try {
refreshToken(loginHelper)
} catch (e: Exception) {
throw IOException(e)
}
return if (token != null) { return if (token != null) {
response.request.newBuilder().header("Authorization", token).build() response.request.newBuilder().header("Authorization", token).build()
} else { } else {
@@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.queries.getAllMergedMangaQuery import eu.kanade.tachiyomi.data.database.queries.getAllMergedMangaQuery
import eu.kanade.tachiyomi.data.database.queries.getMergedChaptersQuery import eu.kanade.tachiyomi.data.database.queries.getMergedChaptersQuery
import eu.kanade.tachiyomi.data.database.queries.getMergedMangaForDownloadingQuery
import eu.kanade.tachiyomi.data.database.queries.getMergedMangaFromUrlQuery import eu.kanade.tachiyomi.data.database.queries.getMergedMangaFromUrlQuery
import eu.kanade.tachiyomi.data.database.queries.getMergedMangaQuery import eu.kanade.tachiyomi.data.database.queries.getMergedMangaQuery
import eu.kanade.tachiyomi.data.database.tables.ChapterTable import eu.kanade.tachiyomi.data.database.tables.ChapterTable
@@ -61,6 +62,16 @@ interface MergedQueries : DbProvider {
) )
.prepare() .prepare()
fun getMergedMangasForDownloading(mergedMangaId: Long) = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getMergedMangaForDownloadingQuery())
.args(mergedMangaId)
.build(),
)
.prepare()
fun getMergedMangas(mergedMangaUrl: String) = db.get() fun getMergedMangas(mergedMangaUrl: String) = db.get()
.listOfObjects(Manga::class.java) .listOfObjects(Manga::class.java)
.withQuery( .withQuery(
@@ -51,16 +51,14 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
val cover = thumbnailUrl val cover = thumbnailUrl
// No title bug? // No title bug?
val title = if (Injekt.get<PreferencesHelper>().useJapaneseTitle().get()) { val title = altTitle
altTitle ?: title ?.takeIf { Injekt.get<PreferencesHelper>().useJapaneseTitle().get() }
} else { ?: title
title
}
// Set artist (if we can find one) // Set artist (if we can find one)
val artist = tags.ofNamespace(EH_ARTIST_NAMESPACE).let { tags -> val artist = tags.ofNamespace(EH_ARTIST_NAMESPACE)
if (tags.isNotEmpty()) tags.joinToString(transform = { it.name }) else null .ifEmpty { null }
} ?.joinToString { it.name }
// Copy tags -> genres // Copy tags -> genres
val genres = tagsToGenreList() val genres = tagsToGenreList()
@@ -92,25 +90,25 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
return with(context) { return with(context) {
listOfNotNull( listOfNotNull(
gId?.let { getString(R.string.id) to it }, getItem(gId) { getString(R.string.id) },
gToken?.let { getString(R.string.token) to it }, getItem(gToken) { getString(R.string.token) },
exh?.let { getString(R.string.is_exhentai_gallery) to it.toString() }, getItem(exh) { getString(R.string.is_exhentai_gallery) },
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it }, getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
title?.let { getString(R.string.title) to it }, getItem(title) { getString(R.string.title) },
altTitle?.let { getString(R.string.alt_title) to it }, getItem(altTitle) { getString(R.string.alt_title) },
genre?.let { getString(R.string.genre) to it }, getItem(genre) { getString(R.string.genre) },
datePosted?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }, getItem(datePosted, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.date_posted) },
parent?.let { getString(R.string.parent) to it }, getItem(parent) { getString(R.string.parent) },
visible?.let { getString(R.string.visible) to it }, getItem(visible) { getString(R.string.visible) },
language?.let { getString(R.string.language) to it }, getItem(language) { getString(R.string.language) },
translated?.let { getString(R.string.translated) to it.toString() }, getItem(translated) { getString(R.string.translated) },
size?.let { getString(R.string.gallery_size) to MetadataUtil.humanReadableByteCount(it, true) }, getItem(size, { MetadataUtil.humanReadableByteCount(it, true) }) { getString(R.string.gallery_size) },
length?.let { getString(R.string.page_count) to it.toString() }, getItem(length) { getString(R.string.page_count) },
favorites?.let { getString(R.string.total_favorites) to it.toString() }, getItem(favorites) { getString(R.string.total_favorites) },
ratingCount?.let { getString(R.string.total_ratings) to it.toString() }, getItem(ratingCount) { getString(R.string.total_ratings) },
averageRating?.let { getString(R.string.average_rating) to it.toString() }, getItem(averageRating) { getString(R.string.average_rating) },
aged.let { getString(R.string.aged) to it.toString() }, getItem(aged) { getString(R.string.aged) },
lastUpdateCheck.let { getString(R.string.last_update_check) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }, getItem(lastUpdateCheck, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.last_update_check) },
) )
} }
} }
@@ -128,6 +126,7 @@ class EHentaiSearchMetadata : RaisedSearchMetadata() {
const val EH_LANGUAGE_NAMESPACE = "language" const val EH_LANGUAGE_NAMESPACE = "language"
const val EH_META_NAMESPACE = "meta" const val EH_META_NAMESPACE = "meta"
const val EH_UPLOADER_NAMESPACE = "uploader" const val EH_UPLOADER_NAMESPACE = "uploader"
const val EH_VISIBILITY_NAMESPACE = "visibility"
private fun splitGalleryUrl(url: String) = private fun splitGalleryUrl(url: String) =
url.let { url.let {
@@ -41,10 +41,9 @@ class EightMusesSearchMetadata : RaisedSearchMetadata() {
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
return with(context) { return with(context) {
listOfNotNull( listOfNotNull(
title?.let { getString(R.string.title) to it }, getItem(title) { getString(R.string.title) },
path.nullIfEmpty()?.joinToString("/", prefix = "/") getItem(path.nullIfEmpty(), { it.joinToString("/", prefix = "/") }) { getString(R.string.path) },
?.let { getString(R.string.path) to it }, getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it },
) )
} }
} }
@@ -48,11 +48,11 @@ class HBrowseSearchMetadata : RaisedSearchMetadata() {
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
return with(context) { return with(context) {
listOfNotNull( listOfNotNull(
hbId?.let { getString(R.string.id) to it.toString() }, getItem(hbId) { getString(R.string.id) },
hbUrl?.let { getString(R.string.url) to it }, getItem(hbUrl) { getString(R.string.url) },
thumbnail?.let { getString(R.string.thumbnail_url) to it }, getItem(thumbnail) { getString(R.string.thumbnail_url) },
title?.let { getString(R.string.title) to it }, getItem(title) { getString(R.string.title) },
length?.let { getString(R.string.page_count) to it.toString() }, getItem(length) { getString(R.string.page_count) },
) )
} }
} }
@@ -59,13 +59,13 @@ class HitomiSearchMetadata : RaisedSearchMetadata() {
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
return with(context) { return with(context) {
listOfNotNull( listOfNotNull(
hlId?.let { getString(R.string.id) to it }, getItem(hlId) { getString(R.string.id) },
title?.let { getString(R.string.title) to it }, getItem(title) { getString(R.string.title) },
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it }, getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
artists.nullIfEmpty()?.joinToString()?.let { getString(R.string.artist) to it }, getItem(artists.nullIfEmpty(), { it.joinToString() }) { getString(R.string.artist) },
genre?.let { getString(R.string.genre) to it }, getItem(genre) { getString(R.string.genre) },
language?.let { getString(R.string.language) to it }, getItem(language) { getString(R.string.language) },
uploadDate?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }, getItem(uploadDate, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.date_posted) },
) )
} }
} }
@@ -77,24 +77,24 @@ class MangaDexSearchMetadata : RaisedSearchMetadata() {
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
return with(context) { return with(context) {
listOfNotNull( listOfNotNull(
mdUuid?.let { getString(R.string.id) to it }, getItem(mdUuid) { getString(R.string.id) },
// mdUrl?.let { getString(R.string.url) to it }, // getItem(mdUrl) { getString(R.string.url) },
cover?.let { getString(R.string.thumbnail_url) to it }, getItem(cover) { getString(R.string.thumbnail_url) },
title?.let { getString(R.string.title) to it }, getItem(title) { getString(R.string.title) },
authors?.let { getString(R.string.author) to it.joinToString() }, getItem(authors, { it.joinToString() }) { getString(R.string.author) },
artists?.let { getString(R.string.artist) to it.joinToString() }, getItem(artists, { it.joinToString() }) { getString(R.string.artist) },
langFlag?.let { getString(R.string.language) to it }, getItem(langFlag) { getString(R.string.language) },
lastChapterNumber?.let { getString(R.string.last_chapter_number) to it.toString() }, getItem(lastChapterNumber) { getString(R.string.last_chapter_number) },
rating?.let { getString(R.string.average_rating) to it.toString() }, getItem(rating) { getString(R.string.average_rating) },
// users?.let { getString(R.string.total_ratings) to it }, // getItem(users) { getString(R.string.total_ratings) },
status?.let { getString(R.string.status) to it.toString() }, getItem(status) { getString(R.string.status) },
// missing_chapters?.let { getString(R.string.missing_chapters) to it }, // getItem(missing_chapters) { getString(R.string.missing_chapters) },
followStatus?.let { getString(R.string.follow_status) to it.toString() }, getItem(followStatus) { getString(R.string.follow_status) },
anilistId?.let { getString(R.string.anilist_id) to it }, getItem(anilistId) { getString(R.string.anilist_id) },
kitsuId?.let { getString(R.string.kitsu_id) to it }, getItem(kitsuId) { getString(R.string.kitsu_id) },
myAnimeListId?.let { getString(R.string.mal_id) to it }, getItem(myAnimeListId) { getString(R.string.mal_id) },
mangaUpdatesId?.let { getString(R.string.manga_updates_id) to it }, getItem(mangaUpdatesId) { getString(R.string.manga_updates_id) },
animePlanetId?.let { getString(R.string.anime_planet_id) to it }, getItem(animePlanetId) { getString(R.string.anime_planet_id) },
) )
} }
} }
@@ -88,17 +88,17 @@ class NHentaiSearchMetadata : RaisedSearchMetadata() {
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
return with(context) { return with(context) {
listOfNotNull( listOfNotNull(
nhId?.let { getString(R.string.id) to it.toString() }, getItem(nhId) { getString(R.string.id) },
uploadDate?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it * 1000)) }, getItem(uploadDate, { MetadataUtil.EX_DATE_FORMAT.format(Date(it * 1000)) }) { getString(R.string.date_posted) },
favoritesCount?.let { getString(R.string.total_favorites) to it.toString() }, getItem(favoritesCount) { getString(R.string.total_favorites) },
mediaId?.let { getString(R.string.media_id) to it }, getItem(mediaId) { getString(R.string.media_id) },
japaneseTitle?.let { getString(R.string.japanese_title) to it }, getItem(japaneseTitle) { getString(R.string.japanese_title) },
englishTitle?.let { getString(R.string.english_title) to it }, getItem(englishTitle) { getString(R.string.english_title) },
shortTitle?.let { getString(R.string.short_title) to it }, getItem(shortTitle) { getString(R.string.short_title) },
coverImageType?.let { getString(R.string.cover_image_file_type) to it }, getItem(coverImageType) { getString(R.string.cover_image_file_type) },
pageImageTypes.size.let { getString(R.string.page_count) to it.toString() }, getItem(pageImageTypes.size) { getString(R.string.page_count) },
thumbnailImageType?.let { getString(R.string.thumbnail_image_file_type) to it }, getItem(thumbnailImageType) { getString(R.string.thumbnail_image_file_type) },
scanlator?.let { getString(R.string.scanlator) to it }, getItem(scanlator) { getString(R.string.scanlator) },
) )
} }
} }
@@ -67,17 +67,16 @@ class PervEdenSearchMetadata : RaisedSearchMetadata() {
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
return with(context) { return with(context) {
listOfNotNull( listOfNotNull(
pvId?.let { getString(R.string.id) to it }, getItem(pvId) { getString(R.string.id) },
url?.let { getString(R.string.url) to it }, getItem(url) { getString(R.string.url) },
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it }, getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
title?.let { getString(R.string.title) to it }, getItem(title) { getString(R.string.title) },
altTitles.nullIfEmpty()?.joinToString() getItem(altTitles.nullIfEmpty(), { it.joinToString() }) { getString(R.string.alt_titles) },
?.let { getString(R.string.alt_titles) to it }, getItem(artist) { getString(R.string.artist) },
artist?.let { getString(R.string.artist) to it }, getItem(genre) { getString(R.string.genre) },
genre?.let { getString(R.string.genre) to it }, getItem(rating) { getString(R.string.average_rating) },
rating?.let { getString(R.string.average_rating) to it.toString() }, getItem(status) { getString(R.string.status) },
status?.let { getString(R.string.status) to it }, getItem(lang) { getString(R.string.language) },
lang?.let { getString(R.string.language) to it },
) )
} }
} }
@@ -56,16 +56,16 @@ class PururinSearchMetadata : RaisedSearchMetadata() {
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
return with(context) { return with(context) {
listOfNotNull( listOfNotNull(
prId?.let { getString(R.string.id) to it.toString() }, getItem(prId) { getString(R.string.id) },
title?.let { getString(R.string.title) to it }, getItem(title) { getString(R.string.title) },
altTitle?.let { getString(R.string.alt_title) to it }, getItem(altTitle) { getString(R.string.alt_title) },
thumbnailUrl?.let { getString(R.string.thumbnail_url) to it }, getItem(thumbnailUrl) { getString(R.string.thumbnail_url) },
uploaderDisp?.let { getString(R.string.uploader_capital) to it }, getItem(uploaderDisp) { getString(R.string.uploader_capital) },
uploader?.let { getString(R.string.uploader) to it }, getItem(uploader) { getString(R.string.uploader) },
pages?.let { getString(R.string.page_count) to it.toString() }, getItem(pages) { getString(R.string.page_count) },
fileSize?.let { getString(R.string.gallery_size) to it }, getItem(fileSize) { getString(R.string.gallery_size) },
ratingCount?.let { getString(R.string.total_ratings) to it.toString() }, getItem(ratingCount) { getString(R.string.total_ratings) },
averageRating?.let { getString(R.string.average_rating) to it.toString() }, getItem(averageRating) { getString(R.string.average_rating) },
) )
} }
} }
@@ -69,20 +69,20 @@ class TsuminoSearchMetadata : RaisedSearchMetadata() {
override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> { override fun getExtraInfoPairs(context: Context): List<Pair<String, String>> {
return with(context) { return with(context) {
listOfNotNull( listOfNotNull(
tmId?.let { getString(R.string.id) to it.toString() }, getItem(tmId) { getString(R.string.id) },
title?.let { getString(R.string.title) to it }, getItem(title) { getString(R.string.title) },
uploader?.let { getString(R.string.uploader) to it }, getItem(uploader) { getString(R.string.uploader) },
uploadDate?.let { getString(R.string.date_posted) to MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }, getItem(uploadDate, { MetadataUtil.EX_DATE_FORMAT.format(Date(it)) }) { getString(R.string.date_posted) },
length?.let { getString(R.string.page_count) to it.toString() }, getItem(length) { getString(R.string.page_count) },
ratingString?.let { getString(R.string.rating_string) to it }, getItem(ratingString) { getString(R.string.rating_string) },
averageRating?.let { getString(R.string.average_rating) to it.toString() }, getItem(averageRating) { getString(R.string.average_rating) },
userRatings?.let { getString(R.string.total_ratings) to it.toString() }, getItem(userRatings) { getString(R.string.total_ratings) },
favorites?.let { getString(R.string.total_favorites) to it.toString() }, getItem(favorites) { getString(R.string.total_favorites) },
category?.let { getString(R.string.genre) to it }, getItem(category) { getString(R.string.genre) },
collection?.let { getString(R.string.collection) to it }, getItem(collection) { getString(R.string.collection) },
group?.let { getString(R.string.group) to it }, getItem(group) { getString(R.string.group) },
parody.nullIfEmpty()?.joinToString()?.let { getString(R.string.parodies) to it }, getItem(parody.nullIfEmpty(), { it.joinToString() }) { getString(R.string.parodies) },
character.nullIfEmpty()?.joinToString()?.let { getString(R.string.characters) to it }, getItem(character.nullIfEmpty(), { it.joinToString() }) { getString(R.string.characters) },
) )
} }
} }
@@ -54,6 +54,15 @@ abstract class RaisedSearchMetadata {
if (newTitle != null) titles += RaisedTitle(newTitle, type) if (newTitle != null) titles += RaisedTitle(newTitle, type)
} }
fun <T : Any> getItem(
item: T?,
toString: (T) -> String = Any::toString,
block: (T) -> String,
): Pair<String, String>? {
item ?: return null
return block(item) to toString(item)
}
open fun copyTo(manga: SManga) { open fun copyTo(manga: SManga) {
val infoManga = createMangaInfo(manga.toMangaInfo()).toSManga() val infoManga = createMangaInfo(manga.toMangaInfo()).toSManga()
manga.copyFrom(infoManga) manga.copyFrom(infoManga)
@@ -20,8 +20,8 @@ fun OkHttpClient.Builder.injectPatches(sourceIdProducer: () -> Long): OkHttpClie
fun findAndApplyPatches(sourceId: Long): EHInterceptor { fun findAndApplyPatches(sourceId: Long): EHInterceptor {
// TODO make it so captcha doesnt auto open in manga eden while applying universal interceptors // TODO make it so captcha doesnt auto open in manga eden while applying universal interceptors
return if (Injekt.get<PreferencesHelper>().autoSolveCaptcha().get()) ((EH_INTERCEPTORS[sourceId].orEmpty()) + (EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR].orEmpty())).merge() return if (Injekt.get<PreferencesHelper>().autoSolveCaptcha().get()) (EH_INTERCEPTORS[sourceId].orEmpty() + EH_INTERCEPTORS[EH_UNIVERSAL_INTERCEPTOR].orEmpty()).merge()
else (EH_INTERCEPTORS[sourceId].orEmpty()).merge() else EH_INTERCEPTORS[sourceId].orEmpty().merge()
} }
fun List<EHInterceptor>.merge(): EHInterceptor { fun List<EHInterceptor>.merge(): EHInterceptor {
@@ -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>
@@ -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>
@@ -1,8 +1,24 @@
<?xml version="1.0" encoding="utf-8"?> <vector xmlns:android="http://schemas.android.com/apk/res/android"
<layer-list xmlns:android="http://schemas.android.com/apk/res/android"> android:width="108dp"
<item android:height="108dp"
android:width="36dp" android:viewportWidth="108"
android:height="36dp" android:viewportHeight="108">
android:drawable="@drawable/ic_tachi" <path
android:gravity="center" /> android:pathData="M52.2,35c7.9,-0.1 9.9,3.3 12.7,5.1c1.5,0.6 1.2,1.1 1.8,2c0.2,0.2 0.4,0.3 0.7,0.5c2.1,1.2 -0.7,2.7 -1.9,2.8c0.5,-0.3 0.9,-0.4 1.2,-0.8c0,0 0,0 -0.1,0c0,0 0,0 0,-0.1c-1,-0.3 -0.9,0.6 -2,0.4c0.2,0 0.9,-0.4 0.8,-0.9c-0.8,-0.5 -2.2,-1 -3.2,-1c0,0 0,0 0,0.1c2.5,1.7 1.8,3.9 3.2,4.4c-0.8,0 -0.4,0.1 0,0.5c-1,-0.2 -1.1,-1.3 -1.6,-2.3c-0.5,-0.9 -1.1,-2 -1.6,-2c-0.1,0.6 0.1,3.9 1,3.2c-0.7,0.8 0.3,0.5 0.8,0.3c-1.6,1.3 -2.4,0.9 -2.9,-0.9c-0.4,-0.9 -1,-2.3 -1.3,-3.2c-0.2,-0.5 -0.5,-0.9 -0.9,-1.2c-2.9,-2.1 -5,-2.3 -8,-1.8c-0.8,-1.5 -1.9,-2.8 -2.7,-4.2C49.6,35.4 50.7,35 52.2,35z"
</layer-list> android:fillColor="#000000"/>
<path
android:pathData="M41.2,41.4c5.6,10.8 11.5,7.3 20.1,11.4c7.5,4.6 5.7,14.6 -2.1,17.6c0,-1.9 0,-3.9 0,-5.8c3.2,-3.1 1.4,-7.7 -2.5,-8.6c-4.7,-1 -10,-2 -13.1,-4.3C40.2,49.3 39.4,44.9 41.2,41.4z"
android:fillColor="#000000"/>
<path
android:pathData="M48.9,65.7c0,1.7 0.1,3.7 -0.1,5.3c-6.4,-1.8 -11.1,-7.1 -8.5,-13.2c0,0.3 0,0.6 0,1C40.8,62.9 44.4,65.2 48.9,65.7z"
android:fillColor="#000000"/>
<path
android:pathData="M36.7,32l9.5,2.5L54,46.8l3.3,-5c2.4,1 2.5,5.9 3.8,6.6l-1.6,2.4c-7.1,-1.9 -13.2,-2.3 -15.7,-7.4C41.5,39.8 36.7,32 36.7,32z"
android:fillColor="#000000"/>
<path
android:pathData="M61,35.9c1.1,0.7 2.3,1.4 3.2,2.2c0.5,0.5 1,0.8 1.7,1.3c0.3,0.2 0.3,0.1 0.5,0.3l4.8,-7.8l-9.4,2.5L61,35.9z"
android:fillColor="#000000"/>
<path
android:pathData="M54.1,76l4.2,-3.4L58.4,58c0,0 -0.3,-0.8 -5.8,-1.7c-1.2,-0.2 -2,-0.5 -2,-0.5l-0.8,-0.2l0.1,17L54.1,76z"
android:fillColor="#000000"/>
</vector>
@@ -17,7 +17,7 @@
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:fitsSystemWindows="true" android:fitsSystemWindows="true"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toEndOf="@id/side_nav"
app:layout_constraintTop_toTopOf="parent"> app:layout_constraintTop_toTopOf="parent">
<com.google.android.material.appbar.MaterialToolbar <com.google.android.material.appbar.MaterialToolbar
@@ -35,7 +35,7 @@
android:background="?attr/colorTertiary" android:background="?attr/colorTertiary"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toEndOf="@id/side_nav"
app:layout_constraintTop_toBottomOf="@+id/appbar" app:layout_constraintTop_toBottomOf="@+id/appbar"
tools:visibility="visible"> tools:visibility="visible">
@@ -56,7 +56,7 @@
android:background="?attr/colorPrimary" android:background="?attr/colorPrimary"
android:visibility="gone" android:visibility="gone"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toEndOf="@id/side_nav"
app:layout_constraintTop_toBottomOf="@+id/downloaded_only" app:layout_constraintTop_toBottomOf="@+id/downloaded_only"
tools:visibility="visible"> tools:visibility="visible">
@@ -73,11 +73,10 @@
<com.google.android.material.navigationrail.NavigationRailView <com.google.android.material.navigationrail.NavigationRailView
android:id="@+id/side_nav" android:id="@+id/side_nav"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="0dp" android:layout_height="match_parent"
app:elevation="0dp" android:paddingTop="?attr/actionBarSize"
app:layout_constraintBottom_toBottomOf="parent" app:elevation="1dp"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/incognito_mode"
app:menu="@menu/main_nav" /> app:menu="@menu/main_nav" />
<com.google.android.material.tabs.TabLayout <com.google.android.material.tabs.TabLayout
@@ -31,57 +31,63 @@
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" /> app:layout_constraintTop_toTopOf="parent" />
<HorizontalScrollView <androidx.constraintlayout.widget.ConstraintLayout
android:id="@+id/migration_data_scrollView" android:id="@+id/migration_data_group"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="8dp"
android:layout_marginTop="8dp"
android:layout_marginEnd="8dp"
android:scrollbars="none"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/data_label"> app:layout_constraintTop_toBottomOf="@+id/data_label">
<LinearLayout <androidx.constraintlayout.helper.widget.Flow
android:layout_width="0dp"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:constraint_referenced_ids="mig_chapters,mig_categories,mig_tracking,mig_custom_cover,mig_extra"
app:flow_horizontalBias="0"
app:flow_horizontalGap="8dp"
app:flow_horizontalStyle="packed"
app:flow_verticalGap="2dp"
app:flow_wrapMode="chain"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<CheckBox
android:id="@+id/mig_chapters"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="wrap_content"
android:orientation="horizontal"> android:checked="true"
android:text="@string/chapters" />
<CheckBox <CheckBox
android:id="@+id/mig_chapters" android:id="@+id/mig_categories"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:checked="true"
android:checked="true" android:text="@string/categories" />
android:text="@string/chapters" />
<CheckBox <CheckBox
android:id="@+id/mig_categories" android:id="@+id/mig_tracking"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:checked="true"
android:checked="true" android:text="@string/track" />
android:text="@string/categories" />
<CheckBox <CheckBox
android:id="@+id/mig_tracking" android:id="@+id/mig_custom_cover"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:checked="true"
android:checked="true" android:text="@string/custom_cover" />
android:text="@string/track" />
<CheckBox <CheckBox
android:id="@+id/mig_extra" android:id="@+id/mig_extra"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginStart="4dp" android:checked="true"
android:checked="true" android:text="@string/log_extra" />
android:text="@string/log_extra" /> </androidx.constraintlayout.widget.ConstraintLayout>
</LinearLayout>
</HorizontalScrollView>
<View <View
android:id="@+id/migration_data_divider" android:id="@+id/migration_data_divider"
@@ -91,17 +97,18 @@
android:background="?android:attr/divider" android:background="?android:attr/divider"
app:layout_constraintEnd_toEndOf="parent" app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintStart_toStartOf="parent" app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@id/migration_data_scrollView"/> app:layout_constraintTop_toBottomOf="@id/migration_data_group"/>
<TextView <TextView
android:id="@+id/options_label" android:id="@+id/options_label"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp" android:layout_marginTop="16dp"
android:layout_marginStart="8dp"
android:text="@string/action_settings" android:text="@string/action_settings"
android:textAppearance="?attr/textAppearanceTitleMedium" android:textAppearance="?attr/textAppearanceTitleMedium"
android:textColor="?attr/colorPrimary" android:textColor="?attr/colorPrimary"
app:layout_constraintStart_toStartOf="@+id/migration_data_scrollView" app:layout_constraintStart_toStartOf="@+id/migration_data_group"
app:layout_constraintTop_toBottomOf="@+id/migration_data_divider" /> app:layout_constraintTop_toBottomOf="@+id/migration_data_divider" />
<RadioGroup <RadioGroup
+4 -2
View File
@@ -350,9 +350,10 @@
<TextView <TextView
android:id="@+id/left_page_text" android:id="@+id/left_page_text"
android:layout_width="32dp" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:minWidth="32dp"
android:textColor="?attr/colorOnSurface" android:textColor="?attr/colorOnSurface"
android:textSize="15sp" android:textSize="15sp"
tools:text="1" /> tools:text="1" />
@@ -371,9 +372,10 @@
<TextView <TextView
android:id="@+id/right_page_text" android:id="@+id/right_page_text"
android:layout_width="32dp" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:gravity="center" android:gravity="center"
android:minWidth="32dp"
android:textColor="?attr/colorOnSurface" android:textColor="?attr/colorOnSurface"
android:textSize="15sp" android:textSize="15sp"
tools:text="15" /> tools:text="15" />
@@ -11,7 +11,6 @@
android:id="@+id/upper_text" android:id="@+id/upper_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp"
android:textAppearance="?attr/textAppearanceTitleMedium" android:textAppearance="?attr/textAppearanceTitleMedium"
tools:text="Top" /> tools:text="Top" />
@@ -19,7 +18,7 @@
android:id="@+id/warning" android:id="@+id/warning"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginBottom="16dp" android:layout_marginTop="16dp"
android:gravity="center_vertical"> android:gravity="center_vertical">
<ImageView <ImageView
@@ -44,6 +43,7 @@
android:id="@+id/lower_text" android:id="@+id/lower_text"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="wrap_content" android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:textAppearance="?attr/textAppearanceTitleMedium" android:textAppearance="?attr/textAppearanceTitleMedium"
tools:text="Bottom" /> tools:text="Bottom" />

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