Compare commits

..

120 Commits

Author SHA1 Message Date
arkon b7779ba14f v1.8.5 2022-08-14 15:49:00 -04:00
arkon 9a291e6da4 Fix sources not loading
(cherry picked from commit 1f79444a53)

# Conflicts:
#	app/proguard-rules.pro
2022-08-14 15:41:41 -04:00
Jobobby04 e7423e3715 Add missing mangadex languages, remove language prettyPrint since its not used
(cherry picked from commit 240d821a58)
2022-08-13 15:49:13 -04:00
Jobobby04 0ed26dbc49 More fixes 2022-08-13 15:48:49 -04:00
Jobobby04 a08e4e616d Update EHTags list
(cherry picked from commit 05f2f79e0d)
2022-08-13 15:47:53 -04:00
Jobobby04 655126eaa2 Fixes 2022-08-13 15:46:27 -04:00
CVIUS af070a3f0a Detect identical mangas when long pressing to add to library (#7095)
* Detect identical mangas when long pressing to add to library

* Use extracted duplicate manga dialog to avoid duplication

* Partially revert previous commit

* Review changes

* Review changes part 2

(cherry picked from commit f1afeac0bc)
(cherry picked from commit 431c04e54f)
2022-08-13 15:46:05 -04:00
Jobobby04 2b9d564841 Minor improvements for delegated source id lists
(cherry picked from commit 1d593de654)
2022-08-13 15:40:55 -04:00
arkon d09de07a3f Cleanup 2022-08-13 15:40:28 -04:00
arkon 048587468d Don't allow swiping away app update install notification
Also show the new version number in the notifications.

(cherry picked from commit 4aa5c6107c)
2022-08-13 15:32:29 -04:00
CVIUS 5dcdd3454b Detect identical mangas when long pressing to add to library (#7095)
* Detect identical mangas when long pressing to add to library

* Use extracted duplicate manga dialog to avoid duplication

* Partially revert previous commit

* Review changes

* Review changes part 2

(cherry picked from commit f1afeac0bc)
(cherry picked from commit afd1c3b491)
2022-08-13 15:32:18 -04:00
nicki 5d5678861d Fix Links to Changelog/Readme/Commits for multisrc (#7252)
* Fix Links to Changelog/Readme/Commits for `multisrc`

working basic fix. Needs to be refactored into `createUrl()`

* Refactor back into `createUrl`

hopefully the logic is understandable
there's three cases:
 - when multisrc, if `path` isn't mentioned, then we're trying to open
   commmit history
 - when multisrc, if `path` is mentioned, then its either a changelog or
   a readme to a multisrc extension, the files are stored in the
   `overrides` subfolder
 - when not multisrc, we're looking at a single source where the links
   are constructed in the same way regardless of it being
   changelog/readme/commit history

(cherry picked from commit e7695aef78)
(cherry picked from commit 25e0075041)
2022-08-13 15:31:46 -04:00
arkon 85bd12e731 Actually compare chapter numbers as numbers when sorting (fixes #7247)
(cherry picked from commit da8669c826)
(cherry picked from commit 4b7b710b7c)

# Conflicts:
#	app/src/test/java/eu/kanade/tachiyomi/util/chapter/ChapterRecognitionTest.kt
2022-08-13 15:31:37 -04:00
arkon f322a7e660 Add auto split tall images setting
Also includes some fixes for bad merges in earlier commits

Co-authored-by: Saud-97 <Saud-97@users.noreply.github.com>
Co-authored-by: AntsyLich <AntsyLich@users.noreply.github.com>
(cherry picked from commit 6db2becd30)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerPageHolder.kt
#	app/src/main/java/eu/kanade/tachiyomi/util/system/ImageUtil.kt
2022-08-13 15:30:47 -04:00
Andreas 5f7b7c652c Log extension loading errors directly (#7716)
(cherry picked from commit 7892cc1519)
(cherry picked from commit 0b7d0f7f67)
2022-08-13 15:29:38 -04:00
Alessandro Jean 214cbed3f0 Add missing Authorization header on MAL refresh token request (#7686)
* Add missing Authorization header on MAL refresh token request.

* Make sure to also close the response when it have failed.

(cherry picked from commit 5315467908)
(cherry picked from commit af1ee662ed)
2022-08-13 15:29:28 -04:00
stevenyomi 71db4eebea Filter out empty genres before saving manga to database (#7655)
(cherry picked from commit 4efb736e56)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/database/models/Manga.kt
#	app/src/main/java/eu/kanade/tachiyomi/source/model/SManga.kt
(cherry picked from commit 702fdb054a)
2022-08-13 15:29:15 -04:00
Andreas 9a577e1c69 Remove deprecated LibrarySort (#7659)
* Remove deprecated LibrarySort

* Apply suggestions from code review

(cherry picked from commit 58acf0a8aa)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibrarySort.kt
(cherry picked from commit 4b87831bdd)

# Conflicts:
#	app/src/main/java/exh/EXHMigrations.kt
2022-08-13 15:29:03 -04:00
MatchaSoba 9a5ea9b507 Fix logic for searchWithGenre (#7559)
(cherry picked from commit b563e85c3b)
(cherry picked from commit b729b7f0aa)
2022-08-13 15:28:04 -04:00
arkon 474eea1c84 Avoid catastrophic failure when cover can't be created in local source (fixes #7577)
(cherry picked from commit d6977e5676)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/source/LocalSource.kt
(cherry picked from commit cfe78ff907)
2022-08-13 15:27:52 -04:00
arkon 43010e92ac Show better error when trying to open RARv5 file
(cherry picked from commit a843054388)
(cherry picked from commit 53a381ce28)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/loader/ChapterLoader.kt
2022-08-13 15:27:44 -04:00
nzoba 38b7240728 Add downloaded icon in TransitionView when chapter is downloaded (#7575)
* Add downloaded icon in TransitionView

* Change icon

(cherry picked from commit e8b7743826)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/viewer/pager/PagerViewer.kt
(cherry picked from commit ea37a5a7a1)
2022-08-13 15:26:46 -04:00
AntsyLich d52511d5ce Fix logic of app unlock (#7569)
(cherry picked from commit 8ea05e852e)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/App.kt
(cherry picked from commit 09e5bcaec1)
2022-08-13 15:26:36 -04:00
stevenyomi 06f0817bec Fix image MIME issues that cause download errors (#7562)
* Downloader: ignore non-image MIME to prevent .bin extensions

* ProgressResponseBody: allow null content type

Co-authored-by: anenasa <84259093+anenasa@users.noreply.github.com>

Co-authored-by: anenasa <84259093+anenasa@users.noreply.github.com>
(cherry picked from commit 3547d0142f)
(cherry picked from commit d734993349)
2022-08-13 15:26:28 -04:00
f1998f1998 2ee6d2d902 fix concurrent download (#7552)
* Fix concurrent download

* lower Concurrency

* artist Update app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>

Co-authored-by: Vetle Ledaal <vetle.ledaal@gmail.com>
(cherry picked from commit b635f02d93)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/download/Downloader.kt
(cherry picked from commit c69f53a8f4)
2022-08-13 15:26:11 -04:00
Jobobby04 8df8622dfa Handle new default user agent where SY uses it
(cherry picked from commit f3ffd3b930)
2022-08-13 15:25:34 -04:00
arkon 58ef239959 Make default user agent string configurable
(cherry picked from commit 4ee1d72b6f)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/source/online/HttpSource.kt
(cherry picked from commit bcf9398987)
2022-08-13 15:21:47 -04:00
arkon a126180ca3 Replace deprecated ACTION_MEDIA_SCANNER_SCAN_FILE intent
(cherry picked from commit 0b4f3f5532)
(cherry picked from commit c7e44aa22f)
2022-08-13 15:21:36 -04:00
arkon ae7a4744bd Configure SQLite
- Turn on `foreign_keys` to cascade on delete properly
- Turn on `journal_mode` and set `synchronous` to NORMAL which may help performance for larger libraries

Based on d977b89af1

Co-authored-by: ghostbear <andreas.everos@gmail.com>
(cherry picked from commit ac4f98e152)
2022-08-13 15:21:24 -04:00
arkon 63cd8f8c07 Use Material3 switches in XML layouts
(cherry picked from commit da7a64b40d)

# Conflicts:
#	app/src/main/res/layout/reader_general_settings.xml
#	app/src/main/res/layout/reader_pager_settings.xml
#	app/src/main/res/layout/reader_webtoon_settings.xml
(cherry picked from commit 72aba18dab)
2022-08-13 15:21:04 -04:00
arkon 2ecd2bce51 Bump dependencies + compile SDK to 33 + linting
(cherry picked from commit 3966a917ee)

# Conflicts:
#	app/build.gradle.kts
#	app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryController.kt
#	app/src/main/java/eu/kanade/tachiyomi/ui/reader/ReaderActivity.kt
2022-08-13 15:20:53 -04:00
arkon c7ecb58c61 Update .editorconfig
(cherry picked from commit be33a57d43)
2022-08-13 15:19:06 -04:00
arkon 422721bb64 Update chapter recognition and related tests
Includes 3e07100dc2

Co-authored-by: Saud-97 <Saud-97@users.noreply.github.com>
(cherry picked from commit 4a71022a60)

# Conflicts:
#	.github/workflows/build_pull_request.yml
#	.github/workflows/build_push.yml
#	app/src/test/java/eu/kanade/tachiyomi/CustomRobolectricGradleTestRunner.kt
2022-08-13 15:18:53 -04:00
arkon 92bc9d8801 Update AGP/Gradle
(cherry picked from commit 34ac39e7e5)

# Conflicts:
#	.github/workflows/build_pull_request.yml
#	.github/workflows/build_push.yml
2022-08-13 15:18:20 -04:00
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
190 changed files with 28758 additions and 26986 deletions
+3 -1
View File
@@ -2,4 +2,6 @@
indent_size=4 indent_size=4
insert_final_newline=true insert_final_newline=true
ij_kotlin_allow_trailing_comma=true ij_kotlin_allow_trailing_comma=true
ij_kotlin_allow_trailing_comma_on_call_site=true ij_kotlin_allow_trailing_comma_on_call_site=true
ij_kotlin_name_count_to_use_star_import = 2147483647
ij_kotlin_name_count_to_use_star_import_for_members = 2147483647
+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.5)
- 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.5"
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.5](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.5](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
@@ -1,5 +0,0 @@
org.gradle.daemon=false
org.gradle.jvmargs=-Xmx5120m
org.gradle.workers.max=2
kotlin.incremental=false
@@ -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,14 +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
run: |
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Write google-services.json - name: Write google-services.json
uses: DamianReeves/write-file-action@v1.0 uses: DamianReeves/write-file-action@v1.0
+2 -6
View File
@@ -28,14 +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
run: |
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build app - name: Build app
uses: gradle/gradle-command-action@v2 uses: gradle/gradle-command-action@v2
+23 -16
View File
@@ -1,3 +1,4 @@
import org.gradle.api.tasks.testing.logging.TestLogEvent
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
plugins { plugins {
@@ -18,6 +19,7 @@ if (gradle.startParameter.taskRequests.toString().contains("Standard")) {
shortcutHelper.setFilePath("./shortcuts.xml") shortcutHelper.setFilePath("./shortcuts.xml")
android { android {
namespace = "eu.kanade.tachiyomi"
compileSdk = AndroidConfig.compileSdk compileSdk = AndroidConfig.compileSdk
ndkVersion = AndroidConfig.ndk ndkVersion = AndroidConfig.ndk
@@ -25,8 +27,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 = 36
versionName = "1.8.2" versionName = "1.8.5"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"") buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"") buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@@ -202,6 +204,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)
@@ -224,13 +227,10 @@ dependencies {
// Tests // Tests
testImplementation(libs.junit) testImplementation(libs.junit)
testImplementation(libs.assertj.core)
testImplementation(libs.mockito.core)
testImplementation(libs.bundles.robolectric)
// For detecting memory leaks; see https://square.github.io/leakcanary/ // For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation(libs.leakcanary.android) // debugImplementation(libs.leakcanary.android)
implementation(libs.leakcanary.plumber)
// SY --> // SY -->
// Changelog // Changelog
@@ -257,23 +257,30 @@ dependencies {
} }
tasks { tasks {
withType<Test> {
useJUnitPlatform()
testLogging {
events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
}
}
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers) // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api(-markers)
withType<KotlinCompile> { withType<KotlinCompile> {
kotlinOptions.freeCompilerArgs += listOf( kotlinOptions.freeCompilerArgs += listOf(
"-Xopt-in=kotlin.Experimental", "-opt-in=kotlin.Experimental",
"-Xopt-in=kotlin.RequiresOptIn", "-opt-in=kotlin.RequiresOptIn",
"-Xopt-in=kotlin.ExperimentalStdlibApi", "-opt-in=kotlin.ExperimentalStdlibApi",
"-Xopt-in=kotlinx.coroutines.FlowPreview", "-opt-in=kotlinx.coroutines.FlowPreview",
"-Xopt-in=kotlinx.coroutines.ExperimentalCoroutinesApi", "-opt-in=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xopt-in=kotlinx.coroutines.InternalCoroutinesApi", "-opt-in=kotlinx.coroutines.InternalCoroutinesApi",
"-Xopt-in=kotlinx.serialization.ExperimentalSerializationApi", "-opt-in=kotlinx.serialization.ExperimentalSerializationApi",
"-Xopt-in=coil.annotation.ExperimentalCoilApi", "-opt-in=coil.annotation.ExperimentalCoilApi",
"-Xopt-in=kotlin.time.ExperimentalTime", "-opt-in=kotlin.time.ExperimentalTime",
) )
} }
// Duplicating Hebrew string assets due to some locale code issues on different devices // Duplicating Hebrew string assets due to some locale code issues on different devices
val copyHebrewStrings = task("copyHebrewStrings", type = Copy::class) { val copyHebrewStrings by registering(Copy::class) {
from("./src/main/res/values-he") from("./src/main/res/values-he")
into("./src/main/res/values-iw") into("./src/main/res/values-iw")
include("**/*") include("**/*")
+1 -2
View File
@@ -1,6 +1,5 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest xmlns:android="http://schemas.android.com/apk/res/android">
package="eu.kanade.tachiyomi">
<!-- Internet --> <!-- Internet -->
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
@@ -72,6 +72,7 @@ import uy.kohesive.injekt.injectLazy
import java.io.File import java.io.File
import java.security.Security import java.security.Security
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale import java.util.Locale
import kotlin.time.Duration.Companion.days import kotlin.time.Duration.Companion.days
@@ -182,6 +183,7 @@ open class App : Application(), DefaultLifecycleObserver, ImageLoaderFactory {
} }
override fun onStop(owner: LifecycleOwner) { override fun onStop(owner: LifecycleOwner) {
preferences.lastAppClosed().set(Date().time)
if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) { if (!AuthenticatorUtil.isAuthenticating && preferences.lockAppAfter().get() >= 0) {
SecureActivityDelegate.locked = true SecureActivityDelegate.locked = true
} }
@@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.AppUpdateJob import eu.kanade.tachiyomi.data.updater.AppUpdateJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting import eu.kanade.tachiyomi.ui.library.setting.SortDirectionSetting
import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting import eu.kanade.tachiyomi.ui.library.setting.SortModeSetting
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
@@ -106,10 +105,9 @@ object Migrations {
// Reset sorting preference if using removed sort by source // Reset sorting preference if using removed sort by source
val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0) val oldSortingMode = prefs.getInt(PreferenceKeys.librarySortingMode, 0)
@Suppress("DEPRECATION") if (oldSortingMode == 5 /* SOURCE */) {
if (oldSortingMode == LibrarySort.SOURCE) {
prefs.edit { prefs.edit {
putInt(PreferenceKeys.librarySortingMode, LibrarySort.ALPHA) putInt(PreferenceKeys.librarySortingMode, 0 /* ALPHABETICAL */)
} }
} }
} }
@@ -202,16 +200,15 @@ object Migrations {
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 0 -> SortModeSetting.ALPHABETICAL
LibrarySort.LAST_READ -> SortModeSetting.LAST_READ 1 -> SortModeSetting.LAST_READ
LibrarySort.LAST_CHECKED -> SortModeSetting.LAST_CHECKED 2 -> SortModeSetting.LAST_CHECKED
LibrarySort.UNREAD -> SortModeSetting.UNREAD 3 -> SortModeSetting.UNREAD
LibrarySort.TOTAL -> SortModeSetting.TOTAL_CHAPTERS 4 -> SortModeSetting.TOTAL_CHAPTERS
LibrarySort.LATEST_CHAPTER -> SortModeSetting.LATEST_CHAPTER 6 -> SortModeSetting.LATEST_CHAPTER
LibrarySort.CHAPTER_FETCH_DATE -> SortModeSetting.DATE_FETCHED 8 -> SortModeSetting.DATE_FETCHED
LibrarySort.DATE_ADDED -> SortModeSetting.DATE_ADDED 7 -> SortModeSetting.DATE_ADDED
else -> SortModeSetting.ALPHABETICAL else -> SortModeSetting.ALPHABETICAL
} }
@@ -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 -->
@@ -115,5 +115,14 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
override fun onConfigure(db: SupportSQLiteDatabase) { override fun onConfigure(db: SupportSQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true) db.setForeignKeyConstraintsEnabled(true)
setPragma(db, "foreign_keys = ON")
setPragma(db, "journal_mode = WAL")
setPragma(db, "synchronous = NORMAL")
}
private fun setPragma(db: SupportSQLiteDatabase, pragma: String) {
val cursor = db.query("PRAGMA $pragma")
cursor.moveToFirst()
cursor.close()
} }
} }
@@ -34,11 +34,6 @@ interface Manga : SManga {
return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC return chapter_flags and CHAPTER_SORT_MASK == CHAPTER_SORT_DESC
} }
fun getGenres(): List<String>? {
if (genre.isNullOrBlank()) return null
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
}
// SY --> // SY -->
fun getOriginalGenres(): List<String>? { fun getOriginalGenres(): List<String>? {
return originalGenre?.split(", ")?.map { it.trim() } return originalGenre?.split(", ")?.map { it.trim() }
@@ -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
@@ -274,11 +275,12 @@ class Downloader(
// Start downloader if needed // Start downloader if needed
if (autoStart && wasEmpty) { if (autoStart && wasEmpty) {
val queuedDownloads = queue.filter { it.source !is UnmeteredSource }.count() val queuedDownloads = queue.count { it.source !is UnmeteredSource }
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),
) )
} }
} }
@@ -344,8 +347,8 @@ class Downloader(
// Get all the URLs to the source images, fetch pages if necessary // Get all the URLs to the source images, fetch pages if necessary
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) } .flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
// Start downloading images, consider we can have downloaded images already // Start downloading images, consider we can have downloaded images already
// Concurrently do 5 pages at a time // Concurrently do 2 pages at a time
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir, dataSaver) }, 5) .flatMap({ page -> getOrDownloadImage(page, download, tmpDir, dataSaver).subscribeOn(Schedulers.io()) }, 2)
.onBackpressureLatest() .onBackpressureLatest()
// Do when page is downloaded. // Do when page is downloaded.
.doOnNext { notifier.onProgressChange(download) } .doOnNext { notifier.onProgressChange(download) }
@@ -355,6 +358,7 @@ class Downloader(
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) } .doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
// If the page list threw, it will resume here // If the page list threw, it will resume here
.onErrorReturn { error -> .onErrorReturn { error ->
logcat(LogPriority.ERROR, error)
download.status = Download.State.ERROR download.status = Download.State.ERROR
notifier.onError(error.message, download.chapter.name, download.manga.title) notifier.onError(error.message, download.chapter.name, download.manga.title)
download download
@@ -382,7 +386,7 @@ class Downloader(
tmpFile?.delete() tmpFile?.delete()
// Try to find the image file. // Try to find the image file.
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") } val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") || it.name!!.contains("${filename}__001") }
// If the image is already downloaded, do nothing. Otherwise download from network // If the image is already downloaded, do nothing. Otherwise download from network
val pageObservable = when { val pageObservable = when {
@@ -392,8 +396,12 @@ class Downloader(
} }
return pageObservable return pageObservable
// When the image is ready, set image path, progress (just in case) and status // When the page is ready, set page path, progress (just in case) and status
.doOnNext { file -> .doOnNext { file ->
val success = splitTallImageIfNeeded(page, tmpDir)
if (success.not()) {
notifier.onError(context.getString(R.string.download_notifier_split_failed), download.chapter.name, download.manga.title)
}
page.uri = file.uri page.uri = file.uri
page.progress = 100 page.progress = 100
download.downloadedImages++ download.downloadedImages++
@@ -404,6 +412,7 @@ class Downloader(
.onErrorReturn { .onErrorReturn {
page.progress = 0 page.progress = 0
page.status = Page.ERROR page.status = Page.ERROR
notifier.onError(it.message, download.chapter.name, download.manga.title)
page page
} }
} }
@@ -468,13 +477,33 @@ class Downloader(
*/ */
private fun getImageExtension(response: Response, file: UniFile): String { private fun getImageExtension(response: Response, file: UniFile): String {
// Read content type if available. // Read content type if available.
val mime = response.body?.contentType()?.let { ct -> "${ct.type}/${ct.subtype}" } val mime = response.body?.contentType()?.run { if (type == "image") "image/$subtype" else null }
// Else guess from the uri. // Else guess from the uri.
?: context.contentResolver.getType(file.uri) ?: context.contentResolver.getType(file.uri)
// 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)
}
private fun splitTallImageIfNeeded(page: Page, tmpDir: UniFile): Boolean {
if (!preferences.splitTallImages().get()) return true
val filename = String.format("%03d", page.number)
val imageFile = tmpDir.listFiles()?.find { it.name!!.startsWith(filename) }
?: throw Error(context.getString(R.string.download_notifier_split_page_not_found, page.number))
val imageFilePath = imageFile.filePath
?: throw Error(context.getString(R.string.download_notifier_split_page_path_not_found, page.number))
// check if the original page was previously splitted before then skip.
if (imageFile.name!!.contains("__")) return true
return try {
ImageUtil.splitTallImage(imageFile, imageFilePath)
} catch (e: Exception) {
logcat(LogPriority.ERROR, e)
false
}
} }
/** /**
@@ -492,16 +521,10 @@ class Downloader(
dirname: String, dirname: String,
) { ) {
// Ensure that the chapter folder has all the images. // Ensure that the chapter folder has all the images.
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") } val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") || (it.name!!.contains("__") && !it.name!!.contains("__001.jpg")) }
download.status = if (downloadedImages.size == download.pages!!.size) { download.status = if (downloadedImages.size == download.pages!!.size) {
Download.State.DOWNLOADED // Only rename the directory if it's downloaded.
} else {
Download.State.ERROR
}
// Only rename the directory if it's downloaded.
if (download.status == Download.State.DOWNLOADED) {
if (preferences.saveChaptersAsCBZ().get()) { if (preferences.saveChaptersAsCBZ().get()) {
archiveChapter(mangaDir, dirname, tmpDir) archiveChapter(mangaDir, dirname, tmpDir)
} else { } else {
@@ -510,6 +533,10 @@ class Downloader(
cache.addChapter(dirname, mangaDir, download.manga) cache.addChapter(dirname, mangaDir, download.manga)
DiskUtil.createNoMediaFile(tmpDir, context) DiskUtil.createNoMediaFile(tmpDir, context)
Download.State.DOWNLOADED
} else {
Download.State.ERROR
} }
} }
@@ -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
@@ -7,6 +7,7 @@ import android.content.Intent
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupRestoreService import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
@@ -193,7 +194,7 @@ class NotificationReceiver : BroadcastReceiver() {
val file = File(path) val file = File(path)
file.delete() file.delete()
DiskUtil.scanMedia(context, file) DiskUtil.scanMedia(context, file.toUri())
} }
/** /**
@@ -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.
@@ -65,6 +65,8 @@ object PreferenceKeys {
const val dohProvider = "doh_provider" const val dohProvider = "doh_provider"
const val defaultUserAgent = "default_user_agent"
const val defaultChapterFilterByRead = "default_chapter_filter_by_read" const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
const val defaultChapterFilterByDownloaded = "default_chapter_filter_by_downloaded" const val defaultChapterFilterByDownloaded = "default_chapter_filter_by_downloaded"
@@ -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
@@ -58,7 +59,7 @@ class PreferencesHelper(val context: Context) {
fun lockAppAfter() = flowPrefs.getInt("lock_app_after", 0) fun lockAppAfter() = flowPrefs.getInt("lock_app_after", 0)
fun lastAppUnlock() = flowPrefs.getLong("last_app_unlock", 0) fun lastAppClosed() = flowPrefs.getLong("last_app_closed", 0)
fun secureScreen() = flowPrefs.getEnum("secure_screen_v2", Values.SecureScreenMode.INCOGNITO) fun secureScreen() = flowPrefs.getEnum("secure_screen_v2", Values.SecureScreenMode.INCOGNITO)
@@ -212,11 +213,13 @@ 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 splitTallImages() = flowPrefs.getBoolean("split_tall_images", false)
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 +291,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)
@@ -307,6 +310,8 @@ class PreferencesHelper(val context: Context) {
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1) fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
fun defaultUserAgent() = flowPrefs.getString(Keys.defaultUserAgent, "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")
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "") fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL) fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
@@ -330,7 +335,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)
@@ -8,6 +8,7 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.Environment import android.os.Environment
import android.provider.MediaStore import android.provider.MediaStore
import androidx.core.net.toUri
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.cacheImageDir import eu.kanade.tachiyomi.util.storage.cacheImageDir
@@ -82,7 +83,7 @@ class ImageSaver(
} }
} }
DiskUtil.scanMedia(context, destFile) DiskUtil.scanMedia(context, destFile.toUri())
return destFile.getUriCompat(context) return destFile.getUriCompat(context)
} }
@@ -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
}
}
}
}
@@ -22,6 +22,7 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import okhttp3.FormBody import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.Request import okhttp3.Request
import okhttp3.RequestBody import okhttp3.RequestBody
@@ -256,13 +257,21 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.appendPath("my_list_status") .appendPath("my_list_status")
.build() .build()
fun refreshTokenRequest(refreshToken: String): Request { fun refreshTokenRequest(oauth: OAuth): Request {
val formBody: RequestBody = FormBody.Builder() val formBody: RequestBody = FormBody.Builder()
.add("client_id", clientId) .add("client_id", clientId)
.add("refresh_token", refreshToken) .add("refresh_token", oauth.refresh_token)
.add("grant_type", "refresh_token") .add("grant_type", "refresh_token")
.build() .build()
return POST("$baseOAuthUrl/token", body = formBody)
// Add the Authorization header manually as this particular
// request is called by the interceptor itself so it doesn't reach
// the part where the token is added automatically.
val headers = Headers.Builder()
.add("Authorization", "Bearer ${oauth.access_token}")
.build()
return POST("$baseOAuthUrl/token", body = formBody, headers = headers)
} }
private fun getPkceChallengeCode(): String { private fun getPkceChallengeCode(): String {
@@ -1,9 +1,10 @@
package eu.kanade.tachiyomi.data.track.myanimelist package eu.kanade.tachiyomi.data.track.myanimelist
import kotlinx.serialization.decodeFromString import eu.kanade.tachiyomi.network.parseAs
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import okhttp3.internal.closeQuietly
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
import java.io.IOException import java.io.IOException
@@ -24,11 +25,22 @@ class MyAnimeListInterceptor(private val myanimelist: MyAnimeList, private var t
} }
// Refresh access token if expired // Refresh access token if expired
if (oauth != null && oauth!!.isExpired()) { if (oauth != null && oauth!!.isExpired()) {
chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!.refresh_token)).use { val newOauth = runCatching {
if (it.isSuccessful) { val oauthResponse = chain.proceed(MyAnimeListApi.refreshTokenRequest(oauth!!))
setAuth(json.decodeFromString(it.body!!.string()))
if (oauthResponse.isSuccessful) {
oauthResponse.parseAs<OAuth>()
} else {
oauthResponse.closeQuietly()
null
} }
} }
if (newOauth.getOrNull() == null) {
throw IOException("Failed to refresh the access token")
}
setAuth(newOauth.getOrNull())
} }
if (oauth == null) { if (oauth == null) {
throw IOException("No authentication token") throw IOException("No authentication token")
@@ -48,6 +48,7 @@ class AppUpdateChecker {
when (result) { when (result) {
is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release) is AppUpdateResult.NewUpdate -> AppUpdateNotifier(context).promptUpdate(result.release)
is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate() is AppUpdateResult.NewUpdateFdroidInstallation -> AppUpdateNotifier(context).promptFdroidUpdate()
else -> {}
} }
result result
@@ -55,12 +56,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 +71,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
} }
} }
} }
@@ -29,6 +29,7 @@ internal class AppUpdateNotifier(private val context: Context) {
fun promptUpdate(release: GithubRelease) { fun promptUpdate(release: GithubRelease) {
val intent = Intent(context, AppUpdateService::class.java).apply { val intent = Intent(context, AppUpdateService::class.java).apply {
putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink()) putExtra(AppUpdateService.EXTRA_DOWNLOAD_URL, release.getDownloadLink())
putExtra(AppUpdateService.EXTRA_DOWNLOAD_TITLE, release.version)
} }
val updateIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT) val updateIntent = PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
@@ -116,6 +117,7 @@ internal class AppUpdateNotifier(private val context: Context) {
setOnlyAlertOnce(false) setOnlyAlertOnce(false)
setProgress(0, 0, false) setProgress(0, 0, false)
setContentIntent(installIntent) setContentIntent(installIntent)
setOngoing(true)
clearActions() clearActions()
addAction( addAction(
@@ -147,7 +147,7 @@ class AppUpdateService : Service() {
* @param context the application context. * @param context the application context.
* @param url the url to the new update. * @param url the url to the new update.
*/ */
fun start(context: Context, url: String, title: String = context.getString(R.string.app_name)) { fun start(context: Context, url: String, title: String? = context.getString(R.string.app_name)) {
if (!isRunning(context)) { if (!isRunning(context)) {
val intent = Intent(context, AppUpdateService::class.java).apply { val intent = Intent(context, AppUpdateService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_TITLE, title) putExtra(EXTRA_DOWNLOAD_TITLE, title)
@@ -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()
@@ -4,7 +4,5 @@ sealed class LoadResult {
class Success(val extension: Extension.Installed) : LoadResult() class Success(val extension: Extension.Installed) : LoadResult()
class Untrusted(val extension: Extension.Untrusted) : LoadResult() class Untrusted(val extension: Extension.Untrusted) : LoadResult()
class Error(val message: String? = null) : LoadResult() { object Error : LoadResult()
constructor(exception: Throwable) : this(exception.message)
}
} }
@@ -7,10 +7,12 @@ import android.content.IntentFilter
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.LoadResult import eu.kanade.tachiyomi.extension.model.LoadResult
import eu.kanade.tachiyomi.util.lang.launchNow import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.CoroutineStart import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.async import kotlinx.coroutines.async
import logcat.LogPriority
/** /**
* Broadcast receiver that listens for the system's packages installed, updated or removed, and only * Broadcast receiver that listens for the system's packages installed, updated or removed, and only
@@ -52,6 +54,7 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
when (val result = getExtensionFromIntent(context, intent)) { when (val result = getExtensionFromIntent(context, intent)) {
is LoadResult.Success -> listener.onExtensionInstalled(result.extension) is LoadResult.Success -> listener.onExtensionInstalled(result.extension)
is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension) is LoadResult.Untrusted -> listener.onExtensionUntrusted(result.extension)
else -> {}
} }
} }
} }
@@ -60,8 +63,8 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
when (val result = getExtensionFromIntent(context, intent)) { when (val result = getExtensionFromIntent(context, intent)) {
is LoadResult.Success -> listener.onExtensionUpdated(result.extension) is LoadResult.Success -> listener.onExtensionUpdated(result.extension)
// Not needed as a package can't be upgraded if the signature is different // Not needed as a package can't be upgraded if the signature is different
is LoadResult.Untrusted -> { is LoadResult.Untrusted -> {}
} else -> {}
} }
} }
} }
@@ -93,7 +96,10 @@ internal class ExtensionInstallReceiver(private val listener: Listener) :
*/ */
private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult { private suspend fun getExtensionFromIntent(context: Context, intent: Intent?): LoadResult {
val pkgName = getPackageNameFromIntent(intent) val pkgName = getPackageNameFromIntent(intent)
?: return LoadResult.Error("Package name not found") if (pkgName == null) {
logcat(LogPriority.WARN) { "Package name not found" }
return LoadResult.Error
}
return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await() return GlobalScope.async(Dispatchers.Default, CoroutineStart.DEFAULT) { ExtensionLoader.loadExtensionFromPkgName(context, pkgName) }.await()
} }
@@ -80,10 +80,12 @@ internal object ExtensionLoader {
context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS) context.packageManager.getPackageInfo(pkgName, PACKAGE_FLAGS)
} catch (error: PackageManager.NameNotFoundException) { } catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point // Unlikely, but the package may have been uninstalled at this point
return LoadResult.Error(error) logcat(LogPriority.ERROR, error)
return LoadResult.Error
} }
if (!isPackageAnExtension(pkgInfo)) { if (!isPackageAnExtension(pkgInfo)) {
return LoadResult.Error("Tried to load a package that wasn't a extension") logcat(LogPriority.WARN) { "Tried to load a package that wasn't a extension ($pkgName)" }
return LoadResult.Error
} }
return loadExtension(context, pkgName, pkgInfo) return loadExtension(context, pkgName, pkgInfo)
} }
@@ -102,7 +104,8 @@ internal object ExtensionLoader {
pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA) pkgManager.getApplicationInfo(pkgName, PackageManager.GET_META_DATA)
} catch (error: PackageManager.NameNotFoundException) { } catch (error: PackageManager.NameNotFoundException) {
// Unlikely, but the package may have been uninstalled at this point // Unlikely, but the package may have been uninstalled at this point
return LoadResult.Error(error) logcat(LogPriority.ERROR, error)
return LoadResult.Error
} }
val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ") val extName = pkgManager.getApplicationLabel(appInfo).toString().substringAfter("Tachiyomi: ")
@@ -112,7 +115,7 @@ internal object ExtensionLoader {
if (versionName.isNullOrEmpty()) { if (versionName.isNullOrEmpty()) {
val exception = Exception("Missing versionName for extension $extName") val exception = Exception("Missing versionName for extension $extName")
logcat(LogPriority.WARN, exception) logcat(LogPriority.WARN, exception)
return LoadResult.Error(exception) return LoadResult.Error
} }
// Validate lib version // Validate lib version
@@ -123,13 +126,14 @@ internal object ExtensionLoader {
"$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed", "$LIB_VERSION_MIN to $LIB_VERSION_MAX are allowed",
) )
logcat(LogPriority.WARN, exception) logcat(LogPriority.WARN, exception)
return LoadResult.Error(exception) return LoadResult.Error
} }
val signatureHash = getSignatureHash(pkgInfo) val signatureHash = getSignatureHash(pkgInfo)
if (signatureHash == null) { if (signatureHash == null) {
return LoadResult.Error("Package $pkgName isn't signed") logcat(LogPriority.WARN) { "Package $pkgName isn't signed" }
return LoadResult.Error
} else if (signatureHash !in trustedSignatures) { } else if (signatureHash !in trustedSignatures) {
val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash) val extension = Extension.Untrusted(extName, pkgName, versionName, versionCode, signatureHash)
logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" } logcat(LogPriority.WARN) { "Extension $pkgName isn't trusted" }
@@ -138,7 +142,8 @@ internal object ExtensionLoader {
val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1 val isNsfw = appInfo.metaData.getInt(METADATA_NSFW) == 1
if (!loadNsfwSource && isNsfw) { if (!loadNsfwSource && isNsfw) {
return LoadResult.Error("NSFW extension $pkgName not allowed") logcat(LogPriority.WARN) { "NSFW extension $pkgName not allowed" }
return LoadResult.Error
} }
val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1 val hasReadme = appInfo.metaData.getInt(METADATA_HAS_README, 0) == 1
@@ -165,7 +170,7 @@ internal object ExtensionLoader {
} }
} catch (e: Throwable) { } catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" } logcat(LogPriority.ERROR, e) { "Extension load error: $extName ($it)" }
return LoadResult.Error(e) return LoadResult.Error
} }
} }
@@ -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
@@ -60,4 +64,8 @@ open /* SY <-- */ class NetworkHelper(context: Context) {
.addInterceptor(CloudflareInterceptor(context)) .addInterceptor(CloudflareInterceptor(context))
.build() .build()
} }
val defaultUserAgent by lazy {
preferences.defaultUserAgent().get()
}
} }
@@ -15,8 +15,8 @@ class ProgressResponseBody(private val responseBody: ResponseBody, private val p
source(responseBody.source()).buffer() source(responseBody.source()).buffer()
} }
override fun contentType(): MediaType { override fun contentType(): MediaType? {
return responseBody.contentType()!! return responseBody.contentType()
} }
override fun contentLength(): Long { override fun contentLength(): Long {
@@ -9,7 +9,6 @@ import android.widget.Toast
import androidx.core.content.ContextCompat import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.DeviceUtil import eu.kanade.tachiyomi.util.system.DeviceUtil
import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.system.WebViewClientCompat
@@ -109,7 +108,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
webview.settings.userAgentString = request.header("User-Agent") webview.settings.userAgentString = request.header("User-Agent")
?: HttpSource.DEFAULT_USER_AGENT ?: networkHelper.defaultUserAgent
webview.webViewClient = object : WebViewClientCompat() { webview.webViewClient = object : WebViewClientCompat() {
override fun onPageFinished(view: WebView, url: String) { override fun onPageFinished(view: WebView, url: String) {
@@ -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)
} }
@@ -1,10 +1,14 @@
package eu.kanade.tachiyomi.network.interceptor package eu.kanade.tachiyomi.network.interceptor
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.network.NetworkHelper
import okhttp3.Interceptor import okhttp3.Interceptor
import okhttp3.Response import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class UserAgentInterceptor : Interceptor { class UserAgentInterceptor : Interceptor {
private val networkHelper: NetworkHelper by injectLazy()
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request() val originalRequest = chain.request()
@@ -12,7 +16,7 @@ class UserAgentInterceptor : Interceptor {
val newRequest = originalRequest val newRequest = originalRequest
.newBuilder() .newBuilder()
.removeHeader("User-Agent") .removeHeader("User-Agent")
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT) .addHeader("User-Agent", networkHelper.defaultUserAgent)
.build() .build()
chain.proceed(newRequest) chain.proceed(newRequest)
} else { } else {
@@ -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
@@ -28,6 +30,8 @@ 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 +39,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 +50,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 +178,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 +227,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 +243,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
} }
@@ -292,61 +288,129 @@ class LocalSource(private val context: Context) : CatalogueSource, UnmeteredSour
} }
private fun updateCover(chapter: SChapter, manga: SManga): File? { private fun updateCover(chapter: SChapter, manga: SManga): File? {
return when (val format = getFormat(chapter)) { return try {
is Format.Directory -> { when (val format = getFormat(chapter)) {
val entry = format.file.listFiles() is Format.Directory -> {
?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } val entry = format.file.listFiles()
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } ?.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { updateCover(context, manga, it.inputStream()) } entry?.let { updateCover(context, manga, it.inputStream()) }
} }
is Format.Zip -> { is Format.Zip -> {
ZipFile(format.file).use { zip -> ZipFile(format.file).use { zip ->
val entry = zip.entries().toList() val entry = zip.entries().toList()
.sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) } .sortedWith { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }
.find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.name) { zip.getInputStream(it) } }
entry?.let { updateCover(context, manga, zip.getInputStream(it)) } entry?.let { updateCover(context, manga, zip.getInputStream(it)) }
} }
} }
is Format.Rar -> { is Format.Rar -> {
Archive(format.file).use { archive -> Archive(format.file).use { archive ->
val entry = archive.fileHeaders val entry = archive.fileHeaders
.sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) } .sortedWith { f1, f2 -> f1.fileName.compareToCaseInsensitiveNaturalOrder(f2.fileName) }
.find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } } .find { !it.isDirectory && ImageUtil.isImage(it.fileName) { archive.getInputStream(it) } }
entry?.let { updateCover(context, manga, archive.getInputStream(it)) } entry?.let { updateCover(context, manga, archive.getInputStream(it)) }
} }
} }
is Format.Epub -> { is Format.Epub -> {
EpubFile(format.file).use { epub -> EpubFile(format.file).use { epub ->
val entry = epub.getImagesFromPages() val entry = epub.getImagesFromPages()
.firstOrNull() .firstOrNull()
?.let { epub.getEntry(it) } ?.let { epub.getEntry(it) }
entry?.let { updateCover(context, manga, epub.getInputStream(it)) } entry?.let { updateCover(context, manga, epub.getInputStream(it)) }
}
} }
} }
} catch (e: Throwable) {
logcat(LogPriority.ERROR, e) { "Error updating cover for ${manga.title}" }
null
} }
.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)
}
}
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")
@@ -253,7 +253,7 @@ open class SourceManager(private val context: Context) {
), ),
).associateBy { it.originalSourceQualifiedClassName } ).associateBy { it.originalSourceQualifiedClassName }
val currentDelegatedSources = ListenMutableMap(mutableMapOf<Long, DelegatedSource>(), ::handleSourceLibrary) val currentDelegatedSources: MutableMap<Long, DelegatedSource> = ListenMutableMap(mutableMapOf(), ::handleSourceLibrary)
data class DelegatedSource( data class DelegatedSource(
val sourceName: String, val sourceName: String,
@@ -264,19 +264,10 @@ open class SourceManager(private val context: Context) {
) )
} }
class ListenMutableMap<K, V>(private val internalMap: MutableMap<K, V>, val listener: () -> Unit) : MutableMap<K, V> { private class ListenMutableMap<K, V>(
override val size: Int private val internalMap: MutableMap<K, V>,
get() = internalMap.size private val listener: () -> Unit,
override fun containsKey(key: K): Boolean = internalMap.containsKey(key) ) : MutableMap<K, V> by internalMap {
override fun containsValue(value: V): Boolean = internalMap.containsValue(value)
override fun get(key: K): V? = internalMap[key]
override fun isEmpty(): Boolean = internalMap.isEmpty()
override val entries: MutableSet<MutableMap.MutableEntry<K, V>>
get() = internalMap.entries
override val keys: MutableSet<K>
get() = internalMap.keys
override val values: MutableCollection<V>
get() = internalMap.values
override fun clear() { override fun clear() {
val clearResult = internalMap.clear() val clearResult = internalMap.clear()
listener() listener()
@@ -28,6 +28,11 @@ interface SManga : Serializable {
var initialized: Boolean var initialized: Boolean
fun getGenres(): List<String>? {
if (genre.isNullOrBlank()) return null
return genre?.split(", ")?.map { it.trim() }?.filterNot { it.isBlank() }?.distinct()
}
// SY --> // SY -->
val originalTitle: String val originalTitle: String
get() = (this as? MangaImpl)?.ogTitle ?: title get() = (this as? MangaImpl)?.ogTitle ?: title
@@ -104,7 +109,7 @@ fun SManga.toMangaInfo(): MangaInfo {
artist = this.artist ?: "", artist = this.artist ?: "",
author = this.author ?: "", author = this.author ?: "",
description = this.description ?: "", description = this.description ?: "",
genres = this.genre?.split(", ") ?: emptyList(), genres = this.getGenres() ?: emptyList(),
status = this.status, status = this.status,
cover = this.thumbnail_url ?: "", cover = this.thumbnail_url ?: "",
) )
@@ -99,7 +99,7 @@ abstract class HttpSource : CatalogueSource {
* Headers builder for requests. Implementations can override this method for custom headers. * Headers builder for requests. Implementations can override this method for custom headers.
*/ */
protected open fun headersBuilder() = Headers.Builder().apply { protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", DEFAULT_USER_AGENT) add("User-Agent", network.defaultUserAgent)
} }
/** /**
@@ -417,8 +417,4 @@ abstract class HttpSource : CatalogueSource {
this.delegate = delegate this.delegate = delegate
} }
// EXH <-- // EXH <--
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"
}
} }
@@ -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)
}
} }
} }
} }
@@ -831,8 +835,8 @@ class EHentai(
) )
}, },
AutoCompleteTags( AutoCompleteTags(
EHTags.getNamespaces0Tags().map { "$it:" } + EHTags.getAllTags(), EHTags.getNamespaces().map { "$it:" } + EHTags.getAllTags(),
EHTags.getNamespaces0Tags().map { "$it:" }, EHTags.getNamespaces().map { "$it:" },
excludePrefix, excludePrefix,
), ),
if (preferences.exhWatchedListDefaultState().get()) { if (preferences.exhWatchedListDefaultState().get()) {
@@ -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)
@@ -121,16 +121,16 @@ class MangaDex(delegate: HttpSource, val context: Context) :
MangaPlusHandler(network.client) MangaPlusHandler(network.client)
} }
private val comikeyHandler by lazy { private val comikeyHandler by lazy {
ComikeyHandler(network.cloudflareClient) ComikeyHandler(network.cloudflareClient, network.defaultUserAgent)
} }
private val bilibiliHandler by lazy { private val bilibiliHandler by lazy {
BilibiliHandler(network.cloudflareClient) BilibiliHandler(network.cloudflareClient)
} }
private val azukHandler by lazy { private val azukHandler by lazy {
AzukiHandler(network.client) AzukiHandler(network.client, network.defaultUserAgent)
} }
private val mangaHotHandler by lazy { private val mangaHotHandler by lazy {
MangaHotHandler(network.client) MangaHotHandler(network.client, network.defaultUserAgent)
} }
private val pageHandler by lazy { private val pageHandler by lazy {
PageHandler( PageHandler(
@@ -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 {
@@ -98,7 +98,7 @@ abstract class DialogController : Controller {
/** /**
* Dismiss the dialog and pop this controller * Dismiss the dialog and pop this controller
*/ */
private fun dismissDialog() { protected fun dismissDialog() {
if (dismissed) { if (dismissed) {
return return
} }
@@ -59,16 +59,17 @@ abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*
val searchAutoComplete: SearchView.SearchAutoComplete = searchView.findViewById( val searchAutoComplete: SearchView.SearchAutoComplete = searchView.findViewById(
R.id.search_src_text, R.id.search_src_text,
) )
searchAutoComplete.addTextChangedListener(object : TextWatcher { searchAutoComplete.addTextChangedListener(
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {} object : TextWatcher {
override fun beforeTextChanged(s: CharSequence?, start: Int, count: Int, after: Int) {}
override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {} override fun onTextChanged(s: CharSequence?, start: Int, before: Int, count: Int) {}
override fun afterTextChanged(editable: Editable) { override fun afterTextChanged(editable: Editable) {
editable.getSpans(0, editable.length, CharacterStyle::class.java) editable.getSpans(0, editable.length, CharacterStyle::class.java)
.forEach { editable.removeSpan(it) } .forEach { editable.removeSpan(it) }
} }
}, },
) )
searchView.queryTextEvents() searchView.queryTextEvents()
@@ -134,12 +135,12 @@ abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*
searchItem.setOnActionExpandListener( searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener { object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
onSearchMenuItemActionExpand(item) onSearchMenuItemActionExpand(item)
return true return true
} }
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
val localSearchView = searchItem.actionView as SearchView val localSearchView = searchItem.actionView as SearchView
// if it is blank the flow event won't trigger so we would stay in a COLLAPSING state // if it is blank the flow event won't trigger so we would stay in a COLLAPSING state
@@ -119,6 +119,6 @@ class SecureActivityDelegateImpl : SecureActivityDelegate, DefaultLifecycleObser
// SY <-- // SY <--
return preferences.lockAppAfter().get() <= 0 || return preferences.lockAppAfter().get() <= 0 ||
Date().time >= preferences.lastAppUnlock().get() + 60 * 1000 * preferences.lockAppAfter().get() Date().time >= preferences.lastAppClosed().get() + 60 * 1000 * preferences.lockAppAfter().get()
} }
} }
@@ -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
} }
@@ -247,9 +247,13 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
} }
private fun createUrl(url: String, pkgName: String, pkgFactory: String?, path: String = ""): String { private fun createUrl(url: String, pkgName: String, pkgFactory: String?, path: String = ""): String {
return when { return if (!pkgFactory.isNullOrEmpty()) {
!pkgFactory.isNullOrEmpty() -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory$path" when (path.isEmpty()) {
else -> "$url/src/${pkgName.replace(".", "/")}$path" true -> "$url/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory"
else -> "$url/multisrc/overrides/$pkgFactory/" + (pkgName.split(".").lastOrNull() ?: "") + path
}
} else {
url + "/src/" + pkgName.replace(".", "/") + path
} }
} }
@@ -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)
} }
@@ -474,8 +474,8 @@ class MigrationListController(bundle: Bundle? = null) :
} }
private fun MenuItem.setIconTint(enabled: Boolean, color: Int) { private fun MenuItem.setIconTint(enabled: Boolean, color: Int) {
icon.mutate() icon?.mutate()
icon.setTint(color) icon?.setTint(color)
isEnabled = enabled isEnabled = enabled
} }
@@ -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)) {
@@ -41,6 +41,7 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting import eu.kanade.tachiyomi.ui.library.setting.DisplayModeSetting
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.AddDuplicateMangaDialog
import eu.kanade.tachiyomi.ui.manga.MangaController import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.more.MoreController import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.ui.webview.WebViewActivity
@@ -483,19 +484,20 @@ open class BrowseSourceController(bundle: Bundle) :
* @param genreName the name of the genre * @param genreName the name of the genre
*/ */
fun searchWithGenre(genreName: String) { fun searchWithGenre(genreName: String) {
presenter.sourceFilters = presenter.source.getFilterList() val defaultFilters = presenter.source.getFilterList()
var filterList: FilterList? = null var genreExists = false
filter@ for (sourceFilter in presenter.sourceFilters) { filter@ for (sourceFilter in defaultFilters) {
if (sourceFilter is Filter.Group<*>) { if (sourceFilter is Filter.Group<*>) {
for (filter in sourceFilter.state) { for (filter in sourceFilter.state) {
if (filter is Filter<*> && filter.name.equals(genreName, true)) { if (filter is Filter<*> && filter.name.equals(genreName, true)) {
when (filter) { when (filter) {
is Filter.TriState -> filter.state = 1 is Filter.TriState -> filter.state = 1
is Filter.CheckBox -> filter.state = true is Filter.CheckBox -> filter.state = true
else -> {}
} }
filterList = presenter.sourceFilters genreExists = true
break@filter break@filter
} }
} }
@@ -505,19 +507,20 @@ open class BrowseSourceController(bundle: Bundle) :
if (index != -1) { if (index != -1) {
sourceFilter.state = index sourceFilter.state = index
filterList = presenter.sourceFilters genreExists = true
break break
} }
} }
} }
if (filterList != null) { if (genreExists) {
presenter.sourceFilters = defaultFilters
filterSheet?.setFilters(presenter.filterItems) filterSheet?.setFilters(presenter.filterItems)
showProgressBar() showProgressBar()
adapter?.clear() adapter?.clear()
presenter.restartPager("", filterList) presenter.restartPager("", defaultFilters)
} else { } else {
searchWithQuery(genreName) searchWithQuery(genreName)
} }
@@ -740,6 +743,7 @@ open class BrowseSourceController(bundle: Bundle) :
override fun onItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
val activity = activity ?: return val activity = activity ?: return
val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return val manga = (adapter?.getItem(position) as? SourceItem?)?.manga ?: return
val duplicateManga = presenter.getDuplicateLibraryManga(manga)
if (manga.favorite) { if (manga.favorite) {
MaterialAlertDialogBuilder(activity) MaterialAlertDialogBuilder(activity)
@@ -755,43 +759,53 @@ open class BrowseSourceController(bundle: Bundle) :
} }
.show() .show()
} else { } else {
val categories = presenter.getCategories() if (duplicateManga != null) {
val defaultCategoryId = preferences.defaultCategory() AddDuplicateMangaDialog(this, duplicateManga) { addToLibrary(manga, position) }
val defaultCategory = categories.find { it.id == defaultCategoryId } .showDialog(router)
} else {
addToLibrary(manga, position)
}
}
}
when { private fun addToLibrary(newManga: Manga, position: Int) {
// Default category set val activity = activity ?: return
defaultCategory != null -> { val categories = presenter.getCategories()
presenter.moveMangaToCategory(manga, defaultCategory) val defaultCategoryId = preferences.defaultCategory()
val defaultCategory = categories.find { it.id == defaultCategoryId }
presenter.changeMangaFavorite(manga) when {
adapter?.notifyItemChanged(position) // Default category set
activity.toast(activity.getString(R.string.manga_added_library)) defaultCategory != null -> {
} presenter.moveMangaToCategory(newManga, defaultCategory)
// Automatic 'Default' or no categories presenter.changeMangaFavorite(newManga)
defaultCategoryId == 0 || categories.isEmpty() -> { adapter?.notifyItemChanged(position)
presenter.moveMangaToCategory(manga, null) activity.toast(activity.getString(R.string.manga_added_library))
}
presenter.changeMangaFavorite(manga) // Automatic 'Default' or no categories
adapter?.notifyItemChanged(position) defaultCategoryId == 0 || categories.isEmpty() -> {
activity.toast(activity.getString(R.string.manga_added_library)) presenter.moveMangaToCategory(newManga, null)
}
// Choose a category presenter.changeMangaFavorite(newManga)
else -> { adapter?.notifyItemChanged(position)
val ids = presenter.getMangaCategoryIds(manga) activity.toast(activity.getString(R.string.manga_added_library))
val preselected = categories.map { }
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(manga), categories, preselected) // Choose a category
.showDialog(router) else -> {
} val ids = presenter.getMangaCategoryIds(newManga)
val preselected = categories.map {
if (it.id in ids) {
QuadStateTextView.State.CHECKED.ordinal
} else {
QuadStateTextView.State.UNCHECKED.ordinal
}
}.toTypedArray()
ChangeMangaCategoriesDialog(this, listOf(newManga), categories, preselected)
.showDialog(router)
} }
} }
} }
@@ -435,6 +435,10 @@ open class BrowseSourcePresenter(
return db.getCategories().executeAsBlocking() return db.getCategories().executeAsBlocking()
} }
fun getDuplicateLibraryManga(manga: Manga): Manga? {
return db.getDuplicateLibraryManga(manga).executeAsBlocking()
}
/** /**
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id. * Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
* *
@@ -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)
@@ -28,8 +28,7 @@ class DownloadHeaderHolder(view: View, adapter: FlexibleAdapter<*>) : Expandable
override fun onItemReleased(position: Int) { override fun onItemReleased(position: Int) {
super.onItemReleased(position) super.onItemReleased(position)
binding.container.isDragged = false binding.container.isDragged = false
mAdapter as DownloadAdapter
mAdapter.expandAll() mAdapter.expandAll()
mAdapter.downloadItemListener.onItemReleased(position) (mAdapter as DownloadAdapter).downloadItemListener.onItemReleased(position)
} }
} }
@@ -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.
@@ -441,7 +442,7 @@ class LibraryController(
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search) createOptionsMenu(menu, inflater, R.menu.library, R.id.action_search)
// Mutate the filter icon because it needs to be tinted and the resource is shared. // Mutate the filter icon because it needs to be tinted and the resource is shared.
menu.findItem(R.id.action_filter).icon.mutate() menu.findItem(R.id.action_filter).icon?.mutate()
// SY --> // SY -->
menu.findItem(R.id.action_sync_favorites).isVisible = preferences.isHentaiEnabled().get() menu.findItem(R.id.action_sync_favorites).isVisible = preferences.isHentaiEnabled().get()
@@ -471,7 +472,7 @@ class LibraryController(
// Tint icon if there's a filter active // Tint icon if there's a filter active
if (settingsSheet.filters.hasActiveFilters()) { if (settingsSheet.filters.hasActiveFilters()) {
val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive) val filterColor = activity!!.getResourceColor(R.attr.colorFilterActive)
filterItem.icon.setTint(filterColor) filterItem.icon?.setTint(filterColor)
} }
} }
@@ -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 -> {
@@ -451,6 +451,7 @@ class LibrarySettingsSheet(
unreadBadge -> preferences.unreadBadge().set((item.checked)) unreadBadge -> preferences.unreadBadge().set((item.checked))
localBadge -> preferences.localBadge().set((item.checked)) localBadge -> preferences.localBadge().set((item.checked))
languageBadge -> preferences.languageBadge().set((item.checked)) languageBadge -> preferences.languageBadge().set((item.checked))
else -> {}
} }
adapter.notifyItemChanged(item) adapter.notifyItemChanged(item)
} }
@@ -473,6 +474,7 @@ class LibrarySettingsSheet(
item.checked = !item.checked item.checked = !item.checked
when (item) { when (item) {
startReadingButton -> preferences.startReadingButton().set((item.checked)) startReadingButton -> preferences.startReadingButton().set((item.checked))
else -> Unit
} }
adapter.notifyItemChanged(item) adapter.notifyItemChanged(item)
} }
@@ -498,6 +500,7 @@ class LibrarySettingsSheet(
when (item) { when (item) {
showTabs -> preferences.categoryTabs().set(item.checked) showTabs -> preferences.categoryTabs().set(item.checked)
showNumberOfItems -> preferences.categoryNumberOfItems().set(item.checked) showNumberOfItems -> preferences.categoryNumberOfItems().set(item.checked)
else -> {}
} }
adapter.notifyItemChanged(item) adapter.notifyItemChanged(item)
} }
@@ -1,23 +0,0 @@
package eu.kanade.tachiyomi.ui.library
@Deprecated("Deprecated in favor for SortModeSetting")
object LibrarySort {
const val ALPHA = 0
const val LAST_READ = 1
const val LAST_CHECKED = 2
const val UNREAD = 3
const val TOTAL = 4
const val LATEST_CHAPTER = 6
const val CHAPTER_FETCH_DATE = 10
const val DATE_ADDED = 8
// SY -->
const val DRAG_AND_DROP = 7
const val TAG_LIST = 9
// SY <--
@Deprecated("Removed in favor of searching by source")
const val SOURCE = 5
}
@@ -520,7 +520,7 @@ class MainActivity : BaseActivity() {
// Binding sometimes isn't actually instantiated yet somehow // Binding sometimes isn't actually instantiated yet somehow
nav?.setOnItemSelectedListener(null) nav?.setOnItemSelectedListener(null)
binding?.toolbar.setNavigationOnClickListener(null) binding?.toolbar?.setNavigationOnClickListener(null)
} }
override fun onBackPressed() { override fun onBackPressed() {
@@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.ui.manga
import android.app.Dialog
import android.os.Bundle
import com.bluelinelabs.conductor.Controller
import com.google.android.material.dialog.MaterialAlertDialogBuilder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import uy.kohesive.injekt.injectLazy
class AddDuplicateMangaDialog(bundle: Bundle? = null) : DialogController(bundle) {
private val sourceManager: SourceManager by injectLazy()
private lateinit var libraryManga: Manga
private lateinit var onAddToLibrary: () -> Unit
constructor(
target: Controller,
libraryManga: Manga,
onAddToLibrary: () -> Unit,
) : this() {
targetController = target
this.libraryManga = libraryManga
this.onAddToLibrary = onAddToLibrary
}
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val source = sourceManager.getOrStub(libraryManga.source)
return MaterialAlertDialogBuilder(activity!!)
.setMessage(activity?.getString(R.string.confirm_manga_add_duplicate, source.name))
.setPositiveButton(activity?.getString(R.string.action_add)) { _, _ ->
onAddToLibrary()
}
.setNegativeButton(android.R.string.cancel, null)
.setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
dismissDialog()
router.pushController(MangaController(libraryManga.id!!).withFadeTransaction())
}
.setCancelable(true)
.create()
}
}
@@ -673,18 +673,8 @@ class MangaController :
private fun showAddDuplicateDialog(newManga: Manga, libraryManga: Manga) { private fun showAddDuplicateDialog(newManga: Manga, libraryManga: Manga) {
activity?.let { activity?.let {
val source = sourceManager.getOrStub(libraryManga.source) AddDuplicateMangaDialog(this, libraryManga) { addToLibrary(newManga) }
MaterialAlertDialogBuilder(it).apply { .showDialog(router)
setMessage(activity?.getString(R.string.confirm_manga_add_duplicate, source.name))
setPositiveButton(activity?.getString(R.string.action_add)) { _, _ ->
addToLibrary(newManga)
}
setNegativeButton(activity?.getString(R.string.action_cancel)) { _, _ -> }
setNeutralButton(activity?.getString(R.string.action_show_manga)) { _, _ ->
router.pushController(MangaController(libraryManga).withFadeTransaction())
}
setCancelable(true)
}.create().show()
} }
} }
@@ -1118,7 +1108,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()
@@ -161,6 +161,7 @@ class ChaptersSettingsSheet(
downloaded -> presenter.setDownloadedFilter(newState) downloaded -> presenter.setDownloadedFilter(newState)
unread -> presenter.setUnreadFilter(newState) unread -> presenter.setUnreadFilter(newState)
bookmarked -> presenter.setBookmarkedFilter(newState) bookmarked -> presenter.setBookmarkedFilter(newState)
else -> {}
} }
initModels() initModels()
@@ -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,61 @@ 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,
VERSION_KEY to update.release.version,
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) val version = args.getString(VERSION_KEY)
AppUpdateService.start(context, url, version)
} }
} }
.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 VERSION_KEY = "NewUpdateDialogController.version"
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
}
} }
} }
} }
@@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.loader package eu.kanade.tachiyomi.ui.reader.loader
import android.content.Context import android.content.Context
import com.github.junrar.exception.UnsupportedRarV5Exception
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
@@ -116,7 +117,11 @@ class ChapterLoader(
when (format) { when (format) {
is LocalSource.Format.Directory -> DirectoryPageLoader(format.file) is LocalSource.Format.Directory -> DirectoryPageLoader(format.file)
is LocalSource.Format.Zip -> ZipPageLoader(format.file) is LocalSource.Format.Zip -> ZipPageLoader(format.file)
is LocalSource.Format.Rar -> RarPageLoader(format.file) is LocalSource.Format.Rar -> try {
RarPageLoader(format.file)
} catch (e: UnsupportedRarV5Exception) {
error(context.getString(R.string.loader_rar5_error))
}
is LocalSource.Format.Epub -> EpubPageLoader(format.file) is LocalSource.Format.Epub -> EpubPageLoader(format.file)
} }
} }
@@ -10,9 +10,9 @@ data class ReaderChapter(val chapter: Chapter) {
var state: State = var state: State =
State.Wait State.Wait
set(value) { set(value) {
field = value field = value
stateRelay.call(value) stateRelay.call(value)
} }
private val stateRelay by lazy { BehaviorRelay.create(state) } private val stateRelay by lazy { BehaviorRelay.create(state) }
@@ -34,27 +34,28 @@ class ReaderSettingsSheet(
behavior.halfExpandedRatio = 0.25f behavior.halfExpandedRatio = 0.25f
val filterTabIndex = getTabViews().indexOf(colorFilterSettings) val filterTabIndex = getTabViews().indexOf(colorFilterSettings)
binding.tabs.addOnTabSelectedListener(object : SimpleTabSelectedListener() { binding.tabs.addOnTabSelectedListener(
override fun onTabSelected(tab: TabLayout.Tab?) { object : SimpleTabSelectedListener() {
val isFilterTab = tab?.position == filterTabIndex override fun onTabSelected(tab: TabLayout.Tab?) {
val isFilterTab = tab?.position == filterTabIndex
// Remove dimmed backdrop so color filter changes can be previewed // Remove dimmed backdrop so color filter changes can be previewed
backgroundDimAnimator.run { backgroundDimAnimator.run {
if (isFilterTab) { if (isFilterTab) {
if (animatedFraction < 1f) { if (animatedFraction < 1f) {
start() start()
}
} else if (animatedFraction > 0f) {
reverse()
} }
} else if (animatedFraction > 0f) { }
reverse()
// Hide toolbars
if (activity.menuVisible != !isFilterTab) {
activity.setMenuVisibility(!isFilterTab)
} }
} }
},
// Hide toolbars
if (activity.menuVisible != !isFilterTab) {
activity.setMenuVisibility(!isFilterTab)
}
}
},
) )
if (showColorFilterSettings) { if (showColorFilterSettings) {
@@ -249,6 +249,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F)) ZoomStartPosition.LEFT -> setScaleAndCenter(scale, PointF(0F, 0F))
ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F)) ZoomStartPosition.RIGHT -> setScaleAndCenter(scale, PointF(sWidth.toFloat(), 0F))
ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F }) ZoomStartPosition.CENTER -> setScaleAndCenter(scale, center.also { it?.y = 0F })
else -> {}
} }
} }
@@ -310,7 +311,7 @@ open class ReaderPageImageView @JvmOverloads constructor(
return true return true
} }
override fun onSingleTapConfirmed(e: MotionEvent?): Boolean { override fun onSingleTapConfirmed(e: MotionEvent): Boolean {
this@ReaderPageImageView.onViewClicked() this@ReaderPageImageView.onViewClicked()
return super.onSingleTapConfirmed(e) return super.onSingleTapConfirmed(e)
} }
@@ -1,15 +1,24 @@
package eu.kanade.tachiyomi.ui.reader.viewer package eu.kanade.tachiyomi.ui.reader.viewer
import android.content.Context import android.content.Context
import android.text.SpannableStringBuilder
import android.text.style.ImageSpan
import android.util.AttributeSet import android.util.AttributeSet
import android.view.LayoutInflater import android.view.LayoutInflater
import android.widget.LinearLayout import android.widget.LinearLayout
import androidx.core.content.ContextCompat
import androidx.core.text.bold import androidx.core.text.bold
import androidx.core.text.buildSpannedString import androidx.core.text.buildSpannedString
import androidx.core.text.inSpans
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.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding import eu.kanade.tachiyomi.databinding.ReaderTransitionViewBinding
import eu.kanade.tachiyomi.ui.reader.loader.DownloadPageLoader
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.util.system.dpToPx
import kotlin.math.roundToInt
class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) : class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: AttributeSet? = null) :
LinearLayout(context, attrs) { LinearLayout(context, attrs) {
@@ -21,32 +30,42 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT) layoutParams = LayoutParams(LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT)
} }
fun bind(transition: ChapterTransition) { fun bind(transition: ChapterTransition, downloadManager: DownloadManager, manga: Manga?) {
manga ?: return
when (transition) { when (transition) {
is ChapterTransition.Prev -> bindPrevChapterTransition(transition) is ChapterTransition.Prev -> bindPrevChapterTransition(transition, downloadManager, manga)
is ChapterTransition.Next -> bindNextChapterTransition(transition) is ChapterTransition.Next -> bindNextChapterTransition(transition, downloadManager, manga)
} }
missingChapterWarning(transition) missingChapterWarning(transition)
} }
/** /**
* Binds a previous chapter transition on this view and subscribes to the page load status. * Binds a previous chapter transition on this view and subscribes to the page load status.
*/ */
private fun bindPrevChapterTransition(transition: ChapterTransition) { private fun bindPrevChapterTransition(
val prevChapter = transition.to transition: ChapterTransition,
downloadManager: DownloadManager,
manga: Manga,
) {
val prevChapter = transition.to?.chapter
val hasPrevChapter = prevChapter != null binding.lowerText.isVisible = prevChapter != null
binding.lowerText.isVisible = hasPrevChapter if (prevChapter != null) {
if (hasPrevChapter) {
binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
val isPrevDownloaded = downloadManager.isChapterDownloaded(
prevChapter,
manga,
)
val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader
binding.upperText.text = buildSpannedString { binding.upperText.text = buildSpannedString {
bold { append(context.getString(R.string.transition_previous)) } bold { append(context.getString(R.string.transition_previous)) }
append("\n${prevChapter!!.chapter.name}") append("\n${prevChapter.name}")
if (isPrevDownloaded) addDLImageSpan()
} }
binding.lowerText.text = buildSpannedString { binding.lowerText.text = buildSpannedString {
bold { append(context.getString(R.string.transition_current)) } bold { append(context.getString(R.string.transition_current)) }
append("\n${transition.from.chapter.name}") append("\n${transition.from.chapter.name}")
if (isCurrentDownloaded) addDLImageSpan()
} }
} else { } else {
binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER
@@ -57,20 +76,30 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
/** /**
* Binds a next chapter transition on this view and subscribes to the load status. * Binds a next chapter transition on this view and subscribes to the load status.
*/ */
private fun bindNextChapterTransition(transition: ChapterTransition) { private fun bindNextChapterTransition(
val nextChapter = transition.to transition: ChapterTransition,
downloadManager: DownloadManager,
manga: Manga,
) {
val nextChapter = transition.to?.chapter
val hasNextChapter = nextChapter != null binding.lowerText.isVisible = nextChapter != null
binding.lowerText.isVisible = hasNextChapter if (nextChapter != null) {
if (hasNextChapter) {
binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START binding.upperText.textAlignment = TEXT_ALIGNMENT_TEXT_START
val isCurrentDownloaded = transition.from.pageLoader is DownloadPageLoader
val isNextDownloaded = downloadManager.isChapterDownloaded(
nextChapter,
manga,
)
binding.upperText.text = buildSpannedString { binding.upperText.text = buildSpannedString {
bold { append(context.getString(R.string.transition_finished)) } bold { append(context.getString(R.string.transition_finished)) }
append("\n${transition.from.chapter.name}") append("\n${transition.from.chapter.name}")
if (isCurrentDownloaded) addDLImageSpan()
} }
binding.lowerText.text = buildSpannedString { binding.lowerText.text = buildSpannedString {
bold { append(context.getString(R.string.transition_next)) } bold { append(context.getString(R.string.transition_next)) }
append("\n${nextChapter!!.chapter.name}") append("\n${nextChapter.name}")
if (isNextDownloaded) addDLImageSpan()
} }
} else { } else {
binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER binding.upperText.textAlignment = TEXT_ALIGNMENT_CENTER
@@ -78,6 +107,17 @@ class ReaderTransitionView @JvmOverloads constructor(context: Context, attrs: At
} }
} }
private fun SpannableStringBuilder.addDLImageSpan() {
val icon = ContextCompat.getDrawable(context, R.drawable.ic_offline_pin_24dp)?.mutate()
?.apply {
val size = binding.lowerText.textSize + 4.dpToPx
setTint(binding.lowerText.currentTextColor)
setBounds(0, 0, size.roundToInt(), size.roundToInt())
} ?: return
append(" ")
inSpans(ImageSpan(icon)) { append("image") }
}
private fun missingChapterWarning(transition: ChapterTransition) { private fun missingChapterWarning(transition: ChapterTransition) {
if (transition.to == null) { if (transition.to == null) {
binding.warning.isVisible = false binding.warning.isVisible = false
@@ -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,8 @@ 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.BufferedInputStream
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
@@ -332,7 +333,7 @@ class PagerPageHolder(
.subscribe({}, {}) .subscribe({}, {})
} }
private fun process(page: ReaderPage, imageStream: InputStream): InputStream { private fun process(page: ReaderPage, imageStream: BufferedInputStream): InputStream {
if (!viewer.config.dualPageSplit) { if (!viewer.config.dualPageSplit) {
return imageStream return imageStream
} }
@@ -341,7 +342,7 @@ class PagerPageHolder(
return splitInHalf(imageStream) return splitInHalf(imageStream)
} }
val isDoublePage = ImageUtil.isDoublePage(imageStream) val isDoublePage = ImageUtil.isWideImage(imageStream)
if (!isDoublePage) { if (!isDoublePage) {
return imageStream return imageStream
} }
@@ -366,13 +367,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 +394,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) }
@@ -61,7 +61,7 @@ class PagerTransitionHolder(
addView(transitionView) addView(transitionView)
addView(pagesContainer) addView(pagesContainer)
transitionView.bind(transition) transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga)
transition.to?.let { observeStatus(it) } transition.to?.let { observeStatus(it) }
} }
@@ -11,6 +11,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.viewpager.widget.ViewPager import androidx.viewpager.widget.ViewPager
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
import eu.kanade.tachiyomi.ui.reader.model.InsertPage import eu.kanade.tachiyomi.ui.reader.model.InsertPage
@@ -21,6 +22,7 @@ import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation.NavigationRegion
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import kotlinx.coroutines.MainScope import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel import kotlinx.coroutines.cancel
import uy.kohesive.injekt.injectLazy
import kotlin.math.min import kotlin.math.min
/** /**
@@ -29,6 +31,8 @@ import kotlin.math.min
@Suppress("LeakingThis") @Suppress("LeakingThis")
abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer { abstract class PagerViewer(val activity: ReaderActivity) : BaseViewer {
val downloadManager: DownloadManager by injectLazy()
val scope = MainScope() val scope = MainScope()
/** /**
@@ -67,9 +71,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)
}
}
} }
} }
} }
@@ -52,7 +52,7 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
* Scale listener used to delegate events to the recycler view. * Scale listener used to delegate events to the recycler view.
*/ */
inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() { inner class ScaleListener : ScaleGestureDetector.SimpleOnScaleGestureListener() {
override fun onScaleBegin(detector: ScaleGestureDetector?): Boolean { override fun onScaleBegin(detector: ScaleGestureDetector): Boolean {
recycler?.onScaleBegin() recycler?.onScaleBegin()
return true return true
} }
@@ -71,13 +71,13 @@ class WebtoonFrame(context: Context) : FrameLayout(context) {
* Fling listener used to delegate events to the recycler view. * Fling listener used to delegate events to the recycler view.
*/ */
inner class FlingListener : GestureDetector.SimpleOnGestureListener() { inner class FlingListener : GestureDetector.SimpleOnGestureListener() {
override fun onDown(e: MotionEvent?): Boolean { override fun onDown(e: MotionEvent): Boolean {
return true return true
} }
override fun onFling( override fun onFling(
e1: MotionEvent?, e1: MotionEvent,
e2: MotionEvent?, e2: MotionEvent,
velocityX: Float, velocityX: Float,
velocityY: Float, velocityY: Float,
): Boolean { ): Boolean {
@@ -23,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 java.io.BufferedInputStream
import java.io.InputStream import java.io.InputStream
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@@ -272,12 +273,12 @@ class WebtoonPageHolder(
addSubscription(readImageHeaderSubscription) addSubscription(readImageHeaderSubscription)
} }
private fun process(imageStream: InputStream): InputStream { private fun process(imageStream: BufferedInputStream): InputStream {
if (!viewer.config.dualPageSplit) { if (!viewer.config.dualPageSplit) {
return imageStream return imageStream
} }
val isDoublePage = ImageUtil.isDoublePage(imageStream) val isDoublePage = ImageUtil.isWideImage(imageStream)
if (!isDoublePage) { if (!isDoublePage) {
return imageStream return imageStream
} }
@@ -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)
@@ -63,7 +63,7 @@ class WebtoonTransitionHolder(
* Binds the given [transition] with this view holder, subscribing to its state. * Binds the given [transition] with this view holder, subscribing to its state.
*/ */
fun bind(transition: ChapterTransition) { fun bind(transition: ChapterTransition) {
transitionView.bind(transition) transitionView.bind(transition, viewer.downloadManager, viewer.activity.presenter.manga)
transition.to?.let { observeStatus(it, transition) } transition.to?.let { observeStatus(it, transition) }
} }
@@ -11,6 +11,7 @@ import androidx.core.view.isGone
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import androidx.recyclerview.widget.WebtoonLayoutManager import androidx.recyclerview.widget.WebtoonLayoutManager
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition import eu.kanade.tachiyomi.ui.reader.model.ChapterTransition
@@ -24,6 +25,7 @@ import kotlinx.coroutines.cancel
import rx.subscriptions.CompositeSubscription import rx.subscriptions.CompositeSubscription
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 kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
@@ -32,6 +34,8 @@ import kotlin.math.min
*/ */
class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true, private val tapByPage: Boolean = false) : BaseViewer { class WebtoonViewer(val activity: ReaderActivity, val isContinuous: Boolean = true, private val tapByPage: Boolean = false) : BaseViewer {
val downloadManager: DownloadManager by injectLazy()
private val scope = MainScope() private val scope = MainScope()
/** /**
@@ -104,6 +108,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 +233,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 +259,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)
} }
@@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.util.system.AuthenticatorUtil
import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.startAuthentication import eu.kanade.tachiyomi.util.system.AuthenticatorUtil.startAuthentication
import eu.kanade.tachiyomi.util.system.logcat import eu.kanade.tachiyomi.util.system.logcat
import logcat.LogPriority import logcat.LogPriority
import java.util.Date
/** /**
* Blank activity with a BiometricPrompt. * Blank activity with a BiometricPrompt.
@@ -39,7 +38,6 @@ class UnlockActivity : BaseActivity() {
) { ) {
super.onAuthenticationSucceeded(activity, result) super.onAuthenticationSucceeded(activity, result)
SecureActivityDelegate.locked = false SecureActivityDelegate.locked = false
preferences.lastAppUnlock().set(Date().time)
finish() finish()
} }
}, },
@@ -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"
@@ -195,6 +219,28 @@ class SettingsAdvancedController : SettingsController() {
true true
} }
} }
editTextPreference {
key = Keys.defaultUserAgent
titleRes = R.string.pref_user_agent_string
text = preferences.defaultUserAgent().get()
summary = network.defaultUserAgent
onChange {
activity?.toast(R.string.requires_app_restart)
true
}
}
if (preferences.defaultUserAgent().isSet()) {
preference {
key = "pref_reset_user_agent"
titleRes = R.string.pref_reset_user_agent_string
onClick {
preferences.defaultUserAgent().delete()
activity?.toast(R.string.requires_app_restart)
}
}
}
} }
preferenceCategory { preferenceCategory {
@@ -486,11 +532,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
@@ -24,6 +24,7 @@ import eu.kanade.tachiyomi.util.preference.multiSelectListPreference
import eu.kanade.tachiyomi.util.preference.onClick import eu.kanade.tachiyomi.util.preference.onClick
import eu.kanade.tachiyomi.util.preference.preference import eu.kanade.tachiyomi.util.preference.preference
import eu.kanade.tachiyomi.util.preference.preferenceCategory import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.summaryRes
import eu.kanade.tachiyomi.util.preference.switchPreference import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.titleRes import eu.kanade.tachiyomi.util.preference.titleRes
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@@ -72,6 +73,12 @@ class SettingsDownloadController : SettingsController() {
bindTo(preferences.saveChaptersAsCBZ()) bindTo(preferences.saveChaptersAsCBZ())
titleRes = R.string.save_chapter_as_cbz titleRes = R.string.save_chapter_as_cbz
} }
switchPreference {
bindTo(preferences.splitTallImages())
titleRes = R.string.split_tall_images
summaryRes = R.string.split_tall_images_summary
}
preferenceCategory { preferenceCategory {
titleRes = R.string.pref_category_delete_chapters titleRes = R.string.pref_category_delete_chapters
@@ -125,20 +132,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 +154,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 +170,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 +261,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 +289,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
} }
} }
@@ -121,13 +121,13 @@ class SettingsMainController : SettingsController() {
searchItem.setOnActionExpandListener( searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener { object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean { override fun onMenuItemActionExpand(item: MenuItem): Boolean {
preferences.lastSearchQuerySearchSettings().set("") // reset saved search query preferences.lastSearchQuerySearchSettings().set("") // reset saved search query
router.pushController(SettingsSearchController().withFadeTransaction()) router.pushController(SettingsSearchController().withFadeTransaction())
return true return true
} }
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean { override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
return true return true
} }
}, },

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