Compare commits

...

172 Commits

Author SHA1 Message Date
0dc4862d79 Revert case insensitive source folder check 2021-04-11 18:19:41 -04:00
a3f1b72126 Lint fixes/ignore some errors 2021-04-11 18:16:15 -04:00
5ff10799e4 Release 0.10.10 2021-04-11 17:59:07 -04:00
a82e5f5452 Make library update/backup error log action clearer for non-technical users 2021-04-11 16:19:56 -04:00
e10cb0e632 Add locales: jv, lt, ne 2021-04-11 16:03:03 -04:00
c7e07a6df0 Update DoH translations 2021-04-11 16:02:02 -04:00
2e0c778090 Weblate translations (#4647)
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Arlangue <virgilemp@outlook.fr>
Co-authored-by: August Wale <bleachlithium@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Edgar Mejía <edgar13155@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Hajba Károly <karoly.hajba98@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jakub Fabijan <animatorzPolski@gmail.com>
Co-authored-by: Kiroki Benjamin <heptahex999@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: LOKE__01 <luckylakshman378@gmail.com>
Co-authored-by: Luis Andrés Bajaña F <labfernandez2014@gmail.com>
Co-authored-by: Lusuho <jevpsychox@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Matyáš Caras <hernik27@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nikola Perović <nikolaperovicccc@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: P6N7L <nichitapospai@gmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pijus Bend <pijus.bend@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Q farfayoux <aym.belrhiti@gmail.com>
Co-authored-by: Riztard Lanthorn <riyanluqman@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Schrödinger's cat <schrodingers-kate@protonmail.com>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Tantia <ilovechocobi@yahoo.com>
Co-authored-by: TheLastMelody <swordofthefallen@hotmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yardi van Nimwegen <yardivn@live.nl>
Co-authored-by: antocs <roxasthethund@yahoo.it>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 殺Mustafa <mustafasheref8@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/eo/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hu/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/jv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/kn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/my/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ne/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ro/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translation: Tachiyomi/Tachiyomi 0.x

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Arlangue <virgilemp@outlook.fr>
Co-authored-by: August Wale <bleachlithium@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Edgar Mejía <edgar13155@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: FateXBlood <zecrofelix@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Hajba Károly <karoly.hajba98@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jakub Fabijan <animatorzPolski@gmail.com>
Co-authored-by: Kiroki Benjamin <heptahex999@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: LOKE__01 <luckylakshman378@gmail.com>
Co-authored-by: Luis Andrés Bajaña F <labfernandez2014@gmail.com>
Co-authored-by: Lusuho <jevpsychox@gmail.com>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Matyáš Caras <contact@hernikplays.cz>
Co-authored-by: Matyáš Caras <hernik27@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Nikola Perović <nikolaperovicccc@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: P6N7L <nichitapospai@gmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pijus Bend <pijus.bend@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Q farfayoux <aym.belrhiti@gmail.com>
Co-authored-by: Riztard Lanthorn <riyanluqman@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Schrödinger's cat <schrodingers-kate@protonmail.com>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Shjosan <shjosan@kakmix.co>
Co-authored-by: Tantia <ilovechocobi@yahoo.com>
Co-authored-by: TheLastMelody <swordofthefallen@hotmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yardi van Nimwegen <yardivn@live.nl>
Co-authored-by: antocs <roxasthethund@yahoo.it>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 殺Mustafa <mustafasheref8@gmail.com>
2021-04-11 15:56:32 -04:00
592050c668 Actually ignore the case... 2021-04-11 14:23:24 -04:00
02c9191525 Make source download folder name case-insensitive
Fixes issues from things like "Mangasee" being renamed to "MangaSee"
2021-04-11 14:03:23 -04:00
d421401626 Log "Invalid download location" issues to error log 2021-04-11 14:00:45 -04:00
b2d4e5ab84 Add Google DoH provider 2021-04-11 13:10:03 -04:00
84e023607c BrowseSourceController: Fix navigation bar insets not properly applied (#4810) 2021-04-11 10:49:23 -04:00
f145fd0dec Move deletion actions to the IO thread (#4808) 2021-04-11 10:49:13 -04:00
42a9f911d8 Update some dependencies; downgrade core-ktx
Fixes ActionMode being underneath statusbar
2021-04-10 22:29:52 -04:00
9567d55312 Revert manga title folder for saved pages (closes #4803)
People also didn't like it making their galleries more complicate to navigate.
2021-04-10 14:33:14 -04:00
531cd99247 Update to Gradle 7 2021-04-10 09:48:30 -04:00
f3660d88dd Draw edge-to-edge (#4802) 2021-04-10 09:38:55 -04:00
3accb9a08b [SKIP CI] Add lock workflow 2021-04-10 09:36:01 -04:00
63ce7371bb Update some internal dependencies
They no longer rely on jcenter
2021-04-09 22:44:59 -04:00
01c3498dbf Search in library include manga description (#4787)
Co-Authored-By: jobobby04 <jobobby04@gmail.com>

Co-authored-by: jobobby04 <jobobby04@gmail.com>
2021-04-08 17:56:33 -04:00
b3471234ad Update NDK, more KTX usage (#4792)
* Update NDK

* Utilize more KTX extensions
2021-04-08 17:56:01 -04:00
b2d697131c Add clarification for category exclusion (closes #4777) 2021-04-06 23:29:46 -04:00
ef49fc91d8 Minor cleanup 2021-04-06 23:21:21 -04:00
6222b47a4f Flip crop borders and orientation toggles 2021-04-06 22:31:36 -04:00
f58e3c390a Update Kotlin and Kotlinter 2021-04-06 22:29:56 -04:00
7504621a24 Make reader spinner colors a bit more consistent 2021-04-06 22:29:39 -04:00
88e49a9b8b Align filter spinners (closes #2995) 2021-04-06 11:21:39 -04:00
5b23f29d06 Revert using fetch date for updates list
Spamming the list post-migration is currently a more common usecase than sources without chapter dates. We'll need to figure out a better way of handling both scenarios.
2021-04-04 18:11:11 -04:00
c1bdebee78 Fix global update category exclusion 2021-04-04 18:09:07 -04:00
ddd4cc10ff add sort by date fetched in library (#4773)
* add sort by date fetched in library

* chapter fetch date to 8
2021-04-04 17:18:28 -04:00
0ca62a4acc Allow excluding categories from auto-download
Closes #1412

Supersedes #4121
2021-04-04 17:15:06 -04:00
4f1275ac01 Allow excluding categories from library update
Closes #3467, #4661, #1839

Supersedes #4474
2021-04-04 16:48:39 -04:00
b2fee7035f Use Material Dialogs for auto-download categories preference
To allow for negative selections in the future.
2021-04-03 16:13:12 -04:00
e15d7cb548 Use Material Dialogs for global update categories preference
To allow for negative selections in the future.
2021-04-03 16:07:42 -04:00
3257cbe21f Fix label overflow for reader spinner preferences 2021-04-03 15:46:07 -04:00
1237af1ff3 Move BiometricUtil to correct package 2021-04-03 11:38:01 -04:00
68600b337e Allow weaker unlock methods (closes #4265) 2021-04-03 11:35:33 -04:00
dac2072eaa Use app name for page download folder and use manga title subfolders (closes #4684) 2021-04-03 10:40:35 -04:00
1b921f9845 Make extension load error logs less verbose 2021-04-03 10:27:40 -04:00
a3992d9fbe Minor cleanup 2021-04-03 10:12:31 -04:00
efd2a0cb7b Replace reading mode snackbar with toast (#4752) 2021-04-03 10:07:49 -04:00
fba428257b Remove weird cropping from icon when showing missing chapter warning (#4769) 2021-04-03 10:00:23 -04:00
ff36901007 Don't repeatedly vibrate/make sounds on download progress 2021-04-01 12:18:54 -04:00
940d8389b5 Add QuadStateCheckBox view 2021-03-31 23:03:42 -04:00
f7a6cbe5e2 Revert "Drop support for Android 5.x"
This reverts commit 443024cebb. Guess I'll do this a bit later so scb can get another major update first.

April Fools or whatever.
2021-03-31 22:20:59 -04:00
7aa379a857 Better handle webtoon SSIV crop border change 2021-03-31 22:20:17 -04:00
443024cebb Drop support for Android 5.x
It's 5-6 years old, and only accounts for 2% of users in the Firebase analytics.
2021-03-30 23:15:17 -04:00
1657f04d55 Add tooltips for previous/next chapter buttons
Based on d0738f5b00
2021-03-30 23:11:36 -04:00
407e798fdb Recreate webtoon SSIV when crop borders setting changes (fixes #4734) 2021-03-30 18:47:44 -04:00
4054f2a6a0 Add icon for crop border shortcut off state 2021-03-30 18:27:09 -04:00
468cdf603c Allow translating DNS over HTTPS (closes #4747) 2021-03-30 17:54:31 -04:00
988ec6a224 Fix nav overlay always showing on start (fixes #4736) 2021-03-29 16:54:32 -04:00
bdbdf211e2 Remove insert page when dual page split get turned off (#4739) 2021-03-29 16:54:20 -04:00
0437703cbf Fix binding of intarray preferences (maybe fixes #4728) 2021-03-28 17:06:56 -04:00
71aa592111 Use regular crop icon 2021-03-28 17:06:07 -04:00
d501c02f8b Add crop borders shortcut 2021-03-28 16:25:53 -04:00
9daf0e78b8 Remove ALPHA from dual page split label 2021-03-28 16:15:11 -04:00
dfa07a5f35 Clean up SpinnerPreference a bit 2021-03-28 16:13:59 -04:00
437c995d12 Show nav overlay on invert tap change
Based on db4eca90e9
2021-03-28 16:13:34 -04:00
cc6ae9d1a8 Fix Some Bangumi Track Bug (#4726) 2021-03-28 11:36:29 -04:00
c58e4f4dee Prevent manga title from jumping (fixes #4709) 2021-03-28 11:20:19 -04:00
c87b0e77de Show number of manga per source in migrate menu (#4703) 2021-03-28 11:11:19 -04:00
355d5af8ae Dismiss action toolbar after download action in updates (closes #4729) 2021-03-28 10:59:35 -04:00
3d99a8ebdb Fix fullscreen not applying on opening reader (fixes #4723) 2021-03-28 10:48:41 -04:00
c4b975b777 Cleanup reader spinner layouts 2021-03-27 17:59:52 -04:00
2911fe7a1a Add onPause\onResume persistence to searchView. Fixes issue #3627 (#4494)
* Add onPause\onResume persistence to searchView. Fixes issue #3627

* New controller subclass with built-in SearchView support

* Implement new SearchableNucleusController in SourceController

* Add query to BasePresenter (for one field it is not worth create a subclass in my opinion), convert BrowseSourceController to inherit from SearchableNucleusController

* move to flows to fix an issue in GlobalSearch where it would trigger the search multiple times

* Continue conversion to SearchableNucleusController

* Convert LibraryController, convert to flows, Known ISSUE with empty string being posted after setting the query upon creation of UI

* Fix issues with the post being tide to the SearchView queue which is not processed until shown. Add COLLAPSING state capture which should wrap this up.

* refactoring & enforce @StringRes for queryHint
2021-03-27 16:38:41 -04:00
14c114756d Clean up reader sheet spinner preferences
Based on fe2543b9d5

Co-Authored-By: Jays2Kings
2021-03-27 16:28:49 -04:00
e7a8107279 Reduce height of sheet when on color filter tab 2021-03-27 15:15:31 -04:00
bff73b1b40 Add tooltips to bottom reader menu items 2021-03-27 10:53:31 -04:00
c255f57d95 Reorganize reader sheet contents a bit 2021-03-27 10:53:19 -04:00
64c47bbaed Split general and reading mode sheet settings 2021-03-26 22:31:21 -04:00
e0b7698d40 Merge reader settings and color filter sheets
Heavily influenced by fe2543b9d5 (diff-8f47d7b7b53769ac18c28fe9978140c6bef44709879567acab2c6ef3270cd3a8)
2021-03-25 23:10:22 -04:00
a01792ac9a Maybe make opening file picker for choosing backup file more reliable 2021-03-25 13:56:39 -04:00
3ba078f64c Use more common MIME type for protobuf 2021-03-25 13:46:53 -04:00
a16240f123 Show unread entries first when sorting by unread (closes #4711)
Based on b212f8233e
2021-03-24 09:27:00 -04:00
e5a120e778 Update plugins 2021-03-24 09:26:27 -04:00
2ba60e9114 Added Start/Finished Date Support to AniList
Based on 1e3de8a67f

Co-Authored-By: Jays2Kings
2021-03-22 22:38:14 -04:00
472ce5a5e4 Fix migration due to variable shadowing (#4689) 2021-03-21 19:47:17 -04:00
99ba84c810 Handle null Anilist start dates (fixes #4685) 2021-03-20 16:36:31 -04:00
78285bdf37 Minor code cleanup 2021-03-20 15:58:54 -04:00
5a7f2684b3 Add navigation layout overlay (#4683)
* Add navigation layout overlay

* Minor clean up 

Destroy animator when done not on start
Move and change pref title 
Add summary
2021-03-20 15:36:01 -04:00
d912a42249 Fix chapters list getting updated from wrong thread (fixes #4505) 2021-03-20 15:35:02 -04:00
6d8c4fb8b1 Fix Bangumi search null image errors 2021-03-20 10:22:11 -04:00
a63cecbfcb Make tapping available extension row prompt install 2021-03-20 10:10:58 -04:00
4a5bceb4e4 Fix offline restore ignoring manga from not installed sources (fixes #4679) 2021-03-20 10:03:13 -04:00
86541445b7 Update AGP 2021-03-20 09:54:29 -04:00
4e826aa8e7 Use newer action for build workflow 2021-03-20 09:53:17 -04:00
b6e6f490e9 Implement migration for source search (#4657) 2021-03-19 23:40:09 -04:00
2145e878a4 Limit query for recent chapters to 500 (#4678) 2021-03-19 23:39:36 -04:00
355f6db255 [SKIP CI] Update README.md (#4667)
Fix link to Code of Conduct.
2021-03-18 16:52:48 -04:00
bc7632bf02 [SKIP CI] Add Code of Conduct (#4665)
* Add Code of Conduct

* Update badge section

* Add Code of Conduct link to README

* Change to relative links
2021-03-18 09:28:08 -04:00
609d8c9685 Add icons for reading mode toggle 2021-03-14 17:13:20 -04:00
2f08515455 Less janky enum iteration 2021-03-14 17:03:43 -04:00
7f450e185d Use fetch date instead of upload date when querying recent chapters (#4645) 2021-03-14 16:38:21 -04:00
747879b4ec Remove __cfduid cookie check
As per email:

Cloudflare is deprecating the __cfduid cookie and the cf-request-id headers. The __cfduid cookie will be removed on 10 May 2021 and the cf-request-id headers will be removed on 1 July. We expect that most customers will not have to take action as a result of this removal. [...] Starting on 10 May 2021, we will stop adding a “Set-Cookie” header on all HTTP responses. The last __cfduid cookies will expire 30 days after that.
2021-03-14 16:24:14 -04:00
4193870fa6 Library update freq: add 4 & 8 hours (#4557) 2021-03-14 16:22:10 -04:00
cdc5de3f1b Flip order of previous chapter reader transition text (closes #4608) 2021-03-14 16:18:52 -04:00
bc34d4fa88 Round snackbar corners 2021-03-14 16:15:25 -04:00
6fd4af8736 Adjust reader navigation button ripples 2021-03-14 16:13:18 -04:00
b5c2934270 Refactor LibraryUpdateService a bit for future changes 2021-03-14 16:08:00 -04:00
94f5117941 Remove online protobuf backup restore option 2021-03-13 18:45:22 -05:00
112e233498 Use Material dialogs for preferences
Partially addresses #2907
2021-03-13 18:00:24 -05:00
18b1326f3a Tweak dialog corner radius 2021-03-13 17:18:22 -05:00
1e58b05ead Add reading mode toggle 2021-03-13 16:47:16 -05:00
938919bd9b Move reader setting related classes 2021-03-13 16:24:44 -05:00
b6b78994d8 Move clear history from advanced settings to history screen menu (closes #4613) 2021-03-13 16:09:12 -05:00
fddd8ce305 Add "my" locale 2021-03-13 16:00:13 -05:00
ccff337975 Weblate translations (#4461)
Co-authored-by: Adaś <adam.prosniak@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Aung Myint Myat Oo <solidifyarmor@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Bail Adnan Farid <fks7dev@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Crazyom <naxom@laposte.net>
Co-authored-by: Cream π <f.t.nayeem014@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Eugene <e.shlyapkin99@gmail.com>
Co-authored-by: Eugene <eugcheung94@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Habibur Rahman <habiburr016@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Iuri Jikidze <ijiki16@freeuni.edu.ge>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jimly Asshiddiqy <j_mly@ymail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Murat Topuz <mrt_tpz@outlook.com>
Co-authored-by: Murilo Simionato Arnemann <murilo2110@hotmail.com>
Co-authored-by: Nick Koroghlishvili <n.koroglishvili5@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rocco Casadei <roccobot@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Ryota Hasegawa <unkchn123456@gmail.com>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yasin Chamsoy <tristeroni@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: dmswd <Bmswad1@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 赤城 悠 <hapipon815@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bg/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fa/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ka/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ko/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/my/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uz/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/
Translation: Tachiyomi/Tachiyomi 0.x

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Adaś <adam.prosniak@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Aung Myint Myat Oo <solidifyarmor@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Bail Adnan Farid <fks7dev@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Crazyom <naxom@laposte.net>
Co-authored-by: Cream π <f.t.nayeem014@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Eugene <e.shlyapkin99@gmail.com>
Co-authored-by: Eugene <eugcheung94@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Habibur Rahman <habiburr016@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Iuri Jikidze <ijiki16@freeuni.edu.ge>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Jimly Asshiddiqy <j_mly@ymail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Murat Topuz <mrt_tpz@outlook.com>
Co-authored-by: Murilo Simionato Arnemann <murilo2110@hotmail.com>
Co-authored-by: Nick Koroghlishvili <n.koroglishvili5@gmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rocco Casadei <roccobot@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Ryota Hasegawa <unkchn123456@gmail.com>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Yasin Chamsoy <tristeroni@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: dmswd <Bmswad1@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 赤城 悠 <hapipon815@gmail.com>
2021-03-13 15:58:55 -05:00
fde6b7af4f Disable sensor when using force orientation (closes #4618) 2021-03-13 15:47:43 -05:00
0657db7dcb Allow scrolling within reader color filter sheet (fixes #4612) 2021-03-13 15:20:07 -05:00
d1c2eaf6d5 Update URL for Local Manga guide (#4641) 2021-03-13 11:38:06 -05:00
91bb6b9016 Dependency updates 2021-03-12 09:06:13 -05:00
90351c6e9e Revert to core-ktx:1.5.0-beta01
Fixes bottom reader menu from being hidden behind navbar on Android 5.0.
2021-03-07 23:04:29 -05:00
dd4740e54f [SKIP CI] Automatically reopen issues when valid 2021-03-07 10:43:35 -05:00
48e7cbd76c Fix a decoder crash with RAR files 2021-03-05 18:52:58 +01:00
f51e32f39b Avoid crash during migration 2021-02-28 16:26:05 -05:00
ae42f59102 Hide subtitle in migration list of sources if no language set (i.e. uninstalled source) 2021-02-28 16:26:05 -05:00
5c8006f9b7 Use correct background for left chapter button in reader 2021-02-28 16:26:05 -05:00
aa5861d3ca AndroidX dependency updates 2021-02-28 16:26:05 -05:00
7a64bf55cb Dual page split allow to have different setting for Paged and Webtoon (#4527) 2021-02-28 16:17:37 -05:00
d4c9ab793f Fix a decoder crash 2021-02-23 16:53:57 +01:00
48d2849d97 Support CMYK and YCCK JPEGs and fix bad PNG cropping 2021-02-22 20:43:15 +01:00
776610d0e6 Let users invert dual page split (#4470)
* Let users invert dual page split

* Use Activity lifecycleScope and cleanup invert logic
2021-02-20 09:26:57 -05:00
3a790f3d66 Add Right and Left to reader settings (#4489)
* Add Right and Left to settings

* Fix whoopsie and minor tweak to how the array is fetched
2021-02-15 12:06:03 -05:00
7382042288 Add Twitter link to About section 2021-02-15 11:58:25 -05:00
33992d80bf Add orientation toggle to bottom reader menu 2021-02-13 18:50:50 -05:00
a92b0e567b Reword bookmark strings to clarify it's for a chapter, not a page 2021-02-13 17:27:40 -05:00
829a65e515 Adjust reader seekbar design
- Revert back to old prev/next chapter icons
- Make views taller for easier actions
- Use more consistent spacing
- Add ripples to prev/next chapter buttons
2021-02-13 17:00:00 -05:00
03ad48c055 [SKIP CI] Add instructions on how to get crash logs in issue templates 2021-02-13 16:07:30 -05:00
89837e4ced Initial adoption of bottom reader menus from TachiyomiSY
Co-authored-by: Jobobby04 <jobobby04@users.noreply.github.com>
Co-authored-by: CrepeTF <CrepeTF@users.noreply.github.com>
2021-02-13 10:47:17 -05:00
ace1db21d1 Rename drawable with more consistent naming 2021-02-13 10:44:35 -05:00
8bb69c455b Allow clicking the toolbar to go to the manga
Co-authored-by: Jobobby04 <jobobby04@users.noreply.github.com>
2021-02-13 10:26:59 -05:00
2dae706198 Avoid crash when source list is animating 2021-02-12 17:31:17 -05:00
3eda2a220a Avoid rare crashes in settings search for ListPreferences 2021-02-12 17:22:01 -05:00
61e5440b7c Avoid crash when device fails to handle opening a URL 2021-02-12 17:02:37 -05:00
2e2663bad9 Avoid crash if activity is already dead 2021-02-12 16:55:14 -05:00
f4dd150b70 [SKIP CI] Update to issue-closer-action@v2.0 2021-02-12 16:26:48 -05:00
2b35d22e25 Switch back to new image decoder for preview builds 2021-02-12 16:07:48 -05:00
f590378761 Release 0.10.9 2021-02-12 16:01:23 -05:00
f5f592be91 Require minimum WebView v88, try to catch fatal errors too 2021-02-12 12:42:33 -05:00
7a373fb43a Minor download icon optimizations 2021-02-12 12:27:40 -05:00
aded11e599 Make backup restoring logic more sequential 2021-02-12 12:27:40 -05:00
41d7cee020 Remove ExperimentalSerializationApi opt-in annotations 2021-02-12 12:27:40 -05:00
f2ef6a20e6 Weblate translations (#4378)
Co-authored-by: Adaś <adam.prosniak@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Crazyom <naxom@laposte.net>
Co-authored-by: Cream π <f.t.nayeem014@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Eugene <eugcheung94@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Habibur Rahman <habiburr016@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Iuri Jikidze <ijiki16@freeuni.edu.ge>
Co-authored-by: Jimly Asshiddiqy <j_mly@ymail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Murilo Simionato Arnemann <murilo2110@hotmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rocco Casadei <roccobot@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 赤城 悠 <hapipon815@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ka/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/
Translation: Tachiyomi/Tachiyomi 0.x

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Adaś <adam.prosniak@gmail.com>
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: Crazyom <naxom@laposte.net>
Co-authored-by: Cream π <f.t.nayeem014@gmail.com>
Co-authored-by: DarKCroX <darkcrox.2020@outlook.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: Eugene <eugcheung94@gmail.com>
Co-authored-by: Flamm <robindevaux25@gmail.com>
Co-authored-by: Habibur Rahman <habiburr016@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Iuri Jikidze <ijiki16@freeuni.edu.ge>
Co-authored-by: Jimly Asshiddiqy <j_mly@ymail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lyfja <yassinelaoud@gmail.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Michalis <michalisntovas@yahoo.gr>
Co-authored-by: Murilo Simionato Arnemann <murilo2110@hotmail.com>
Co-authored-by: Oğuz Ersen <oguzersen@protonmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Rocco Casadei <roccobot@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Soitora <simon.mattila@protonmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Co-authored-by: 赤城 悠 <hapipon815@gmail.com>
2021-02-12 12:27:32 -05:00
a398c3fb81 Handle link for multisource extension commits (closes #4432) 2021-02-11 17:35:15 -05:00
2a454b44cc Adjust some scopes 2021-02-09 19:14:38 -05:00
7b66ece895 Fix invisible overflow icon in chapter filter sheet in light blue theme 2021-02-09 19:12:44 -05:00
b5017eebbf Added dual page split setting (#4252)
* Add DualPageSplit option

* remove extra line

* Split double-page into two pages

* Remove !isAnimated check and add (ALPHA) to the label

* Fix missing insert pages

* Pager cleanup

* Add dual split to Webtoon and fix Vertical

* Fix L2R/R2L

* Add comments and refactor code in ImageUtil

* Use a simpler split solution in webtoon mode

Co-authored-by: weng <>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
2021-02-09 17:54:44 -05:00
aa67229daf Add weekly to library update frequency options (closes #4422) 2021-02-09 17:49:02 -05:00
5af68186d6 Clean up LibraryUpdateService a bit 2021-02-09 17:44:22 -05:00
545bc0e605 Open manga when clicking thumbnail in migration list (closes #4152) 2021-02-08 17:47:44 -05:00
291168f4de Remove unnecessary LayoutContainer implementations 2021-02-08 17:45:42 -05:00
9facb51f22 Add action to directly share crash log file from notification 2021-02-07 23:05:13 -05:00
5b7d8c5e37 Show locales in list of sources to migrate 2021-02-07 22:54:13 -05:00
5945937e4b Update AboutLibraries 2021-02-07 22:51:23 -05:00
9f9f9872eb Fix legacy backups
(cherry picked from commit ded58541f5903c109b70799683829e26018d2af6)
2021-02-07 22:33:07 -05:00
3566072f4a Revert attempt to programmatically determine user agent string; fallback to Edge 2021-02-07 17:54:28 -05:00
b85cd86b24 Add Esperanto locale 2021-02-07 16:55:44 -05:00
79c3767fff Chapter backup optimization
From fc6d9aaf51 (diff-9872ccc3c9af14d2872ec99199409e60a11cb754ab23e733b1d45843778f7c95R24)
2021-02-07 16:20:07 -05:00
cf1609a429 Massage user agent string from WebView a bit more 2021-02-07 16:19:13 -05:00
3aeac7e7b5 Fix selected tab in sheets not being the accent color 2021-02-07 10:54:35 -05:00
1557f713f4 Don't restrict filter sheet height anymore 2021-02-07 10:49:08 -05:00
b63d24ac1a Add Right and Left navigation (#4392)
and remove default navigation classes in favor of the navigation classes
2021-02-06 23:26:56 -05:00
348c1ff29d Avoid some unnecessary re-renderings of download icons 2021-02-06 23:25:39 -05:00
717e55497f Fix downloads getting deleted when marked as unread 2021-02-06 22:48:06 -05:00
d84b5e8b46 Show help action when source fails to load 2021-02-06 13:09:56 -05:00
5f9ddf9ff5 Use AndroidX version of ContextThemeWrapper 2021-02-06 12:51:40 -05:00
bbee093c63 Remove some logic around old legacy backup versions + minor optimizations 2021-02-06 12:15:34 -05:00
e8c35ae4e1 Do a regular return to cancel update jobs instead of throwing an exception 2021-02-06 12:14:55 -05:00
1607658c30 Set clip data when sharing content URIs (closes #4198) 2021-02-06 09:43:33 -05:00
2e9ef373f3 Minor optimizations for restoring full backups
Based on fc6d9aaf51
2021-02-06 09:32:00 -05:00
ec6eef6d37 Switch back to new image decoder for preview builds 2021-02-06 09:31:18 -05:00
265 changed files with 6253 additions and 2924 deletions

View File

@ -2,7 +2,7 @@
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.8)
- I have updated to the latest version of the app (stable is v0.10.10)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
@ -24,3 +24,5 @@ I acknowledge that:
## Other details
Additional details and attachments.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.

View File

@ -9,7 +9,7 @@ labels: "bug"
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.8)
- I have updated to the latest version of the app (stable is v0.10.10)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions
@ -34,3 +34,5 @@ This happened instead.
## Other details
Additional details and attachments.
If you're experiencing crashes, share the crash logs from More → Settings → Advanced → Dump crash logs.

View File

@ -9,7 +9,7 @@ labels: "feature"
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.8)
- I have updated to the latest version of the app (stable is v0.10.10)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/tachiyomiorg/tachiyomi-extensions

View File

@ -71,25 +71,24 @@ jobs:
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Create release
- name: Clean up build artifacts
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
cp ${{ env.SIGNED_RELEASE_FILE }} tachiyomi-${{ env.VERSION_TAG }}.apk
md5=`md5sum tachiyomi-${{ env.VERSION_TAG }}.apk | awk '{ print $1 }'`
echo "APK_MD5=$md5" >> $GITHUB_ENV
- name: Create Release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
uses: softprops/action-gh-release@v1
with:
tag_name: ${{ env.VERSION_TAG }}
release_name: Tachiyomi ${{ env.VERSION_TAG }}
draft: true
name: Tachiyomi ${{ env.VERSION_TAG }}
body: |
MD5: ${{ env.APK_MD5 }}
files: |
tachiyomi-${{ env.VERSION_TAG }}.apk
draft: ${{ github.event.inputs.dry-run != '' }}
prerelease: false
- name: Upload APK to release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'tachiyomiorg/tachiyomi'
uses: actions/upload-release-asset@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
upload_url: ${{ steps.create_release.outputs.upload_url }}
asset_path: ${{ env.SIGNED_RELEASE_FILE }}
asset_name: tachiyomi-${{ env.VERSION_TAG }}.apk
asset_content_type: application/vnd.android.package-archive

View File

@ -7,31 +7,30 @@ jobs:
autoclose:
runs-on: ubuntu-latest
steps:
- name: Autoclose when created in wrong repo
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: title
regex: ".*THIS ISSUE IS IN THE WRONG REPO.*"
message: "@${issue.user.login} this issue was automatically closed because it was not opened in the correct repo, as the template mentioned."
- name: Autoclose when no short description provided
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: title
regex: ".*<Write short description here>*"
message: "@${issue.user.login} this issue was automatically closed because you did not fill out the description in the title."
- name: Autoclose when body acknowledgement section not removed
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: body
regex: ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*"
message: "@${issue.user.login} this issue was automatically closed because the acknowledgment section was not removed."
- name: Autoclose when body requested information not filled out
uses: arkon/issue-closer-action@v1.1
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
type: body
regex: ".*\\* (Tachiyomi version|Android version|Device): \\?.*"
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."
- name: Autoclose issues
uses: arkon/issue-closer-action@v3.0
with:
repo-token: ${{ secrets.GITHUB_TOKEN }}
rules: |
[
{
"type": "title",
"regex": ".*THIS ISSUE IS IN THE WRONG REPO.*",
"message": "It was not opened in the correct repo, as the template mentioned."
},
{
"type": "title",
"regex": ".*<Write short description here>*",
"message": "The description in the title was not filled out."
},
{
"type": "body",
"regex": ".*DELETE THIS SECTION IF YOU HAVE READ AND ACKNOWLEDGED IT.*",
"message": "The acknowledgment section was not removed."
},
{
"type": "body",
"regex": ".*\\* (Tachiyomi version|Android version|Device): \\?.*",
"message": "Requested information in the template was not filled out."
}
]

19
.github/workflows/lock.yml vendored Normal file
View File

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

76
CODE_OF_CONDUCT.md Normal file
View File

@ -0,0 +1,76 @@
# Code of Conduct
## Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, sex characteristics, gender identity and expression,
level of experience, education, socio-economic status, nationality, personal
appearance, race, religion, or sexual identity and orientation.
## Our Standards
Examples of behavior that contributes to creating a positive environment
include:
* Using welcoming and inclusive language
* Being respectful of differing viewpoints and experiences
* Gracefully accepting constructive criticism
* Focusing on what is best for the community
* Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
* The use of sexualized language or imagery and unwelcome sexual attention or
advances
* Trolling, insulting/derogatory comments, and personal or political attacks
* Public or private harassment
* Publishing others' private information, such as a physical or electronic
address, without explicit permission
* Other conduct which could reasonably be considered inappropriate in a
professional setting
## Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or
reject comments, commits, code, wiki edits, issues, and other contributions
that are not aligned to this Code of Conduct, or to ban temporarily or
permanently any contributor for other behaviors that they deem inappropriate,
threatening, offensive, or harmful.
## Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at the Tachiyomi [Discord server](https://discord.gg/tachiyomi). All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an incident.
Further details of specific enforcement policies may be posted separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4,
available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html
[homepage]: https://www.contributor-covenant.org
For answers to common questions about this code of conduct, see
https://www.contributor-covenant.org/faq

View File

@ -1,6 +1,6 @@
| Build | Stable | Weekly Preview | Contribute | Support Server |
|-------|----------|---------|------------|---------|
| ![CI](https://github.com/tachiyomiorg/tachiyomi/workflows/CI/badge.svg?branch=dev&event=push) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest weekly build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi) |
| ![CI](https://github.com/tachiyomiorg/tachiyomi/workflows/CI/badge.svg?branch=dev&event=push) | [![stable release](https://img.shields.io/github/release/tachiyomiorg/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi/releases) | [![latest weekly build](https://img.shields.io/github/v/release/tachiyomiorg/tachiyomi-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/tachiyomi-preview/releases) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg?label=discord&labelColor=7289da&color=2c2f33&style=flat)](https://discord.gg/tachiyomi) |
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
@ -63,7 +63,12 @@ Source requests should be created at https://github.com/tachiyomiorg/tachiyomi-e
<details><summary>Contributing</summary>
See [CONTRIBUTING.md](https://github.com/tachiyomiorg/tachiyomi/blob/master/CONTRIBUTING.md).
See [CONTRIBUTING.md](./CONTRIBUTING.md).
</details>
<details><summary>Code of Conduct</summary>
See [CODE_OF_CONDUCT.md](./CODE_OF_CONDUCT.md).
</details>
## FAQ

View File

@ -9,7 +9,6 @@ plugins {
id("com.mikepenz.aboutlibraries.plugin")
kotlin("android")
kotlin("kapt")
kotlin("plugin.parcelize")
kotlin("plugin.serialization")
id("com.github.zellius.shortcut-helper")
}
@ -30,8 +29,8 @@ android {
minSdkVersion(AndroidConfig.minSdk)
targetSdkVersion(AndroidConfig.targetSdk)
testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner"
versionCode = 55
versionName = "0.10.8"
versionCode = 57
versionName = "0.10.10"
buildConfigField("String", "COMMIT_COUNT", "\"${getCommitCount()}\"")
buildConfigField("String", "COMMIT_SHA", "\"${getGitSha()}\"")
@ -92,6 +91,7 @@ android {
exclude("META-INF/LICENSE")
exclude("META-INF/LICENSE.txt")
exclude("META-INF/NOTICE")
exclude("META-INF/*.kotlin_module")
}
dependenciesInfo {
@ -120,20 +120,20 @@ dependencies {
implementation("tachiyomi.sourceapi:source-api:1.1")
// AndroidX libraries
implementation("androidx.annotation:annotation:1.2.0-beta01")
implementation("androidx.appcompat:appcompat:1.3.0-beta01")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha02")
implementation("androidx.annotation:annotation:1.3.0-alpha01")
implementation("androidx.appcompat:appcompat:1.3.0-rc01")
implementation("androidx.biometric:biometric-ktx:1.2.0-alpha03")
implementation("androidx.browser:browser:1.3.0")
implementation("androidx.cardview:cardview:1.0.0")
implementation("androidx.constraintlayout:constraintlayout:2.1.0-alpha2")
implementation("androidx.constraintlayout:constraintlayout:2.1.0-beta01")
implementation("androidx.coordinatorlayout:coordinatorlayout:1.1.0")
implementation("androidx.core:core-ktx:1.5.0-beta01")
implementation("androidx.core:core-ktx:1.3.2")
implementation("androidx.multidex:multidex:2.0.1")
implementation("androidx.preference:preference-ktx:1.1.1")
implementation("androidx.recyclerview:recyclerview:1.2.0-beta01")
implementation("androidx.recyclerview:recyclerview:1.2.0")
implementation("androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01")
val lifecycleVersion = "2.3.0-rc01"
val lifecycleVersion = "2.3.0"
implementation("androidx.lifecycle:lifecycle-common-java8:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-process:$lifecycleVersion")
implementation("androidx.lifecycle:lifecycle-runtime-ktx:$lifecycleVersion")
@ -144,7 +144,7 @@ dependencies {
// UI library
implementation("com.google.android.material:material:1.3.0")
"standardImplementation"("com.google.firebase:firebase-core:18.0.2")
"standardImplementation"("com.google.firebase:firebase-core:18.0.3")
// ReactiveX
implementation("io.reactivex:rxandroid:1.2.1")
@ -153,7 +153,7 @@ dependencies {
implementation("com.github.pwittchen:reactivenetwork:0.13.0")
// Network client
val okhttpVersion = "4.10.0-RC1"
val okhttpVersion = "5.0.0-alpha.2"
implementation("com.squareup.okhttp3:okhttp:$okhttpVersion")
implementation("com.squareup.okhttp3:logging-interceptor:$okhttpVersion")
implementation("com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttpVersion")
@ -174,7 +174,7 @@ dependencies {
// Disk
implementation("com.jakewharton:disklrucache:2.0.2")
implementation("com.github.inorichi:unifile:e9ee588")
implementation("com.github.tachiyomiorg:unifile:e9e3a40")
implementation("com.github.junrar:junrar:7.4.0")
// HTML parser
@ -187,7 +187,7 @@ dependencies {
implementation("io.requery:sqlite-android:3.33.0")
// Preferences
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3")
implementation("com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.4")
// Model View Presenter
val nucleusVersion = "3.0.0"
@ -198,14 +198,12 @@ dependencies {
implementation("com.github.inorichi.injekt:injekt-core:65b0440")
// Image library
val glideVersion = "4.11.0"
val glideVersion = "4.12.0"
implementation("com.github.bumptech.glide:glide:$glideVersion")
implementation("com.github.bumptech.glide:okhttp3-integration:$glideVersion")
kapt("com.github.bumptech.glide:compiler:$glideVersion")
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:6caf219")
// TODO: switch to new decoder for stable releases
// implementation("com.github.tachiyomiorg:subsampling-scale-image-view:ca26317")
implementation("com.github.tachiyomiorg:subsampling-scale-image-view:547d9c0")
// Logging
implementation("com.jakewharton.timber:timber:4.7.1")
@ -223,7 +221,8 @@ dependencies {
implementation("eu.davidea:flexible-adapter-ui:1.0.0")
implementation("com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0")
implementation("com.github.chrisbanes:PhotoView:2.3.0")
implementation("com.github.tachiyomiorg:DirectionalViewPager:7d0617d")
implementation("com.github.tachiyomiorg:DirectionalViewPager:1.0.0")
implementation("dev.chrisbanes.insetter:insetter:0.5.0")
// 3.2.0+ introduces weird UI blinking or cut off issues on some devices
val materialDialogsVersion = "3.1.1"
@ -236,7 +235,7 @@ dependencies {
implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude(group = "com.android.support")
}
implementation("com.github.tachiyomiorg:conductor-support-preference:1.1.1")
implementation("com.github.tachiyomiorg:conductor-support-preference:2.0.1")
// FlowBinding
val flowbindingVersion = "0.12.0"
@ -250,7 +249,7 @@ dependencies {
implementation("com.mikepenz:aboutlibraries:${BuildPluginsVersion.ABOUTLIB_PLUGIN}")
// Tests
testImplementation("junit:junit:4.13.1")
testImplementation("junit:junit:4.13.2")
testImplementation("org.assertj:assertj-core:3.16.1")
testImplementation("org.mockito:mockito-core:1.10.19")

View File

@ -9,6 +9,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.extension.ExtensionUpdateJob
import eu.kanade.tachiyomi.network.PREF_DOH_CLOUDFLARE
import eu.kanade.tachiyomi.ui.library.LibrarySort
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
@ -127,6 +128,17 @@ object Migrations {
context.toast(R.string.myanimelist_relogin)
}
}
if (oldVersion < 57) {
// Migrate DNS over HTTPS setting
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
val wasDohEnabled = prefs.getBoolean("enable_doh", false)
if (wasDohEnabled) {
prefs.edit {
putInt(PreferenceKeys.dohProvider, PREF_DOH_CLOUDFLARE)
remove("enable_doh")
}
}
}
return true
}

View File

@ -80,6 +80,13 @@ abstract class AbstractBackupManager(protected val context: Context) {
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
}
/**
* Updates a list of chapters with known database ids
*/
protected fun updateKnownChapters(chapters: List<Chapter>) {
databaseHelper.updateKnownChaptersBackup(chapters).executeAsBlocking()
}
/**
* Return number of backups.
*

View File

@ -37,9 +37,9 @@ abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val co
protected val errors = mutableListOf<Pair<Date, String>>()
abstract fun performRestore(uri: Uri): Boolean
abstract suspend fun performRestore(uri: Uri): Boolean
fun restoreBackup(uri: Uri): Boolean {
suspend fun restoreBackup(uri: Uri): Boolean {
val startTime = System.currentTimeMillis()
restoreProgress = 0
errors.clear()

View File

@ -111,7 +111,7 @@ class BackupCreateService : Service() {
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
val unifile = UniFile.fromUri(this, backupFileUri)
notifier.showBackupComplete(unifile)
notifier.showBackupComplete(unifile, backupType == BackupConst.BACKUP_TYPE_LEGACY)
} catch (e: Exception) {
notifier.showBackupError(e.message)
}

View File

@ -24,6 +24,7 @@ class BackupNotifier(private val context: Context) {
setSmallIcon(R.drawable.ic_tachi)
setAutoCancel(false)
setOngoing(true)
setOnlyAlertOnce(true)
}
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_BACKUP_RESTORE_COMPLETE) {
@ -41,7 +42,6 @@ class BackupNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.creating_backup))
setProgress(0, 0, true)
setOnlyAlertOnce(true)
}
builder.show(Notifications.ID_BACKUP_PROGRESS)
@ -60,7 +60,7 @@ class BackupNotifier(private val context: Context) {
}
}
fun showBackupComplete(unifile: UniFile) {
fun showBackupComplete(unifile: UniFile, isLegacyFormat: Boolean) {
context.notificationManager.cancel(Notifications.ID_BACKUP_PROGRESS)
with(completeNotificationBuilder) {
@ -73,7 +73,7 @@ class BackupNotifier(private val context: Context) {
addAction(
R.drawable.ic_share_24dp,
context.getString(R.string.action_share),
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, Notifications.ID_BACKUP_COMPLETE)
NotificationReceiver.shareBackupPendingBroadcast(context, unifile.uri, isLegacyFormat, Notifications.ID_BACKUP_COMPLETE)
)
show(Notifications.ID_BACKUP_COMPLETE)
@ -141,7 +141,7 @@ class BackupNotifier(private val context: Context) {
addAction(
R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log),
context.getString(R.string.action_show_errors),
NotificationReceiver.openErrorLogPendingActivity(context, uri)
)
}

View File

@ -14,7 +14,10 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
import timber.log.Timber
@ -40,12 +43,11 @@ class BackupRestoreService : Service() {
* @param context context of application
* @param uri path of Uri
*/
fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) {
fun start(context: Context, uri: Uri, mode: Int) {
if (!isRunning(context)) {
val intent = Intent(context, BackupRestoreService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri)
putExtra(BackupConst.EXTRA_MODE, mode)
online?.let { putExtra(BackupConst.EXTRA_TYPE, it) }
}
ContextCompat.startForegroundService(context, intent)
}
@ -68,12 +70,14 @@ class BackupRestoreService : Service() {
*/
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var ioScope: CoroutineScope
private var backupRestore: AbstractBackupRestore<*>? = null
private lateinit var notifier: BackupNotifier
override fun onCreate() {
super.onCreate()
ioScope = CoroutineScope(SupervisorJob() + Dispatchers.IO)
notifier = BackupNotifier(this)
wakeLock = acquireWakeLock(javaClass.name)
@ -92,6 +96,7 @@ class BackupRestoreService : Service() {
private fun destroyJob() {
backupRestore?.job?.cancel()
ioScope?.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
@ -113,15 +118,15 @@ class BackupRestoreService : Service() {
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
val uri = intent?.getParcelableExtra<Uri>(BackupConst.EXTRA_URI) ?: return START_NOT_STICKY
val mode = intent.getIntExtra(BackupConst.EXTRA_MODE, BackupConst.BACKUP_TYPE_FULL)
val online = intent.getBooleanExtra(BackupConst.EXTRA_TYPE, true)
// Cancel any previous job if needed.
backupRestore?.job?.cancel()
backupRestore = when (mode) {
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier)
else -> LegacyBackupRestore(this, notifier)
}
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
backupRestore?.writeErrorLog()
@ -129,14 +134,15 @@ class BackupRestoreService : Service() {
notifier.showRestoreError(exception.message)
stopSelf(startId)
}
backupRestore?.job = GlobalScope.launch(handler) {
val job = ioScope.launch(handler) {
if (backupRestore?.restoreBackup(uri) == false) {
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
}
}
backupRestore?.job?.invokeOnCompletion {
job.invokeOnCompletion {
stopSelf(startId)
}
backupRestore?.job = job
return START_NOT_STICKY
}

View File

@ -26,10 +26,6 @@ import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.toSManga
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.protobuf.ProtoBuf
import okio.buffer
import okio.gzip
@ -37,7 +33,6 @@ import okio.sink
import timber.log.Timber
import kotlin.math.max
@OptIn(ExperimentalSerializationApi::class)
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
val parser = ProtoBuf
@ -185,24 +180,13 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
/**
* Fetches manga information
*
* @param source source of manga
* @param manga manga that needs updating
* @return Updated manga info.
*/
suspend fun restoreMangaFetch(source: Source?, manga: Manga, online: Boolean): Manga {
return if (online && source != null) {
val networkManga = source.getMangaDetails(manga.toMangaInfo())
manga.also {
it.copyFrom(networkManga.toSManga())
it.favorite = manga.favorite
it.initialized = true
it.id = insertManga(manga)
}
} else {
manga.also {
it.initialized = it.description != null
it.id = insertManga(it)
}
fun restoreManga(manga: Manga): Manga {
return manga.also {
it.initialized = it.description != null
it.id = insertManga(it)
}
}
@ -247,7 +231,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*/
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
categories.forEach { backupCategoryOrder ->
backupCategories.firstOrNull {
it.order == backupCategoryOrder
@ -274,7 +258,7 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
*/
internal fun restoreHistoryForManga(history: List<BackupHistory>) {
// List containing history to be updated
val historyToBeUpdated = mutableListOf<History>()
val historyToBeUpdated = ArrayList<History>(history.size)
for ((url, lastRead) in history) {
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
// Check if history already in database and update
@ -311,29 +295,26 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
val trackToUpdate = mutableListOf<Track>()
tracks.forEach { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
var isInDatabase = false
for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) {
// The sync is already in the db, only update its fields
if (track.media_id != dbTrack.media_id) {
dbTrack.media_id = track.media_id
}
if (track.library_id != dbTrack.library_id) {
dbTrack.library_id = track.library_id
}
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
var isInDatabase = false
for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) {
// The sync is already in the db, only update its fields
if (track.media_id != dbTrack.media_id) {
dbTrack.media_id = track.media_id
}
if (track.library_id != dbTrack.library_id) {
dbTrack.library_id = track.library_id
}
dbTrack.last_chapter_read = max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
}
if (!isInDatabase) {
// Insert new sync. Let the db assign the id
track.id = null
trackToUpdate.add(track)
}
}
if (!isInDatabase) {
// Insert new sync. Let the db assign the id
track.id = null
trackToUpdate.add(track)
}
}
// Update database
@ -342,25 +323,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
}
}
/**
* Restore the chapters for manga if chapters already in database
*
* @param manga manga of chapters
* @param chapters list containing chapters that get restored
* @return boolean answering if chapter fetch is not needed
*/
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
// Return if fetch is needed
if (dbChapters.isEmpty() || dbChapters.size < chapters.size) {
return false
}
chapters.forEach { chapter ->
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
if (pos != -1) {
val dbChapter = dbChapters[pos]
val dbChapter = dbChapters.find { it.url == chapter.url }
if (dbChapter != null) {
chapter.id = dbChapter.id
chapter.copyFrom(dbChapter)
if (dbChapter.read && !chapter.read) {
@ -373,38 +341,12 @@ class FullBackupManager(context: Context) : AbstractBackupManager(context) {
chapter.bookmark = dbChapter.bookmark
}
}
chapter.manga_id = manga.id
}
// Filter the chapters that couldn't be found.
chapters.filter { it.id != null }
chapters.map { it.manga_id = manga.id }
updateChapters(chapters)
return true
}
internal fun restoreChaptersForMangaOffline(manga: Manga, chapters: List<Chapter>) {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
chapters.forEach { chapter ->
val pos = dbChapters.indexOfFirst { it.url == chapter.url }
if (pos != -1) {
val dbChapter = dbChapters[pos]
chapter.id = dbChapter.id
chapter.copyFrom(dbChapter)
if (dbChapter.read && !chapter.read) {
chapter.read = dbChapter.read
chapter.last_page_read = dbChapter.last_page_read
} else if (chapter.last_page_read == 0 && dbChapter.last_page_read != 0) {
chapter.last_page_read = dbChapter.last_page_read
}
if (!chapter.bookmark && dbChapter.bookmark) {
chapter.bookmark = dbChapter.bookmark
}
}
}
chapters.map { it.manga_id = manga.id }
updateChapters(chapters.filter { it.id != null })
insertChapters(chapters.filter { it.id == null })
val newChapters = chapters.groupBy { it.id != null }
newChapters[true]?.let { updateKnownChapters(it) }
newChapters[false]?.let { insertChapters(it) }
}
}

View File

@ -12,18 +12,14 @@ import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.launchIO
import kotlinx.serialization.ExperimentalSerializationApi
import okio.buffer
import okio.gzip
import okio.source
import java.util.Date
@OptIn(ExperimentalSerializationApi::class)
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
class FullBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
override fun performRestore(uri: Uri): Boolean {
override suspend fun performRestore(uri: Uri): Boolean {
backupManager = FullBackupManager(context)
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
@ -45,9 +41,11 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
return false
}
restoreManga(it, backup.backupCategories, online)
restoreManga(it, backup.backupCategories)
}
// TODO: optionally trigger online library + tracker update
return true
}
@ -60,23 +58,17 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
}
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>) {
val manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl()
val categories = backupManga.categories
val history = backupManga.history
val tracks = backupManga.getTrackingImpl()
val source = backupManager.sourceManager.get(manga.source)
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
try {
if (source != null || !online) {
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, online)
} else {
errors.add(Date() to "${manga.title} [$sourceName]: ${context.getString(R.string.source_not_found_name, sourceName)}")
}
restoreMangaData(manga, chapters, categories, history, tracks, backupCategories)
} catch (e: Exception) {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} [$sourceName]: ${e.message}")
}
@ -88,7 +80,6 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
* Returns a manga restore observable
*
* @param manga manga data from json
* @param source source to get manga data from
* @param chapters chapters data from json
* @param categories categories data from json
* @param history history data from json
@ -96,25 +87,23 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
*/
private fun restoreMangaData(
manga: Manga,
source: Source?,
chapters: List<Chapter>,
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>,
online: Boolean
backupCategories: List<BackupCategory>
) {
val dbManga = backupManager.getMangaFromDatabase(manga)
db.inTransaction {
val dbManga = backupManager.getMangaFromDatabase(manga)
if (dbManga == null) {
// Manga not in database
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
} else { // Manga in database
restoreMangaFetch(manga, chapters, categories, history, tracks, backupCategories)
} else {
// Manga in database
// Copy information from manga already in database
backupManager.restoreMangaNoFetch(manga, dbManga)
// Fetch rest of manga information
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
restoreMangaNoFetch(manga, chapters, categories, history, tracks, backupCategories)
}
}
}
@ -127,58 +116,36 @@ class FullBackupRestore(context: Context, notifier: BackupNotifier, private val
* @param categories categories that need updating
*/
private fun restoreMangaFetch(
source: Source?,
manga: Manga,
chapters: List<Chapter>,
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>,
online: Boolean
backupCategories: List<BackupCategory>
) {
launchIO {
try {
val fetchedManga = backupManager.restoreMangaFetch(source, manga, online)
fetchedManga.id ?: (return@launchIO)
try {
val fetchedManga = backupManager.restoreManga(manga)
fetchedManga.id ?: return
if (online && source != null) {
updateChapters(source, fetchedManga, chapters)
} else {
backupManager.restoreChaptersForMangaOffline(fetchedManga, chapters)
}
backupManager.restoreChaptersForManga(fetchedManga, chapters)
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories)
updateTracking(fetchedManga, tracks)
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
restoreExtraForManga(fetchedManga, categories, history, tracks, backupCategories)
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
}
private fun restoreMangaNoFetch(
source: Source?,
backupManga: Manga,
chapters: List<Chapter>,
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>,
online: Boolean
backupCategories: List<BackupCategory>
) {
launchIO {
if (online && source != null) {
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
updateChapters(source, backupManga, chapters)
}
} else {
backupManager.restoreChaptersForMangaOffline(backupManga, chapters)
}
backupManager.restoreChaptersForManga(backupManga, chapters)
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
updateTracking(backupManga, tracks)
}
restoreExtraForManga(backupManga, categories, history, tracks, backupCategories)
}
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {

View File

@ -5,12 +5,10 @@ import android.net.Uri
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
import kotlinx.serialization.ExperimentalSerializationApi
import okio.buffer
import okio.gzip
import okio.source
@OptIn(ExperimentalSerializationApi::class)
class FullBackupRestoreValidator : AbstractBackupRestoreValidator() {
/**
* Checks for critical backup file data.

View File

@ -53,30 +53,14 @@ import kotlin.math.max
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
var parserVersion: Int = version
private set
var parser: Gson = initParser()
/**
* Set version of parser
*
* @param version version of parser
*/
internal fun setVersion(version: Int) {
this.parserVersion = version
parser = initParser()
}
private fun initParser(): Gson = when (parserVersion) {
2 ->
GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
.create()
val parser: Gson = when (version) {
2 -> GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
.create()
else -> throw Exception("Unknown backup version")
}
@ -308,7 +292,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
*/
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
val mangaCategoriesToUpdate = ArrayList<MangaCategory>(categories.size)
for (backupCategoryStr in categories) {
for (dbCategory in dbCategories) {
if (backupCategoryStr == dbCategory.name) {
@ -332,7 +316,7 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
*/
internal fun restoreHistoryForManga(history: List<DHistory>) {
// List containing history to be updated
val historyToBeUpdated = mutableListOf<History>()
val historyToBeUpdated = ArrayList<History>(history.size)
for ((url, lastRead) in history) {
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
// Check if history already in database and update
@ -361,14 +345,14 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
* @param tracks the track list to restore.
*/
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
// Fix foreign keys with the current manga id
tracks.map { it.manga_id = manga.id!! }
// Get tracks from database
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
val trackToUpdate = mutableListOf<Track>()
val trackToUpdate = ArrayList<Track>(tracks.size)
tracks.forEach { track ->
// Fix foreign keys with the current manga id
track.manga_id = manga.id!!
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
var isInDatabase = false
@ -423,12 +407,13 @@ class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : Ab
chapter.copyFrom(dbChapter)
break
}
}
// Filter the chapters that couldn't be found.
chapters.filter { it.id != null }
chapters.map { it.manga_id = manga.id }
updateChapters(chapters)
chapter.manga_id = manga.id
}
// Filter the chapters that couldn't be found.
updateChapters(chapters.filter { it.id != null })
return true
}
}

View File

@ -21,12 +21,11 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.lang.launchIO
import java.util.Date
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
override fun performRestore(uri: Uri): Boolean {
override suspend fun performRestore(uri: Uri): Boolean {
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser.parseReader(reader).asJsonObject
@ -63,7 +62,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
}
private fun restoreManga(mangaJson: JsonObject) {
private suspend fun restoreManga(mangaJson: JsonObject) {
val manga = backupManager.parser.fromJson<MangaImpl>(
mangaJson.get(
Backup.MANGA
@ -113,7 +112,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
* @param history history data from json
* @param tracks tracking data from json
*/
private fun restoreMangaData(
private suspend fun restoreMangaData(
manga: Manga,
source: Source,
chapters: List<Chapter>,
@ -143,7 +142,7 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
* @param chapters chapters of manga that needs updating
* @param categories categories that need updating
*/
private fun restoreMangaFetch(
private suspend fun restoreMangaFetch(
source: Source,
manga: Manga,
chapters: List<Chapter>,
@ -151,23 +150,21 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
history: List<DHistory>,
tracks: List<Track>
) {
launchIO {
try {
val fetchedManga = backupManager.fetchManga(source, manga)
fetchedManga.id ?: (return@launchIO)
try {
val fetchedManga = backupManager.fetchManga(source, manga)
fetchedManga.id ?: return
updateChapters(source, fetchedManga, chapters)
updateChapters(source, fetchedManga, chapters)
restoreExtraForManga(fetchedManga, categories, history, tracks)
restoreExtraForManga(fetchedManga, categories, history, tracks)
updateTracking(fetchedManga, tracks)
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
updateTracking(fetchedManga, tracks)
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
}
private fun restoreMangaNoFetch(
private suspend fun restoreMangaNoFetch(
source: Source,
backupManga: Manga,
chapters: List<Chapter>,
@ -175,15 +172,13 @@ class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : Abstract
history: List<DHistory>,
tracks: List<Track>
) {
launchIO {
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
updateChapters(source, backupManga, chapters)
}
restoreExtraForManga(backupManga, categories, history, tracks)
updateTracking(backupManga, tracks)
if (!backupManager.restoreChaptersForManga(backupManga, chapters)) {
updateChapters(source, backupManga, chapters)
}
restoreExtraForManga(backupManga, categories, history, tracks)
updateTracking(backupManga, tracks)
}
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {

View File

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterKnownBackupPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
@ -84,6 +85,11 @@ interface ChapterQueries : DbProvider {
.withPutResolver(ChapterBackupPutResolver())
.prepare()
fun updateKnownChaptersBackup(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterKnownBackupPutResolver())
.prepare()
fun updateChapterProgress(chapter: Chapter) = db.put()
.`object`(chapter)
.withPutResolver(ChapterProgressPutResolver())

View File

@ -164,4 +164,14 @@ interface MangaQueries : DbProvider {
.build()
)
.prepare()
fun getChapterFetchDateManga() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(
RawQuery.builder()
.query(getChapterFetchDateMangaQuery())
.observesTables(MangaTable.TABLE)
.build()
)
.prepare()
}

View File

@ -122,6 +122,16 @@ fun getLatestChapterMangaQuery() =
ORDER by max DESC
"""
fun getChapterFetchDateMangaQuery() =
"""
SELECT ${Manga.TABLE}.*, MAX(${Chapter.TABLE}.${Chapter.COL_DATE_FETCH}) AS max
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
ORDER by max DESC
"""
/**
* Query to get the categories for a manga.
*/

View File

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.data.database.resolvers
import androidx.core.content.contentValuesOf
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
class ChapterKnownBackupPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(chapter.id)
.build()
fun mapToContentValues(chapter: Chapter) =
contentValuesOf(
ChapterTable.COL_READ to chapter.read,
ChapterTable.COL_BOOKMARK to chapter.bookmark,
ChapterTable.COL_LAST_PAGE_READ to chapter.last_page_read
)
}

View File

@ -12,6 +12,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.lang.launchIO
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
@ -211,16 +212,16 @@ class DownloadManager(private val context: Context) {
*/
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
val filteredChapters = getChaptersToDelete(chapters)
launchIO {
removeFromDownloadQueue(filteredChapters)
removeFromDownloadQueue(filteredChapters)
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
chapterDirs.forEach { it.delete() }
cache.removeChapters(filteredChapters, manga)
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
chapterDirs.firstOrNull()?.parentFile?.delete()
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
chapterDirs.forEach { it.delete() }
cache.removeChapters(filteredChapters, manga)
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
chapterDirs.firstOrNull()?.parentFile?.delete()
}
}
return filteredChapters
}
@ -249,9 +250,11 @@ class DownloadManager(private val context: Context) {
* @param source the source of the manga.
*/
fun deleteManga(manga: Manga, source: Source) {
downloader.queue.remove(manga)
provider.findMangaDir(manga, source)?.delete()
cache.removeManga(manga)
launchIO {
downloader.queue.remove(manga)
provider.findMangaDir(manga, source)?.delete()
cache.removeManga(manga)
}
}
/**

View File

@ -27,6 +27,9 @@ internal class DownloadNotifier(private val context: Context) {
private val progressNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
setAutoCancel(false)
setOngoing(true)
setOnlyAlertOnce(true)
}
}
@ -84,7 +87,6 @@ internal class DownloadNotifier(private val context: Context) {
// Check if first call.
if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download)
setAutoCancel(false)
clearActions()
// Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
@ -127,7 +129,6 @@ internal class DownloadNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.chapter_paused))
setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_pause_24dp)
setAutoCancel(false)
setProgress(0, 0, false)
clearActions()
// Open download manager when clicked
@ -217,7 +218,6 @@ internal class DownloadNotifier(private val context: Context) {
setContentText(error ?: context.getString(R.string.download_notifier_unknown_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
clearActions()
setAutoCancel(false)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)

View File

@ -53,8 +53,8 @@ class DownloadProvider(private val context: Context) {
return downloadsDir
.createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga))
} catch (e: NullPointerException) {
Timber.w(e)
} catch (e: Throwable) {
Timber.e(e, "Invalid download directory")
throw Exception(context.getString(R.string.invalid_download_dir))
}
}

View File

@ -16,8 +16,8 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
import eu.kanade.tachiyomi.util.lang.RetryWithDelay
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
@ -228,8 +228,8 @@ class Downloader(
* @param chapters the list of chapters to download.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchIO {
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchIO
val wasEmpty = queue.isEmpty()
// Called in background thread, the operation can be slow with SAF.
val chaptersWithoutDir = async {

View File

@ -110,7 +110,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(errorLogIntent)
addAction(
R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log),
context.getString(R.string.action_show_errors),
errorLogIntent
)
}

View File

@ -33,10 +33,10 @@ import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.createFileInCacheDir
import eu.kanade.tachiyomi.util.system.isServiceRunning
import kotlinx.coroutines.CancellationException
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.async
@ -71,6 +71,7 @@ class LibraryUpdateService(
private lateinit var notifier: LibraryUpdateNotifier
private lateinit var ioScope: CoroutineScope
private var mangaToUpdate: List<LibraryManga> = mutableListOf()
private var updateJob: Job? = null
/**
@ -86,6 +87,8 @@ class LibraryUpdateService(
companion object {
private var instance: LibraryUpdateService? = null
/**
* Key for category to update.
*/
@ -116,17 +119,18 @@ class LibraryUpdateService(
* @return true if service newly started, false otherwise
*/
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS): Boolean {
if (!isRunning(context)) {
return if (!isRunning(context)) {
val intent = Intent(context, LibraryUpdateService::class.java).apply {
putExtra(KEY_TARGET, target)
category?.let { putExtra(KEY_CATEGORY, it.id) }
}
ContextCompat.startForegroundService(context, intent)
return true
true
} else {
instance?.addMangaToQueue(category?.id ?: -1, target)
false
}
return false
}
/**
@ -158,11 +162,14 @@ class LibraryUpdateService(
* lock.
*/
override fun onDestroy() {
ioScope?.cancel()
updateJob?.cancel()
ioScope?.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
if (instance == this) {
instance = null
}
super.onDestroy()
}
@ -186,23 +193,25 @@ class LibraryUpdateService(
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
?: return START_NOT_STICKY
// Unsubscribe from any previous subscription if needed.
instance = this
// Unsubscribe from any previous subscription if needed
updateJob?.cancel()
// Update favorite manga. Destroy service when completed or in case of an error.
val selectedScheme = preferences.libraryUpdatePrioritization().get()
val mangaList = getMangaToUpdate(intent, target)
.sortedWith(rankingScheme[selectedScheme])
// Update favorite manga
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
addMangaToQueue(categoryId, target)
// Destroy service when completed or in case of an error.
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
stopSelf(startId)
}
updateJob = ioScope.launch(handler) {
when (target) {
Target.CHAPTERS -> updateChapterList(mangaList)
Target.COVERS -> updateCovers(mangaList)
Target.TRACKING -> updateTrackings(mangaList)
Target.CHAPTERS -> updateChapterList()
Target.COVERS -> updateCovers()
Target.TRACKING -> updateTrackings()
}
}
updateJob?.invokeOnCompletion { stopSelf(startId) }
@ -211,32 +220,41 @@ class LibraryUpdateService(
}
/**
* Returns the list of manga to be updated.
* Adds list of manga to be updated.
*
* @param intent the update intent.
* @param category the ID of the category to update, or -1 if no category specified.
* @param target the target to update.
* @return a list of manga to update
*/
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
fun addMangaToQueue(categoryId: Int, target: Target) {
val libraryManga = db.getLibraryMangas().executeAsBlocking()
var listToUpdate = if (categoryId != -1) {
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
libraryManga.filter { it.category == categoryId }
} else {
val categoriesToUpdate = preferences.libraryUpdateCategories().get().map(String::toInt)
if (categoriesToUpdate.isNotEmpty()) {
db.getLibraryMangas().executeAsBlocking()
.filter { it.category in categoriesToUpdate }
.distinctBy { it.id }
val listToInclude = if (categoriesToUpdate.isNotEmpty()) {
libraryManga.filter { it.category in categoriesToUpdate }
} else {
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
libraryManga
}
val categoriesToExclude = preferences.libraryUpdateCategoriesExclude().get().map(String::toInt)
val listToExclude = if (categoriesToExclude.isNotEmpty()) {
libraryManga.filter { it.category in categoriesToExclude }
} else {
emptyList()
}
listToInclude.minus(listToExclude)
}
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
listToUpdate = listToUpdate.filterNot { it.status == SManga.COMPLETED }
}
return listToUpdate
val selectedScheme = preferences.libraryUpdatePrioritization().get()
mangaToUpdate = listToUpdate
.distinctBy { it.id }
.sortedWith(rankingScheme[selectedScheme])
}
/**
@ -248,54 +266,41 @@ class LibraryUpdateService(
* @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update.
*/
suspend fun updateChapterList(mangaToUpdate: List<LibraryManga>) {
suspend fun updateChapterList() {
val progressCount = AtomicInteger(0)
val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
val failedUpdates = mutableListOf<Pair<Manga, String?>>()
var hasDownloads = false
mangaToUpdate
.map { manga ->
if (updateJob?.isActive != true) {
throw CancellationException()
}
mangaToUpdate.forEach { manga ->
if (updateJob?.isActive != true) {
return
}
// Notify manga that will update.
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
notifier.showProgressNotification(manga, progressCount.andIncrement, mangaToUpdate.size)
// Update the chapters of the manga
try {
val newChapters = updateManga(manga).first
Pair(manga, newChapters)
} catch (e: Throwable) {
// If there's any error, return empty update and continue.
val errorMessage = if (e is NoChaptersException) {
getString(R.string.no_chapters_error)
} else {
e.message
try {
val (newChapters, _) = updateManga(manga)
if (newChapters.isNotEmpty()) {
if (manga.shouldDownloadNewChapters(db, preferences)) {
downloadChapters(manga, newChapters)
hasDownloads = true
}
failedUpdates.add(Pair(manga, errorMessage))
Pair(manga, emptyList())
}
}
// Filter out mangas without new chapters (or failed).
.filter { (_, newChapters) -> newChapters.isNotEmpty() }
.forEach { (manga, newChapters) ->
if (manga.shouldDownloadNewChapters(db, preferences)) {
downloadChapters(manga, newChapters)
hasDownloads = true
}
// Convert to the manga that contains new chapters.
newUpdates.add(
Pair(
manga,
newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray()
)
)
// Convert to the manga that contains new chapters
newUpdates.add(manga to newChapters.sortedByDescending { ch -> ch.source_order }.toTypedArray())
}
} catch (e: Throwable) {
val errorMessage = if (e is NoChaptersException) {
getString(R.string.no_chapters_error)
} else {
e.message
}
failedUpdates.add(manga to errorMessage)
}
}
// Notify result of the overall update.
notifier.cancelProgressNotification()
if (newUpdates.isNotEmpty()) {
@ -334,7 +339,7 @@ class LibraryUpdateService(
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
}
ioScope.launch(handler) {
GlobalScope.launch(Dispatchers.IO + handler) {
val updatedManga = source.getMangaDetails(manga.toMangaInfo())
val sManga = updatedManga.toSManga()
// Avoid "losing" existing cover
@ -355,12 +360,12 @@ class LibraryUpdateService(
return syncChaptersWithSource(db, chapters, manga, source)
}
private suspend fun updateCovers(mangaToUpdate: List<LibraryManga>) {
private suspend fun updateCovers() {
var progressCount = 0
mangaToUpdate.forEach { manga ->
if (updateJob?.isActive != true) {
throw CancellationException()
return
}
notifier.showProgressNotification(manga, progressCount++, mangaToUpdate.size)
@ -388,13 +393,13 @@ class LibraryUpdateService(
* Method that updates the metadata of the connected tracking services. It's called in a
* background thread, so it's safe to do heavy operations or network calls here.
*/
private suspend fun updateTrackings(mangaToUpdate: List<LibraryManga>) {
private suspend fun updateTrackings() {
var progressCount = 0
val loggedServices = trackManager.services.filter { it.isLogged }
mangaToUpdate.forEach { manga ->
if (updateJob?.isActive != true) {
throw CancellationException()
return
}
// Notify manga that will update.

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.notification
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.net.Uri
@ -69,9 +70,10 @@ class NotificationReceiver : BroadcastReceiver() {
)
// Share backup file
ACTION_SHARE_BACKUP ->
shareBackup(
shareFile(
context,
intent.getParcelableExtra(EXTRA_URI),
if (intent.getBooleanExtra(EXTRA_IS_LEGACY_BACKUP, false)) "application/json" else "application/x-protobuf+gzip",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
ACTION_CANCEL_RESTORE -> cancelRestore(
@ -100,6 +102,14 @@ class NotificationReceiver : BroadcastReceiver() {
markAsRead(urls, mangaId)
}
}
// Share crash dump file
ACTION_SHARE_CRASH_LOG ->
shareFile(
context,
intent.getParcelableExtra(EXTRA_URI),
"text/plain",
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
}
}
@ -120,14 +130,13 @@ class NotificationReceiver : BroadcastReceiver() {
* @param notificationId id of notification
*/
private fun shareImage(context: Context, path: String, notificationId: Int) {
// Create intent
val intent = Intent(Intent.ACTION_SEND).apply {
val uri = File(path).getUriCompat(context)
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri(null, uri)
type = "image/*"
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
// Dismiss notification
dismissNotification(context, notificationId)
// Launch share activity
context.startActivity(intent)
@ -140,10 +149,11 @@ class NotificationReceiver : BroadcastReceiver() {
* @param path path of file
* @param notificationId id of notification
*/
private fun shareBackup(context: Context, uri: Uri, notificationId: Int) {
private fun shareFile(context: Context, uri: Uri, fileMimeType: String, notificationId: Int) {
val sendIntent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_STREAM, uri)
type = "application/json"
clipData = ClipData.newRawUri(null, uri)
type = fileMimeType
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}
// Dismiss notification
@ -244,59 +254,34 @@ class NotificationReceiver : BroadcastReceiver() {
companion object {
private const val NAME = "NotificationReceiver"
// Called to launch share intent.
private const val ACTION_SHARE_IMAGE = "$ID.$NAME.SHARE_IMAGE"
// Called to delete image.
private const val ACTION_DELETE_IMAGE = "$ID.$NAME.DELETE_IMAGE"
// Called to launch send intent.
private const val ACTION_SHARE_BACKUP = "$ID.$NAME.SEND_BACKUP"
// Called to cancel backup restore job.
private const val ACTION_SHARE_CRASH_LOG = "$ID.$NAME.SEND_CRASH_LOG"
private const val ACTION_CANCEL_RESTORE = "$ID.$NAME.CANCEL_RESTORE"
// Called to cancel library update.
private const val ACTION_CANCEL_LIBRARY_UPDATE = "$ID.$NAME.CANCEL_LIBRARY_UPDATE"
// Called to mark manga chapters as read.
private const val ACTION_MARK_AS_READ = "$ID.$NAME.MARK_AS_READ"
// Called to open chapter.
private const val ACTION_OPEN_CHAPTER = "$ID.$NAME.ACTION_OPEN_CHAPTER"
// Value containing file location.
private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
// Called to resume downloads.
private const val ACTION_RESUME_DOWNLOADS = "$ID.$NAME.ACTION_RESUME_DOWNLOADS"
// Called to pause downloads.
private const val ACTION_PAUSE_DOWNLOADS = "$ID.$NAME.ACTION_PAUSE_DOWNLOADS"
// Called to clear downloads.
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
// Called to dismiss notification.
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
// Value containing uri.
private const val EXTRA_FILE_LOCATION = "$ID.$NAME.FILE_LOCATION"
private const val EXTRA_URI = "$ID.$NAME.URI"
// Value containing notification id.
private const val EXTRA_NOTIFICATION_ID = "$ID.$NAME.NOTIFICATION_ID"
// Value containing group id.
private const val EXTRA_GROUP_ID = "$ID.$NAME.EXTRA_GROUP_ID"
// Value containing manga id.
private const val EXTRA_MANGA_ID = "$ID.$NAME.EXTRA_MANGA_ID"
// Value containing chapter id.
private const val EXTRA_CHAPTER_ID = "$ID.$NAME.EXTRA_CHAPTER_ID"
// Value containing chapter url.
private const val EXTRA_CHAPTER_URL = "$ID.$NAME.EXTRA_CHAPTER_URL"
private const val EXTRA_IS_LEGACY_BACKUP = "$ID.$NAME.EXTRA_IS_LEGACY_BACKUP"
/**
* Returns a [PendingIntent] that resumes the download of a chapter
@ -509,10 +494,11 @@ class NotificationReceiver : BroadcastReceiver() {
* @param notificationId id of notification
* @return [PendingIntent]
*/
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
internal fun shareBackupPendingBroadcast(context: Context, uri: Uri, isLegacyFormat: Boolean, notificationId: Int): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_SHARE_BACKUP
putExtra(EXTRA_URI, uri)
putExtra(EXTRA_IS_LEGACY_BACKUP, isLegacyFormat)
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
@ -534,6 +520,23 @@ class NotificationReceiver : BroadcastReceiver() {
return PendingIntent.getActivity(context, 0, intent, 0)
}
/**
* Returns [PendingIntent] that starts a share activity for a crash log dump file.
*
* @param context context of application
* @param uri uri of file
* @param notificationId id of notification
* @return [PendingIntent]
*/
internal fun shareCrashLogPendingBroadcast(context: Context, uri: Uri, notificationId: Int): PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_SHARE_CRASH_LOG
putExtra(EXTRA_URI, uri)
putExtra(EXTRA_NOTIFICATION_ID, notificationId)
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/**
* Returns [PendingIntent] that cancels a backup restore job.
*

View File

@ -23,6 +23,14 @@ object PreferenceKeys {
const val showPageNumber = "pref_show_page_number_key"
const val dualPageSplitPaged = "pref_dual_page_split"
const val dualPageSplitWebtoon = "pref_dual_page_split_webtoon"
const val dualPageInvertPaged = "pref_dual_page_invert"
const val dualPageInvertWebtoon = "pref_dual_page_invert_webtoon"
const val showReadingMode = "pref_show_reading_mode"
const val trueColor = "pref_true_color_key"
@ -71,6 +79,10 @@ object PreferenceKeys {
const val navigationModeWebtoon = "reader_navigation_mode_webtoon"
const val showNavigationOverlayNewUser = "reader_navigation_overlay_new_user"
const val showNavigationOverlayOnStart = "reader_navigation_overlay_on_start"
const val webtoonSidePadding = "webtoon_side_padding"
const val portraitColumns = "pref_library_columns_portrait_key"
@ -112,6 +124,7 @@ object PreferenceKeys {
const val libraryUpdateRestriction = "library_update_restriction"
const val libraryUpdateCategories = "library_update_categories"
const val libraryUpdateCategoriesExclude = "library_update_categories_exclude"
const val libraryUpdatePrioritization = "library_update_prioritization"
@ -152,6 +165,7 @@ object PreferenceKeys {
const val downloadNew = "download_new"
const val downloadNewCategories = "download_new_categories"
const val downloadNewCategoriesExclude = "download_new_categories_exclude"
const val libraryDisplayMode = "pref_display_mode_library"
@ -177,7 +191,7 @@ object PreferenceKeys {
const val searchPinnedSourcesOnly = "search_pinned_sources_only"
const val enableDoh = "enable_doh"
const val dohProvider = "doh_provider"
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"

View File

@ -5,6 +5,8 @@ package eu.kanade.tachiyomi.data.preference
*/
object PreferenceValues {
/* ktlint-disable experimental:enum-entry-name-case */
// Keys are lowercase to match legacy string values
enum class ThemeMode {
light,
@ -25,6 +27,8 @@ object PreferenceValues {
amoled,
}
/* ktlint-enable experimental:enum-entry-name-case */
enum class DisplayMode {
COMPACT_GRID,
COMFORTABLE_GRID,

View File

@ -22,7 +22,7 @@ import java.util.Locale
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
fun <T> Preference<T>.asImmediateFlow(block: (T) -> Unit): Flow<T> {
block(get())
return asFlow()
.onEach { block(it) }
@ -36,6 +36,10 @@ operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
set(get() - item)
}
fun Preference<Boolean>.toggle() {
set(!get())
}
class PreferencesHelper(val context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
@ -89,6 +93,14 @@ class PreferencesHelper(val context: Context) {
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
fun dualPageSplitPaged() = flowPrefs.getBoolean(Keys.dualPageSplitPaged, false)
fun dualPageSplitWebtoon() = flowPrefs.getBoolean(Keys.dualPageSplitWebtoon, false)
fun dualPageInvertPaged() = flowPrefs.getBoolean(Keys.dualPageInvertPaged, false)
fun dualPageInvertWebtoon() = flowPrefs.getBoolean(Keys.dualPageInvertWebtoon, false)
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
@ -141,6 +153,10 @@ class PreferencesHelper(val context: Context) {
fun navigationModeWebtoon() = flowPrefs.getInt(Keys.navigationModeWebtoon, 0)
fun showNavigationOverlayNewUser() = flowPrefs.getBoolean(Keys.showNavigationOverlayNewUser, true)
fun showNavigationOverlayOnStart() = flowPrefs.getBoolean(Keys.showNavigationOverlayOnStart, false)
fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
@ -202,6 +218,7 @@ class PreferencesHelper(val context: Context) {
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
fun libraryUpdateCategories() = flowPrefs.getStringSet(Keys.libraryUpdateCategories, emptySet())
fun libraryUpdateCategoriesExclude() = flowPrefs.getStringSet(Keys.libraryUpdateCategoriesExclude, emptySet())
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
@ -248,6 +265,7 @@ class PreferencesHelper(val context: Context) {
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
fun downloadNewCategories() = flowPrefs.getStringSet(Keys.downloadNewCategories, emptySet())
fun downloadNewCategoriesExclude() = flowPrefs.getStringSet(Keys.downloadNewCategoriesExclude, emptySet())
fun lang() = prefs.getString(Keys.lang, "")
@ -261,7 +279,7 @@ class PreferencesHelper(val context: Context) {
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false)
fun dohProvider() = prefs.getInt(Keys.dohProvider, -1)
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")

View File

@ -35,6 +35,8 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
private val api by lazy { AnilistApi(client, interceptor) }
override val supportsReadingDates: Boolean = true
private val scorePreference = preferences.anilistScoreType()
init {

View File

@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri
import androidx.core.net.toUri
import com.afollestad.date.dayOfMonth
import com.afollestad.date.month
import com.afollestad.date.year
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST
@ -9,6 +12,7 @@ import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.jsonMime
import eu.kanade.tachiyomi.network.parseAs
import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.json.JsonNull
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
@ -30,8 +34,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun addLibManga(track: Track): Track {
return withIOContext {
val query =
"""
val query = """
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
| id
@ -65,10 +68,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun updateLibManga(track: Track): Track {
return withIOContext {
val query =
"""
|mutation UpdateManga(${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}score: Int) {
|SaveMediaListEntry (id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, scoreRaw: ${'$'}score) {
val query = """
|mutation UpdateManga(
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus,
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|) {
|SaveMediaListEntry(
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|) {
|id
|status
|progress
@ -82,6 +90,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("progress", track.last_chapter_read)
put("status", track.toAnilistStatus())
put("score", track.score.toInt())
put("startedAt", createDate(track.started_reading_date))
put("completedAt", createDate(track.finished_reading_date))
}
}
authClient.newCall(POST(apiUrl, body = payload.toString().toRequestBody(jsonMime)))
@ -92,8 +102,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun search(search: String): List<TrackSearch> {
return withIOContext {
val query =
"""
val query = """
|query Search(${'$'}query: String) {
|Page (perPage: 50) {
|media(search: ${'$'}query, type: MANGA, format_not_in: [NOVEL]) {
@ -143,8 +152,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun findLibManga(track: Track, userid: Int): Track? {
return withIOContext {
val query =
"""
val query = """
|query (${'$'}id: Int!, ${'$'}manga_id: Int!) {
|Page {
|mediaList(userId: ${'$'}id, type: MANGA, mediaId: ${'$'}manga_id) {
@ -152,6 +160,16 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|status
|scoreRaw: score(format: POINT_100)
|progress
|startedAt {
|year
|month
|day
|}
|completedAt {
|year
|month
|day
|}
|media {
|id
|title {
@ -209,8 +227,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun getCurrentUser(): Pair<Int, String> {
return withIOContext {
val query =
"""
val query = """
|query User {
|Viewer {
|id
@ -243,21 +260,6 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
private fun jsonToALManga(struct: JsonObject): ALManga {
val date = try {
val date = Calendar.getInstance()
date.set(
struct["startDate"]!!.jsonObject["year"]!!.jsonPrimitive.intOrNull ?: 0,
(
struct["startDate"]!!.jsonObject["month"]!!.jsonPrimitive.intOrNull
?: 0
) - 1,
struct["startDate"]!!.jsonObject["day"]!!.jsonPrimitive.intOrNull ?: 0
)
date.timeInMillis
} catch (_: Exception) {
0L
}
return ALManga(
struct["id"]!!.jsonPrimitive.int,
struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content,
@ -265,7 +267,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
struct["description"]!!.jsonPrimitive.contentOrNull,
struct["type"]!!.jsonPrimitive.content,
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
date,
parseDate(struct, "startDate"),
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
)
}
@ -276,10 +278,44 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
struct["status"]!!.jsonPrimitive.content,
struct["scoreRaw"]!!.jsonPrimitive.int,
struct["progress"]!!.jsonPrimitive.int,
parseDate(struct, "startedAt"),
parseDate(struct, "completedAt"),
jsonToALManga(struct["media"]!!.jsonObject)
)
}
private fun parseDate(struct: JsonObject, dateKey: String): Long {
return try {
val date = Calendar.getInstance()
date.set(
struct[dateKey]!!.jsonObject["year"]!!.jsonPrimitive.int,
struct[dateKey]!!.jsonObject["month"]!!.jsonPrimitive.int - 1,
struct[dateKey]!!.jsonObject["day"]!!.jsonPrimitive.int
)
date.timeInMillis
} catch (_: Exception) {
0L
}
}
private fun createDate(dateValue: Long): JsonObject {
if (dateValue == 0L) {
return buildJsonObject {
put("year", JsonNull)
put("month", JsonNull)
put("day", JsonNull)
}
}
val calendar = Calendar.getInstance()
calendar.timeInMillis = dateValue
return buildJsonObject {
put("year", calendar.year)
put("month", calendar.month + 1)
put("day", calendar.dayOfMonth)
}
}
companion object {
private const val clientId = "385"
private const val apiUrl = "https://graphql.anilist.co/"

View File

@ -44,6 +44,8 @@ data class ALUserManga(
val list_status: String,
val score_raw: Int,
val chapters_read: Int,
val start_date_fuzzy: Long,
val completed_date_fuzzy: Long,
val manga: ALManga
) {
@ -51,6 +53,8 @@ data class ALUserManga(
media_id = manga.media_id
status = toTrackStatus()
score = score_raw.toFloat()
started_reading_date = start_date_fuzzy
finished_reading_date = completed_date_fuzzy
last_chapter_read = chapters_read
library_id = this@ALUserManga.library_id
total_chapters = manga.total_chapters

View File

@ -45,8 +45,10 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
return if (remoteTrack != null && statusTrack != null) {
track.copyPersonalFrom(remoteTrack)
track.library_id = remoteTrack.library_id
track.status = remoteTrack.status
track.last_chapter_read = remoteTrack.last_chapter_read
track.status = statusTrack.status
track.score = statusTrack.score
track.last_chapter_read = statusTrack.last_chapter_read
track.total_chapters = remoteTrack.total_chapters
refresh(track)
} else {
// Set default fields if it's not found in the list
@ -66,7 +68,6 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
track.copyPersonalFrom(remoteStatusTrack!!)
api.findLibManga(track)?.let { remoteTrack ->
track.total_chapters = remoteTrack.total_chapters
track.status = remoteTrack.status
}
return track
}

View File

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.util.lang.withIOContext
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
@ -46,6 +47,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
return withIOContext {
// read status update
val sbody = FormBody.Builder()
.add("rating", track.score.toInt().toString())
.add("status", track.toBangumiStatus())
.build()
authClient.newCall(POST("$apiUrl/collection/${track.media_id}/update", body = sbody))
@ -91,12 +93,24 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
}
private fun jsonToSearch(obj: JsonObject): TrackSearch {
val coverUrl = if (obj["images"] is JsonObject) {
obj["images"]?.jsonObject?.get("common")?.jsonPrimitive?.contentOrNull ?: ""
} else {
// Sometimes JsonNull
""
}
val totalChapters = if (obj["eps_count"] != null) {
obj["eps_count"]!!.jsonPrimitive.int
} else {
0
}
return TrackSearch.create(TrackManager.BANGUMI).apply {
media_id = obj["id"]!!.jsonPrimitive.int
title = obj["name_cn"]!!.jsonPrimitive.content
cover_url = obj["images"]!!.jsonObject["common"]!!.jsonPrimitive.content
cover_url = coverUrl
summary = obj["name"]!!.jsonPrimitive.content
tracking_url = obj["url"]!!.jsonPrimitive.content
total_chapters = totalChapters
}
}
@ -119,14 +133,21 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.build()
// TODO: get user readed chapter here
authClient.newCall(requestUserRead)
.await()
.parseAs<Collection>()
.let {
var response = authClient.newCall(requestUserRead).await()
var responseBody = response.body?.string().orEmpty()
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
if (responseBody.contains("\"code\":400")) {
null
} else {
json.decodeFromString<Collection>(responseBody).let {
track.status = it.status?.id!!
track.last_chapter_read = it.ep_status!!
track.score = it.rating!!
track
}
}
}
}

View File

@ -8,7 +8,7 @@ data class Collection(
val comment: String? = "",
val ep_status: Int? = 0,
val lasttouch: Int? = 0,
val rating: Int? = 0,
val rating: Float? = 0f,
val status: Status? = Status(),
val tag: List<String?>? = listOf(),
val user: User? = User(),

View File

@ -18,6 +18,7 @@ sealed class Extension {
override val versionCode: Int,
override val lang: String,
override val isNsfw: Boolean,
val pkgFactory: String?,
val sources: List<Source>,
val hasUpdate: Boolean = false,
val isObsolete: Boolean = false,

View File

@ -31,6 +31,7 @@ internal object ExtensionLoader {
private const val EXTENSION_FEATURE = "tachiyomi.extension"
private const val METADATA_SOURCE_CLASS = "tachiyomi.extension.class"
private const val METADATA_SOURCE_FACTORY = "tachiyomi.extension.factory"
private const val METADATA_NSFW = "tachiyomi.extension.nsfw"
const val LIB_VERSION_MIN = 1.2
const val LIB_VERSION_MAX = 1.2
@ -162,7 +163,7 @@ internal object ExtensionLoader {
else -> throw Exception("Unknown source class type! ${obj.javaClass}")
}
} catch (e: Throwable) {
Timber.e(e, "Extension load error: $extName.")
Timber.w(e, "Extension load error: $extName ($it)")
return LoadResult.Error(e)
}
}
@ -184,7 +185,8 @@ internal object ExtensionLoader {
versionCode,
lang,
isNsfw,
sources,
sources = sources,
pkgFactory = appInfo.metaData.getString(METADATA_SOURCE_FACTORY),
isUnofficial = signatureHash != officialSignature
)
return LoadResult.Success(extension)

View File

@ -9,6 +9,7 @@ import android.webkit.WebSettings
import android.webkit.WebView
import android.widget.Toast
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.WebViewClientCompat
import eu.kanade.tachiyomi.util.system.WebViewUtil
@ -98,7 +99,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
// Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
webview.settings.userAgentString = request.header("User-Agent")
?: WebViewUtil.DEFAULT_USER_AGENT
?: HttpSource.DEFAULT_USER_AGENT
webview.webViewClient = object : WebViewClientCompat() {
override fun onPageFinished(view: WebView, url: String) {
@ -170,6 +171,6 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
companion object {
private val SERVER_CHECK = arrayOf("cloudflare-nginx", "cloudflare")
private val COOKIE_NAMES = listOf("__cfduid", "cf_clearance")
private val COOKIE_NAMES = listOf("cf_clearance")
}
}

View File

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.network
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import java.net.InetAddress
/**
* Based on https://github.com/square/okhttp/blob/ef5d0c83f7bbd3a0c0534e7ca23cbc4ee7550f3b/okhttp-dnsoverhttps/src/test/java/okhttp3/dnsoverhttps/DohProviders.java
*/
const val PREF_DOH_CLOUDFLARE = 1
const val PREF_DOH_GOOGLE = 2
fun OkHttpClient.Builder.dohCloudflare() = dns(
DnsOverHttps.Builder().client(build())
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")
)
.build()
)
fun OkHttpClient.Builder.dohGoogle() = dns(
DnsOverHttps.Builder().client(build())
.url("https://dns.google/dns-query".toHttpUrl())
.bootstrapDnsHosts(
InetAddress.getByName("8.8.4.4"),
InetAddress.getByName("8.8.8.8")
)
.build()
)

View File

@ -4,13 +4,10 @@ import android.content.Context
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import okhttp3.logging.HttpLoggingInterceptor
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.net.InetAddress
import java.util.concurrent.TimeUnit
class NetworkHelper(context: Context) {
@ -38,25 +35,9 @@ class NetworkHelper(context: Context) {
builder.addInterceptor(httpLoggingInterceptor)
}
if (preferences.enableDoh()) {
builder.dns(
DnsOverHttps.Builder().client(builder.build())
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
listOf(
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")
)
)
.build()
)
when (preferences.dohProvider()) {
PREF_DOH_CLOUDFLARE -> builder.dohCloudflare()
PREF_DOH_GOOGLE -> builder.dohGoogle()
}
builder.build()

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.network
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.source.online.HttpSource
import okhttp3.Interceptor
import okhttp3.Response
@ -12,7 +12,7 @@ class UserAgentInterceptor : Interceptor {
val newRequest = originalRequest
.newBuilder()
.removeHeader("User-Agent")
.addHeader("User-Agent", WebViewUtil.DEFAULT_USER_AGENT)
.addHeader("User-Agent", HttpSource.DEFAULT_USER_AGENT)
.build()
chain.proceed(newRequest)
} else {

View File

@ -27,7 +27,7 @@ import java.util.zip.ZipFile
class LocalSource(private val context: Context) : CatalogueSource {
companion object {
const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
const val HELP_URL = "https://tachiyomi.org/help/guides/local-manga/"
private const val COVER_NAME = "cover.jpg"
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")

View File

@ -10,7 +10,6 @@ import eu.kanade.tachiyomi.source.model.MangasPage
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.system.WebViewUtil
import okhttp3.Headers
import okhttp3.OkHttpClient
import okhttp3.Request
@ -75,7 +74,7 @@ abstract class HttpSource : CatalogueSource {
* Headers builder for requests. Implementations can override this method for custom headers.
*/
protected open fun headersBuilder() = Headers.Builder().apply {
add("User-Agent", WebViewUtil.DEFAULT_USER_AGENT)
add("User-Agent", DEFAULT_USER_AGENT)
}
/**
@ -370,4 +369,8 @@ abstract class HttpSource : CatalogueSource {
* Returns the list of filters for the source.
*/
override fun getFilterList() = FilterList()
companion object {
const val DEFAULT_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/88.0.4324.150 Safari/537.36 Edg/88.0.705.63"
}
}

View File

@ -13,7 +13,7 @@ abstract class BaseThemedActivity : AppCompatActivity() {
val preferences: PreferencesHelper by injectLazy()
private val isDarkMode: Boolean by lazy {
val isDarkMode: Boolean by lazy {
val themeMode = preferences.themeMode().get()
(themeMode == Values.ThemeMode.dark) ||
(

View File

@ -11,15 +11,13 @@ import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RestoreViewOnCreateController
import kotlinx.android.extensions.LayoutContainer
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.cancel
import timber.log.Timber
abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
RestoreViewOnCreateController(bundle),
LayoutContainer {
RestoreViewOnCreateController(bundle) {
lateinit var binding: VB
@ -53,9 +51,6 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
)
}
override val containerView: View?
get() = view
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
return inflateView(inflater, container)
}
@ -126,7 +121,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
* [expandActionViewFromInteraction] should be set to true in [onOptionsItemSelected] when the expandable item is selected
* This method should be called as part of [MenuItem.OnActionExpandListener.onMenuItemActionExpand]
*/
fun invalidateMenuOnExpand(): Boolean {
open fun invalidateMenuOnExpand(): Boolean {
return if (expandActionViewFromInteraction) {
activity?.invalidateOptionsMenu()
false

View File

@ -1,11 +1,14 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.content.Intent
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import eu.kanade.tachiyomi.util.system.toast
fun Router.popControllerWithTag(tag: String): Boolean {
val controller = getControllerWithTag(tag)
@ -32,3 +35,12 @@ fun Controller.withFadeTransaction(): RouterTransaction {
.pushChangeHandler(OneWayFadeChangeHandler())
.popChangeHandler(OneWayFadeChangeHandler())
}
fun Controller.openInBrowser(url: String) {
try {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
} catch (e: Throwable) {
activity?.toast(e.message)
}
}

View File

@ -0,0 +1,196 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.app.Activity
import android.os.Bundle
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import androidx.annotation.StringRes
import androidx.appcompat.widget.SearchView
import androidx.viewbinding.ViewBinding
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
/**
* Implementation of the NucleusController that has a built-in ViewSearch
*/
abstract class SearchableNucleusController<VB : ViewBinding, P : BasePresenter<*>>
(bundle: Bundle? = null) : NucleusController<VB, P>(bundle) {
enum class SearchViewState { LOADING, LOADED, COLLAPSING, FOCUSED }
/**
* Used to bypass the initial searchView being set to empty string after an onResume
*/
private var currentSearchViewState: SearchViewState = SearchViewState.LOADING
/**
* Store the query text that has not been submitted to reassign it after an onResume, UI-only
*/
protected var nonSubmittedQuery: String = ""
/**
* To be called by classes that extend this subclass in onCreateOptionsMenu
*/
protected fun createOptionsMenu(
menu: Menu,
inflater: MenuInflater,
menuId: Int,
searchItemId: Int,
@StringRes queryHint: Int? = null,
restoreCurrentQuery: Boolean = true
) {
// Inflate menu
inflater.inflate(menuId, menu)
// Initialize search option.
val searchItem = menu.findItem(searchItemId)
val searchView = searchItem.actionView as SearchView
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
searchView.maxWidth = Int.MAX_VALUE
searchView.queryTextEvents()
.onEach {
val newText = it.queryText.toString()
if (newText.isNotBlank() or acceptEmptyQuery()) {
if (it is QueryTextEvent.QuerySubmitted) {
// Abstract function for implementation
// Run it first in case the old query data is needed (like BrowseSourceController)
onSearchViewQueryTextSubmit(newText)
presenter.query = newText
nonSubmittedQuery = ""
} else if ((it is QueryTextEvent.QueryChanged) && (presenter.query != newText)) {
nonSubmittedQuery = newText
// Abstract function for implementation
onSearchViewQueryTextChange(newText)
}
}
// clear the collapsing flag
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.COLLAPSING)
}
.launchIn(viewScope)
val query = presenter.query
// Restoring a query the user had not submitted
if (nonSubmittedQuery.isNotBlank() and (nonSubmittedQuery != query)) {
searchItem.expandActionView()
searchView.setQuery(nonSubmittedQuery, false)
onSearchViewQueryTextChange(nonSubmittedQuery)
} else {
if (queryHint != null) {
searchView.queryHint = applicationContext?.getString(queryHint)
}
if (restoreCurrentQuery) {
// Restoring a query the user had submitted
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
onSearchViewQueryTextChange(query)
onSearchViewQueryTextSubmit(query)
}
}
}
// Workaround for weird behavior where searchView gets empty text change despite
// query being set already, prevents the query from being cleared
binding.root.post {
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.LOADING)
}
searchView.setOnQueryTextFocusChangeListener { _, hasFocus ->
if (hasFocus) {
setCurrentSearchViewState(SearchViewState.FOCUSED)
} else {
setCurrentSearchViewState(SearchViewState.LOADED, SearchViewState.FOCUSED)
}
}
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
onSearchMenuItemActionExpand(item)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
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 (localSearchView.toString().isNotBlank()) {
setCurrentSearchViewState(SearchViewState.COLLAPSING)
}
onSearchMenuItemActionCollapse(item)
return true
}
}
)
}
override fun onActivityResumed(activity: Activity) {
super.onActivityResumed(activity)
// Until everything is up and running don't accept empty queries
setCurrentSearchViewState(SearchViewState.LOADING)
}
private fun acceptEmptyQuery(): Boolean {
return when (currentSearchViewState) {
SearchViewState.COLLAPSING, SearchViewState.FOCUSED -> true
else -> false
}
}
private fun setCurrentSearchViewState(to: SearchViewState, from: SearchViewState? = null) {
// When loading ignore all requests other than loaded
if ((currentSearchViewState == SearchViewState.LOADING) && (to != SearchViewState.LOADED)) {
return
}
// Prevent changing back to an unwanted state when using async flows (ie onFocus event doing
// COLLAPSING -> LOADED)
if ((from != null) && (currentSearchViewState != from)) {
return
}
currentSearchViewState = to
}
/**
* Called by the SearchView since since the implementation of these can vary in subclasses
* Not abstract as they are optional
*/
protected open fun onSearchViewQueryTextChange(newText: String?) {
}
protected open fun onSearchViewQueryTextSubmit(query: String?) {
}
protected open fun onSearchMenuItemActionExpand(item: MenuItem?) {
}
protected open fun onSearchMenuItemActionCollapse(item: MenuItem?) {
}
/**
* During the conversion to SearchableNucleusController (after which I plan to merge its code
* into BaseController) this addresses an issue where the searchView.onTextFocus event is not
* triggered
*/
override fun invalidateMenuOnExpand(): Boolean {
return if (expandActionViewFromInteraction) {
activity?.invalidateOptionsMenu()
setCurrentSearchViewState(SearchViewState.FOCUSED) // we are technically focused here
false
} else {
true
}
}
}

View File

@ -1,11 +0,0 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import kotlinx.android.extensions.LayoutContainer
abstract class BaseViewHolder(view: View) : RecyclerView.ViewHolder(view), LayoutContainer {
override val containerView: View?
get() = itemView
}

View File

@ -12,6 +12,11 @@ open class BasePresenter<V> : RxPresenter<V>() {
lateinit var presenterScope: CoroutineScope
/**
* Query from the view where applicable
*/
var query: String = ""
override fun onCreate(savedState: Bundle?) {
try {
super.onCreate(savedState)

View File

@ -10,6 +10,7 @@ import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
@ -58,6 +59,11 @@ open class ExtensionController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = ExtensionControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
@ -104,6 +110,8 @@ open class ExtensionController :
override fun onButtonClick(position: Int) {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return
when (extension) {
is Extension.Available -> presenter.installExtension(extension)
is Extension.Untrusted -> openTrustDialog(extension)
is Extension.Installed -> {
if (!extension.hasUpdate) {
openDetails(extension)
@ -111,12 +119,6 @@ open class ExtensionController :
presenter.updateExtension(extension)
}
}
is Extension.Available -> {
presenter.installExtension(extension)
}
is Extension.Untrusted -> {
openTrustDialog(extension)
}
}
}
@ -147,12 +149,11 @@ open class ExtensionController :
override fun onItemClick(view: View, position: Int): Boolean {
val extension = (adapter?.getItem(position) as? ExtensionItem)?.extension ?: return false
if (extension is Extension.Installed) {
openDetails(extension)
} else if (extension is Extension.Untrusted) {
openTrustDialog(extension)
when (extension) {
is Extension.Available -> presenter.installExtension(extension)
is Extension.Untrusted -> openTrustDialog(extension)
is Extension.Installed -> openDetails(extension)
}
return false
}

View File

@ -14,7 +14,6 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.preference.Preference
import androidx.preference.PreferenceGroupAdapter
@ -23,6 +22,7 @@ import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
@ -36,6 +36,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.ToolbarLiftOnScrollController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.preference.DSL
import eu.kanade.tachiyomi.util.preference.onChange
@ -67,6 +68,11 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = ExtensionDetailControllerBinding.inflate(themedInflater)
binding.extensionPrefsRecycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
@ -208,9 +214,12 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
private fun openCommitHistory() {
val pkgName = presenter.extension!!.pkgName.substringAfter("eu.kanade.tachiyomi.extension.")
val url = "https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master/src/${pkgName.replace(".", "/")}"
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
val pkgFactory = presenter.extension!!.pkgFactory
val url = when {
!pkgFactory.isNullOrEmpty() -> "$URL_EXTENSION_COMMITS/multisrc/src/main/java/eu/kanade/tachiyomi/multisrc/$pkgFactory"
else -> "$URL_EXTENSION_COMMITS/src/${pkgName.replace(".", "/")}"
}
openInBrowser(url)
}
private fun openInSettings() {
@ -232,5 +241,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
private companion object {
const val PKGNAME_KEY = "pkg_name"
private const val URL_EXTENSION_COMMITS = "https://github.com/tachiyomiorg/tachiyomi-extensions/commits/master"
}
}

View File

@ -4,10 +4,10 @@ import android.annotation.SuppressLint
import android.content.Context
import android.os.Bundle
import android.util.TypedValue
import android.view.ContextThemeWrapper
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.core.os.bundleOf
import androidx.preference.DialogPreference
import androidx.preference.EditTextPreference

View File

@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
class MigrationMangaAdapter(controller: MigrationMangaController) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val coverClickListener: OnCoverClickListener = controller
interface OnCoverClickListener {
fun onCoverClick(position: Int)
}
}

View File

@ -7,17 +7,18 @@ import android.view.ViewGroup
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.databinding.MigrationMangaControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
import eu.kanade.tachiyomi.ui.manga.MangaController
class MigrationMangaController :
NucleusController<MigrationMangaControllerBinding, MigrationMangaPresenter>,
FlexibleAdapter.OnItemClickListener {
FlexibleAdapter.OnItemClickListener,
MigrationMangaAdapter.OnCoverClickListener {
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
private var adapter: MigrationMangaAdapter? = null
constructor(sourceId: Long, sourceName: String?) : super(
bundleOf(
@ -51,7 +52,7 @@ class MigrationMangaController :
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = FlexibleAdapter<IFlexible<*>>(null, this)
adapter = MigrationMangaAdapter(this)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = adapter
adapter?.fastScroller = binding.fastScroller
@ -62,17 +63,22 @@ class MigrationMangaController :
super.onDestroyView(view)
}
fun setManga(manga: List<MangaItem>) {
fun setManga(manga: List<MigrationMangaItem>) {
adapter?.updateDataSet(manga)
}
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? MangaItem ?: return false
val item = adapter?.getItem(position) as? MigrationMangaItem ?: return false
val controller = SearchController(item.manga)
router.pushController(controller.withFadeTransaction())
return false
}
override fun onCoverClick(position: Int) {
val mangaItem = adapter?.getItem(position) as? MigrationMangaItem ?: return
router.pushController(MangaController(mangaItem.manga).withFadeTransaction())
}
companion object {
const val SOURCE_ID_EXTRA = "source_id_extra"
const val SOURCE_NAME_EXTRA = "source_name_extra"

View File

@ -5,29 +5,27 @@ import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.databinding.SourceListItemBinding
class MangaHolder(
class MigrationMangaHolder(
view: View,
adapter: FlexibleAdapter<*>
private val adapter: MigrationMangaAdapter
) : FlexibleViewHolder(view, adapter) {
private val binding = SourceListItemBinding.bind(view)
fun bind(item: MangaItem) {
// Update the title of the manga.
binding.title.text = item.manga.title
// Create thumbnail onclick to simulate long click
init {
binding.thumbnail.setOnClickListener {
// Simulate long click on this view to enter selection mode
onLongClick(itemView)
adapter.coverClickListener.onCoverClick(bindingAdapterPosition)
}
}
fun bind(item: MigrationMangaItem) {
binding.title.text = item.manga.title
// Update the cover.
GlideApp.with(itemView.context).clear(binding.thumbnail)

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.os.Parcelable
import android.view.View
import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter
@ -8,25 +7,20 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import kotlinx.parcelize.Parcelize
@Parcelize
class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>(), Parcelable {
class MigrationMangaItem(val manga: Manga) : AbstractFlexibleItem<MigrationMangaHolder>() {
override fun getLayoutRes(): Int {
return R.layout.source_list_item
}
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MangaHolder {
return MangaHolder(
view,
adapter
)
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MigrationMangaHolder {
return MigrationMangaHolder(view, adapter as MigrationMangaAdapter)
}
override fun bindViewHolder(
adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
holder: MangaHolder,
holder: MigrationMangaHolder,
position: Int,
payloads: List<Any?>?
) {
@ -34,7 +28,7 @@ class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>(), Parcela
}
override fun equals(other: Any?): Boolean {
if (other is MangaItem) {
if (other is MigrationMangaItem) {
return manga.id == other.manga.id
}
return false

View File

@ -23,9 +23,9 @@ class MigrationMangaPresenter(
.subscribeLatestCache(MigrationMangaController::setManga)
}
private fun libraryToMigrationItem(library: List<Manga>): List<MangaItem> {
private fun libraryToMigrationItem(library: List<Manga>): List<MigrationMangaItem> {
return library.filter { it.source == sourceId }
.sortedBy { it.title }
.map { MangaItem(it) }
.map { MigrationMangaItem(it) }
}
}

View File

@ -5,10 +5,13 @@ import android.os.Bundle
import androidx.core.view.isVisible
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItemsMultiChoice
import com.bluelinelabs.conductor.Controller
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
@ -39,16 +42,16 @@ class SearchController(
newManga = savedInstanceState.getSerializable(::newManga.name) as? Manga
}
fun migrateManga() {
val manga = manga ?: return
val newManga = newManga ?: return
fun migrateManga(manga: Manga? = null, newManga: Manga?) {
manga ?: return
newManga ?: return
(presenter as? SearchPresenter)?.migrateManga(manga, newManga, true)
}
fun copyManga() {
val manga = manga ?: return
val newManga = newManga ?: return
fun copyManga(manga: Manga? = null, newManga: Manga?) {
manga ?: return
newManga ?: return
(presenter as? SearchPresenter)?.migrateManga(manga, newManga, false)
}
@ -56,7 +59,7 @@ class SearchController(
override fun onMangaClick(manga: Manga) {
newManga = manga
val dialog =
MigrationDialog()
MigrationDialog(this.manga, newManga, this)
dialog.targetController = this
dialog.showDialog(router)
}
@ -75,7 +78,7 @@ class SearchController(
}
}
class MigrationDialog : DialogController() {
class MigrationDialog(private val manga: Manga? = null, private val newManga: Manga? = null, private val callingController: Controller? = null) : DialogController() {
private val preferences: PreferencesHelper by injectLazy()
@ -88,7 +91,7 @@ class SearchController(
)
return MaterialDialog(activity!!)
.message(R.string.migration_dialog_what_to_include)
.title(R.string.migration_dialog_what_to_include)
.listItemsMultiChoice(
items = MigrationFlags.titles.map { resources?.getString(it) as CharSequence },
initialSelection = preselected.toIntArray()
@ -101,12 +104,28 @@ class SearchController(
preferences.migrateFlags().set(newValue)
}
.positiveButton(R.string.migrate) {
(targetController as? SearchController)?.migrateManga()
if (callingController != null) {
if (callingController.javaClass == SourceSearchController::class.java) {
router.popController(callingController)
}
}
(targetController as? SearchController)?.migrateManga(manga, newManga)
}
.negativeButton(R.string.copy) {
(targetController as? SearchController)?.copyManga()
if (callingController != null) {
if (callingController.javaClass == SourceSearchController::class.java) {
router.popController(callingController)
}
}
(targetController as? SearchController)?.copyManga(manga, newManga)
}
.neutralButton(android.R.string.cancel)
}
}
override fun onTitleClick(source: CatalogueSource) {
presenter.preferences.lastUsedSource().set(source.id)
router.pushController(SourceSearchController(manga, source, presenter.query).withFadeTransaction())
}
}

View File

@ -17,6 +17,8 @@ import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.toast
import java.util.Date
class SearchPresenter(
@ -56,11 +58,15 @@ class SearchPresenter(
replacingMangaRelay.call(true)
presenterScope.launchIO {
val chapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() }
try {
val chapters = source.getChapterList(manga.toMangaInfo())
.map { it.toSChapter() }
migrateMangaInternal(source, chapters, prevManga, manga, replace)
} catch (e: Throwable) {
withUIContext { view?.applicationContext?.toast(e.message) }
}
migrateMangaInternal(source, chapters, prevManga, manga, replace)
}.invokeOnCompletion {
presenterScope.launchUI { replacingMangaRelay.call(false) }
}
}

View File

@ -0,0 +1,39 @@
package eu.kanade.tachiyomi.ui.browse.migration.search
import android.os.Bundle
import android.view.View
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.browse.SourceItem
class SourceSearchController(
bundle: Bundle
) : BrowseSourceController(bundle) {
constructor(manga: Manga? = null, source: CatalogueSource, searchQuery: String? = null) : this(
Bundle().apply {
putLong(SOURCE_ID_KEY, source.id)
putSerializable(MANGA_KEY, manga)
if (searchQuery != null) {
putString(SEARCH_QUERY_KEY, searchQuery)
}
}
)
private var oldManga: Manga? = args.getSerializable(MANGA_KEY) as Manga?
private var newManga: Manga? = null
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
newManga = item.manga
val searchController = router.backstack.findLast { it.controller().javaClass == SearchController::class.java }?.controller() as SearchController?
val dialog =
SearchController.MigrationDialog(oldManga, newManga, this)
dialog.targetController = searchController
dialog.showDialog(router)
return true
}
private companion object {
const val MANGA_KEY = "oldManga"
}
}

View File

@ -7,6 +7,7 @@ import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding
@ -31,6 +32,11 @@ class MigrationSourcesController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationSourcesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}

View File

@ -27,9 +27,14 @@ class MigrationSourcesPresenter(
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
val header = SelectionHeader()
return library.asSequence().map { it.source }.toSet()
.mapNotNull { if (it != LocalSource.ID) sourceManager.getOrStub(it) else null }
.sortedBy { it.name.toLowerCase() }
.map { SourceItem(it, header) }.toList()
return library
.groupBy { it.source }
.filterKeys { it != LocalSource.ID }
.map {
val source = sourceManager.getOrStub(it.key)
SourceItem(source, it.value.size, header)
}
.sortedBy { it.source.name.toLowerCase() }
.toList()
}
}

View File

@ -1,9 +1,11 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.view.View
import androidx.core.view.isVisible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.databinding.SourceMainControllerCardItemBinding
import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.util.system.LocaleHelper
class SourceHolder(view: View, val adapter: SourceAdapter) :
FlexibleViewHolder(view, adapter) {
@ -13,10 +15,10 @@ class SourceHolder(view: View, val adapter: SourceAdapter) :
fun bind(item: SourceItem) {
val source = item.source
// Set source name
binding.title.text = source.name
binding.title.text = "${source.name} (${item.mangaCount})"
binding.subtitle.isVisible = source.lang != ""
binding.subtitle.text = LocaleHelper.getDisplayName(source.lang)
// Set source icon
itemView.post {
binding.image.setImageDrawable(source.icon())
}

View File

@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.Source
* @param source Instance of [Source] containing source information.
* @param header The header for this item.
*/
data class SourceItem(val source: Source, val header: SelectionHeader) :
data class SourceItem(val source: Source, val mangaCount: Int, val header: SelectionHeader) :
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
/**

View File

@ -9,12 +9,12 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.recyclerview.widget.LinearLayoutManager
import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItems
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
@ -26,18 +26,13 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
@ -48,7 +43,7 @@ import uy.kohesive.injekt.api.get
* [SourceAdapter.OnLatestClickListener] call function data on latest item click
*/
class SourceController :
NucleusController<SourceMainControllerBinding, SourcePresenter>(),
SearchableNucleusController<SourceMainControllerBinding, SourcePresenter>(),
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
SourceAdapter.OnSourceClickListener {
@ -81,6 +76,11 @@ class SourceController :
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = SourceMainControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
@ -200,37 +200,6 @@ class SourceController :
parentController!!.router.pushController(controller.withFadeTransaction())
}
/**
* Adds items to the options menu.
*
* @param menu menu containing options.
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu
inflater.inflate(R.menu.source_main, menu)
// Initialize search option.
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
// Change hint to show global search.
searchView.queryHint = applicationContext?.getString(R.string.action_global_search_hint)
// Create query listener which opens the global search view.
searchView.queryTextEvents()
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { performGlobalSearch(it.queryText.toString()) }
.launchIn(viewScope)
}
private fun performGlobalSearch(query: String) {
parentController!!.router.pushController(
GlobalSearchController(query).withFadeTransaction()
)
}
/**
* Called when an option menu item has been selected by the user.
*
@ -290,4 +259,21 @@ class SourceController :
}
}
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
createOptionsMenu(
menu,
inflater,
R.menu.source_main,
R.id.action_search,
R.string.action_global_search_hint,
false // GlobalSearch handles the searching here
)
}
override fun onSearchViewQueryTextSubmit(query: String?) {
parentController!!.router.pushController(
GlobalSearchController(query).withFadeTransaction()
)
}
}

View File

@ -49,12 +49,17 @@ data class SourceItem(
override fun equals(other: Any?): Boolean {
if (other is SourceItem) {
return source.id == other.source.id && getHeader()?.code == other.getHeader()?.code
return source.id == other.source.id &&
getHeader()?.code == other.getHeader()?.code &&
isPinned == other.isPinned
}
return false
}
override fun hashCode(): Int {
return source.id.hashCode() + (getHeader()?.code?.hashCode() ?: 0).toInt()
var result = source.id.hashCode()
result = 31 * result + (header?.hashCode() ?: 0)
result = 31 * result + isPinned.hashCode()
return result
}
}

View File

@ -8,7 +8,6 @@ import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.core.view.updatePadding
import androidx.recyclerview.widget.GridLayoutManager
@ -19,6 +18,7 @@ import com.afollestad.materialdialogs.list.listItems
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import com.tfcporciuncula.flow.Preference
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
@ -33,12 +33,13 @@ import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.model.FilterList
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.base.controller.FabController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.library.ChangeMangaCategoriesDialog
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.more.MoreController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.openInBrowser
@ -50,12 +51,8 @@ import eu.kanade.tachiyomi.widget.AutofitRecyclerView
import eu.kanade.tachiyomi.widget.EmptyView
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
@ -63,7 +60,7 @@ import uy.kohesive.injekt.injectLazy
* Controller to manage the catalogues available in the app.
*/
open class BrowseSourceController(bundle: Bundle) :
NucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
SearchableNucleusController<SourceControllerBinding, BrowseSourcePresenter>(bundle),
FabController,
FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener,
@ -85,7 +82,7 @@ open class BrowseSourceController(bundle: Bundle) :
/**
* Adapter containing the list of manga from the catalogue.
*/
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
protected var adapter: FlexibleAdapter<IFlexible<*>>? = null
private var actionFab: ExtendedFloatingActionButton? = null
private var actionFabScrollListener: RecyclerView.OnScrollListener? = null
@ -246,6 +243,11 @@ open class BrowseSourceController(bundle: Bundle) :
actionFab?.shrinkOnScroll(recycler)
}
recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
recycler.setHasFixedSize(true)
recycler.adapter = adapter
@ -258,25 +260,8 @@ open class BrowseSourceController(bundle: Bundle) :
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.source_browse, menu)
// Initialize search menu
createOptionsMenu(menu, inflater, R.menu.source_browse, R.id.action_search)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
val query = presenter.query
if (query.isNotBlank()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
}
searchView.queryTextEvents()
.filter { router.backstack.lastOrNull()?.controller() == this@BrowseSourceController }
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { searchWithQuery(it.queryText.toString()) }
.launchIn(viewScope)
searchItem.fixExpand(
onExpand = { invalidateMenuOnExpand() },
@ -299,6 +284,10 @@ open class BrowseSourceController(bundle: Bundle) :
menu.findItem(displayItem).isChecked = true
}
override fun onSearchViewQueryTextSubmit(query: String?) {
searchWithQuery(query ?: "")
}
override fun onPrepareOptionsMenu(menu: Menu) {
super.onPrepareOptionsMenu(menu)
@ -391,16 +380,16 @@ open class BrowseSourceController(bundle: Bundle) :
}
if (adapter.isEmpty) {
val actions = emptyList<EmptyView.Action>().toMutableList()
if (presenter.source is LocalSource) {
actions += EmptyView.Action(R.string.local_source_help_guide) { openLocalSourceHelpGuide() }
val actions = if (presenter.source is LocalSource) {
listOf(
EmptyView.Action(R.string.local_source_help_guide, R.drawable.ic_help_24dp) { openLocalSourceHelpGuide() }
)
} else {
actions += EmptyView.Action(R.string.action_retry, retryAction)
}
if (presenter.source is HttpSource) {
actions += EmptyView.Action(R.string.action_open_in_web_view) { openInWebView() }
listOf(
EmptyView.Action(R.string.action_retry, R.drawable.ic_refresh_24dp, retryAction),
EmptyView.Action(R.string.action_open_in_web_view, R.drawable.ic_public_24dp) { openInWebView() },
EmptyView.Action(R.string.label_help, R.drawable.ic_help_24dp) { activity?.openInBrowser(MoreController.URL_HELP) }
)
}
binding.emptyView.show(message, actions)

View File

@ -66,12 +66,6 @@ open class BrowseSourcePresenter(
*/
lateinit var source: CatalogueSource
/**
* Query from the view.
*/
var query = searchQuery ?: ""
private set
/**
* Modifiable list of filters.
*/
@ -108,6 +102,10 @@ open class BrowseSourcePresenter(
*/
private var pageSubscription: Subscription? = null
init {
query = searchQuery ?: ""
}
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.browse.source.filter
import android.view.View
import android.widget.EditText
import androidx.core.widget.doOnTextChanged
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.textfield.TextInputLayout
import eu.davidea.flexibleadapter.FlexibleAdapter
@ -10,7 +11,6 @@ import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.viewholders.FlexibleViewHolder
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Holder>() {
@ -25,11 +25,9 @@ open class TextItem(val filter: Filter.Text) : AbstractFlexibleItem<TextItem.Hol
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>, holder: Holder, position: Int, payloads: List<Any?>?) {
holder.wrapper.hint = filter.name
holder.edit.setText(filter.state)
holder.edit.addTextChangedListener(object : SimpleTextWatcher() {
override fun onTextChanged(text: CharSequence, start: Int, before: Int, count: Int) {
filter.state = text.toString()
}
})
holder.edit.doOnTextChanged { text, _, _, _ ->
filter.state = text.toString()
}
}
override fun equals(other: Any?): Boolean {

View File

@ -10,20 +10,16 @@ import android.view.ViewGroup
import androidx.appcompat.widget.SearchView
import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.GlobalSearchControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent
import reactivecircus.flowbinding.appcompat.queryTextEvents
import uy.kohesive.injekt.injectLazy
/**
@ -34,7 +30,7 @@ import uy.kohesive.injekt.injectLazy
open class GlobalSearchController(
protected val initialQuery: String? = null,
protected val extensionFilter: String? = null
) : NucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
) : SearchableNucleusController<GlobalSearchControllerBinding, GlobalSearchPresenter>(),
GlobalSearchCardAdapter.OnMangaClickListener,
GlobalSearchAdapter.OnTitleClickListener {
@ -45,6 +41,11 @@ open class GlobalSearchController(
*/
protected var adapter: GlobalSearchAdapter? = null
/**
* Ref to the OptionsMenu.SearchItem created in onCreateOptionsMenu
*/
private var optionsMenuSearchItem: MenuItem? = null
init {
setHasOptionsMenu(true)
}
@ -58,6 +59,11 @@ open class GlobalSearchController(
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = GlobalSearchControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
@ -100,36 +106,32 @@ open class GlobalSearchController(
* @param inflater used to load the menu xml.
*/
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
// Inflate menu.
inflater.inflate(R.menu.global_search, menu)
// Initialize search menu
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
searchItem.setOnActionExpandListener(
object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem?): Boolean {
searchView.onActionViewExpanded() // Required to show the query in the view
searchView.setQuery(presenter.query, false)
return true
}
override fun onMenuItemActionCollapse(item: MenuItem?): Boolean {
return true
}
}
createOptionsMenu(
menu,
inflater,
R.menu.global_search,
R.id.action_search,
null,
false // the onMenuItemActionExpand will handle this
)
searchView.queryTextEvents()
.filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach {
presenter.search(it.queryText.toString())
searchItem.collapseActionView()
setTitle() // Update toolbar title
}
.launchIn(viewScope)
optionsMenuSearchItem = menu.findItem(R.id.action_search)
}
override fun onSearchMenuItemActionExpand(item: MenuItem?) {
super.onSearchMenuItemActionExpand(item)
val searchView = optionsMenuSearchItem?.actionView as SearchView
searchView.onActionViewExpanded() // Required to show the query in the view
if (nonSubmittedQuery.isBlank()) {
searchView.setQuery(presenter.query, false)
}
}
override fun onSearchViewQueryTextSubmit(query: String?) {
presenter.search(query ?: "")
optionsMenuSearchItem?.collapseActionView()
setTitle() // Update toolbar title
}
/**

View File

@ -47,12 +47,6 @@ open class GlobalSearchPresenter(
*/
val sources by lazy { getSourcesToQuery() }
/**
* Query from the view.
*/
var query = ""
private set
/**
* Fetches the different sources by user settings.
*/

View File

@ -11,6 +11,7 @@ import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.davidea.flexibleadapter.helpers.UndoHelper
@ -75,6 +76,11 @@ class CategoryController :
*/
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = CategoriesControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}

View File

@ -10,6 +10,7 @@ import androidx.core.view.isVisible
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download
@ -56,6 +57,11 @@ class DownloadController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = DownloadControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}

View File

@ -14,9 +14,7 @@ class ChangeMangaCategoriesDialog<T>(bundle: Bundle? = null) :
DialogController(bundle) where T : Controller, T : ChangeMangaCategoriesDialog.Listener {
private var mangas = emptyList<Manga>()
private var categories = emptyList<Category>()
private var preselected = emptyArray<Int>()
constructor(

View File

@ -6,6 +6,7 @@ import android.view.View
import android.widget.FrameLayout
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R
@ -82,6 +83,12 @@ class LibraryCategoryView @JvmOverloads constructor(context: Context, attrs: Att
}
}
recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
adapter = LibraryCategoryAdapter(this)
recycler.setHasFixedSize(true)

View File

@ -10,7 +10,6 @@ import android.view.View
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.appcompat.view.ActionMode
import androidx.appcompat.widget.SearchView
import androidx.core.graphics.drawable.DrawableCompat
import androidx.core.view.isVisible
import com.bluelinelabs.conductor.ControllerChangeHandler
@ -27,8 +26,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.databinding.LibraryControllerBinding
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.SearchableNucleusController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
@ -37,11 +36,9 @@ import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.drop
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
import reactivecircus.flowbinding.appcompat.queryTextChanges
import reactivecircus.flowbinding.viewpager.pageSelections
import rx.Subscription
import uy.kohesive.injekt.Injekt
@ -50,7 +47,7 @@ import uy.kohesive.injekt.api.get
class LibraryController(
bundle: Bundle? = null,
private val preferences: PreferencesHelper = Injekt.get()
) : NucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
) : SearchableNucleusController<LibraryControllerBinding, LibraryPresenter>(bundle),
RootController,
TabbedController,
ActionMode.Callback,
@ -67,11 +64,6 @@ class LibraryController(
*/
private var actionMode: ActionMode? = null
/**
* Library search query.
*/
private var query: String = ""
/**
* Currently selected mangas.
*/
@ -212,12 +204,12 @@ class LibraryController(
binding.btnGlobalSearch.clicks()
.onEach {
router.pushController(
GlobalSearchController(query).withFadeTransaction()
GlobalSearchController(presenter.query).withFadeTransaction()
)
}
.launchIn(viewScope)
(activity!! as MainActivity).fixViewToBottom(binding.actionToolbar)
(activity as? MainActivity)?.fixViewToBottom(binding.actionToolbar)
}
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
@ -230,7 +222,7 @@ class LibraryController(
override fun onDestroyView(view: View) {
destroyActionModeIfNeeded()
(activity!! as MainActivity).clearFixViewToBottom(binding.actionToolbar)
(activity as? MainActivity)?.clearFixViewToBottom(binding.actionToolbar)
binding.actionToolbar.destroy()
adapter?.onDestroy()
adapter = null
@ -384,52 +376,21 @@ class LibraryController(
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.library, menu)
val searchItem = menu.findItem(R.id.action_search)
val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
if (query.isNotEmpty()) {
searchItem.expandActionView()
searchView.setQuery(query, true)
searchView.clearFocus()
performSearch()
// Workaround for weird behavior where searchview gets empty text change despite
// query being set already
searchView.postDelayed({ initSearchHandler(searchView) }, 500)
} else {
initSearchHandler(searchView)
}
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.
menu.findItem(R.id.action_filter).icon.mutate()
}
fun search(query: String) {
this.query = query
}
private fun initSearchHandler(searchView: SearchView) {
searchView.queryTextChanges()
// Ignore events if this controller isn't at the top to avoid query being reset
.filter { router.backstack.lastOrNull()?.controller() == this }
.onEach {
query = it.toString()
performSearch()
}
.launchIn(viewScope)
presenter.query = query
}
private fun performSearch() {
searchRelay.call(query)
if (query.isNotEmpty()) {
searchRelay.call(presenter.query)
if (presenter.query.isNotEmpty()) {
binding.btnGlobalSearch.isVisible = true
binding.btnGlobalSearch.text =
resources?.getString(R.string.action_global_search_query, query)
resources?.getString(R.string.action_global_search_query, presenter.query)
} else {
binding.btnGlobalSearch.isVisible = false
}
@ -611,4 +572,12 @@ class LibraryController(
selectInverseRelay.call(it)
}
}
override fun onSearchViewQueryTextChange(newText: String?) {
// Ignore events if this controller isn't at the top to avoid query being reset
if (router.backstack.lastOrNull()?.controller() == this) {
presenter.query = newText ?: ""
performSearch()
}
}
}

View File

@ -90,6 +90,7 @@ class LibraryItem(val manga: LibraryManga, private val libraryDisplayMode: Prefe
return manga.title.contains(constraint, true) ||
(manga.author?.contains(constraint, true) ?: false) ||
(manga.artist?.contains(constraint, true) ?: false) ||
(manga.description?.contains(constraint, true) ?: false) ||
sourceManager.getOrStub(manga.source).name.contains(constraint, true) ||
if (constraint.contains(",")) {
constraint.split(",").all { containsGenre(it.trim(), manga.getGenres()) }

View File

@ -235,7 +235,12 @@ class LibraryPresenter(
var counter = 0
db.getLatestChapterManga().executeAsBlocking().associate { it.id!! to counter++ }
}
val chapterFetchDateManga by lazy {
var counter = 0
db.getChapterFetchDateManga().executeAsBlocking().associate { it.id!! to counter++ }
}
val sortAscending = preferences.librarySortingAscending().get()
val sortFn: (LibraryItem, LibraryItem) -> Int = { i1, i2 ->
when (sortingMode) {
LibrarySort.ALPHA -> i1.manga.title.compareTo(i2.manga.title, true)
@ -246,7 +251,13 @@ class LibraryPresenter(
manga1LastRead.compareTo(manga2LastRead)
}
LibrarySort.LAST_CHECKED -> i2.manga.last_update.compareTo(i1.manga.last_update)
LibrarySort.UNREAD -> i1.manga.unread.compareTo(i2.manga.unread)
LibrarySort.UNREAD -> when {
// Ensure unread content comes first
i1.manga.unread == i2.manga.unread -> 0
i1.manga.unread == 0 -> if (sortAscending) 1 else -1
i2.manga.unread == 0 -> if (sortAscending) -1 else 1
else -> i1.manga.unread.compareTo(i2.manga.unread)
}
LibrarySort.TOTAL -> {
val manga1TotalChapter = totalChapterManga[i1.manga.id!!] ?: 0
val mange2TotalChapter = totalChapterManga[i2.manga.id!!] ?: 0
@ -259,12 +270,19 @@ class LibraryPresenter(
?: latestChapterManga.size
manga1latestChapter.compareTo(manga2latestChapter)
}
LibrarySort.CHAPTER_FETCH_DATE -> {
val manga1chapterFetchDate = chapterFetchDateManga[i1.manga.id!!]
?: chapterFetchDateManga.size
val manga2chapterFetchDate = chapterFetchDateManga[i2.manga.id!!]
?: chapterFetchDateManga.size
manga1chapterFetchDate.compareTo(manga2chapterFetchDate)
}
LibrarySort.DATE_ADDED -> i2.manga.date_added.compareTo(i1.manga.date_added)
else -> throw Exception("Unknown sorting mode")
}
}
val comparator = if (preferences.librarySortingAscending().get()) {
val comparator = if (sortAscending) {
Comparator(sortFn)
} else {
Collections.reverseOrder(sortFn)

View File

@ -20,7 +20,7 @@ class LibrarySettingsSheet(
router: Router,
private val trackManager: TrackManager = Injekt.get(),
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit
) : TabbedBottomSheetDialog(router) {
) : TabbedBottomSheetDialog(router.activity!!) {
val filters: Filter
private val sort: Sort
@ -157,11 +157,12 @@ class LibrarySettingsSheet(
private val lastChecked = Item.MultiSort(R.string.action_sort_last_checked, this)
private val unread = Item.MultiSort(R.string.action_filter_unread, this)
private val latestChapter = Item.MultiSort(R.string.action_sort_latest_chapter, this)
private val chapterFetchDate = Item.MultiSort(R.string.action_sort_chapter_fetch_date, this)
private val dateAdded = Item.MultiSort(R.string.action_sort_date_added, this)
override val header = null
override val items =
listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter, dateAdded)
listOf(alphabetically, lastRead, lastChecked, unread, total, latestChapter, chapterFetchDate, dateAdded)
override val footer = null
override fun initModels() {
@ -184,6 +185,8 @@ class LibrarySettingsSheet(
if (sorting == LibrarySort.TOTAL) order else Item.MultiSort.SORT_NONE
latestChapter.state =
if (sorting == LibrarySort.LATEST_CHAPTER) order else Item.MultiSort.SORT_NONE
chapterFetchDate.state =
if (sorting == LibrarySort.CHAPTER_FETCH_DATE) order else Item.MultiSort.SORT_NONE
dateAdded.state =
if (sorting == LibrarySort.DATE_ADDED) order else Item.MultiSort.SORT_NONE
}
@ -211,6 +214,7 @@ class LibrarySettingsSheet(
unread -> LibrarySort.UNREAD
total -> LibrarySort.TOTAL
latestChapter -> LibrarySort.LATEST_CHAPTER
chapterFetchDate -> LibrarySort.CHAPTER_FETCH_DATE
dateAdded -> LibrarySort.DATE_ADDED
else -> throw Exception("Unknown sorting")
}

View File

@ -8,6 +8,7 @@ object LibrarySort {
const val UNREAD = 3
const val TOTAL = 4
const val LATEST_CHAPTER = 6
const val CHAPTER_FETCH_DATE = 8
const val DATE_ADDED = 7
@Deprecated("Removed in favor of searching by source")

View File

@ -2,11 +2,16 @@ package eu.kanade.tachiyomi.ui.main
import android.app.SearchManager
import android.content.Intent
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.widget.Toast
import androidx.coordinatorlayout.widget.CoordinatorLayout
import androidx.core.view.ViewCompat
import androidx.core.view.WindowCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.lifecycleScope
@ -18,6 +23,7 @@ import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.google.android.material.appbar.AppBarLayout
import com.google.android.material.behavior.HideBottomViewOnScrollBehavior
import dev.chrisbanes.insetter.applyInsetter
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.Migrations
import eu.kanade.tachiyomi.R
@ -43,6 +49,7 @@ import eu.kanade.tachiyomi.ui.recent.history.HistoryController
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.launchIn
import timber.log.Timber
@ -84,6 +91,35 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
setContentView(binding.root)
setSupportActionBar(binding.toolbar)
// Draw edge-to-edge
WindowCompat.setDecorFitsSystemWindows(window, false)
binding.appbar.applyInsetter {
type(navigationBars = true, statusBars = true) {
padding(left = true, top = true, right = true)
}
}
binding.bottomNav.applyInsetter {
type(navigationBars = true) {
padding()
}
}
binding.rootFab.applyInsetter {
type(navigationBars = true) {
margin()
}
}
// Make sure navigation bar is on bottom when making it transparent
ViewCompat.setOnApplyWindowInsetsListener(binding.root) { _, insets ->
if (insets.getInsets(WindowInsetsCompat.Type.navigationBars()).bottom > 0) {
// Keep scrim on light theme if windowLightNavigationBar is not available
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1 || isDarkMode) {
window.navigationBarColor = Color.TRANSPARENT
}
}
insets
}
tabAnimator = ViewHeightAnimator(binding.tabs, 0L)
bottomNavAnimator = ViewHeightAnimator(binding.bottomNav)
@ -301,11 +337,8 @@ class MainActivity : BaseViewBindingActivity<MainActivityBinding>() {
private suspend fun resetExitConfirmation() {
isConfirmingExit = true
val toast = Toast.makeText(this, R.string.confirm_exit, Toast.LENGTH_LONG)
toast.show()
val toast = toast(R.string.confirm_exit, Toast.LENGTH_LONG)
delay(2000)
toast.cancel()
isConfirmingExit = false
}

View File

@ -1,13 +1,12 @@
package eu.kanade.tachiyomi.ui.main
import android.app.Dialog
import android.content.Intent
import android.os.Bundle
import androidx.core.net.toUri
import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
class WhatsNewDialogController(bundle: Bundle? = null) : DialogController(bundle) {
@ -16,9 +15,7 @@ class WhatsNewDialogController(bundle: Bundle? = null) : DialogController(bundle
.title(text = activity!!.getString(R.string.updated_version, BuildConfig.VERSION_NAME))
.positiveButton(android.R.string.ok)
.neutralButton(R.string.whats_new) {
val url = "https://github.com/tachiyomiorg/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}"
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
openInBrowser("https://github.com/tachiyomiorg/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}")
}
}
}

View File

@ -26,6 +26,7 @@ import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
import com.google.android.material.snackbar.Snackbar
import dev.chrisbanes.insetter.applyInsetter
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.SelectableAdapter
import eu.kanade.tachiyomi.R
@ -72,6 +73,7 @@ import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
import eu.kanade.tachiyomi.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import eu.kanade.tachiyomi.util.hasCustomCover
import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.getCoordinates
@ -199,6 +201,11 @@ class MangaController :
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MangaControllerBinding.inflate(inflater)
binding.recycler.applyInsetter {
type(navigationBars = true) {
padding()
}
}
return binding.root
}
@ -242,7 +249,7 @@ class MangaController :
}
.launchIn(viewScope)
(activity!! as MainActivity).fixViewToBottom(binding.actionToolbar)
(activity as? MainActivity)?.fixViewToBottom(binding.actionToolbar)
settingsSheet = ChaptersSettingsSheet(router, presenter) { group ->
if (group is ChaptersSettingsSheet.Filter.FilterGroup) {
@ -321,7 +328,7 @@ class MangaController :
override fun onDestroyView(view: View) {
destroyActionModeIfNeeded()
(activity!! as MainActivity).clearFixViewToBottom(binding.actionToolbar)
(activity as? MainActivity)?.clearFixViewToBottom(binding.actionToolbar)
binding.actionToolbar.destroy()
mangaInfoAdapter = null
chaptersHeaderAdapter = null
@ -608,8 +615,9 @@ class MangaController :
override fun openMangaCoverPicker(manga: Manga) {
if (manga.favorite) {
val intent = Intent(Intent.ACTION_GET_CONTENT)
intent.type = "image/*"
val intent = Intent(Intent.ACTION_GET_CONTENT).apply {
type = "image/*"
}
startActivityForResult(
Intent.createChooser(
intent,
@ -988,7 +996,9 @@ class MangaController :
chapters.forEach {
chaptersAdapter?.updateItem(it)
}
chaptersAdapter?.notifyDataSetChanged()
launchUI {
chaptersAdapter?.notifyDataSetChanged()
}
}
fun onChaptersDeletedError(error: Throwable) {

View File

@ -316,17 +316,26 @@ class MangaPresenter(
private fun observeDownloads() {
observeDownloadsStatusSubscription?.let { remove(it) }
observeDownloadsStatusSubscription = downloadManager.queue.getStatusObservable()
.observeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io())
.onBackpressureLatest()
.filter { download -> download.manga.id == manga.id }
.doOnNext { onDownloadStatusChange(it) }
.subscribeLatestCache(MangaController::onChapterDownloadUpdate) { _, error ->
Timber.e(error)
}
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(
{ view, it ->
onDownloadStatusChange(it)
view.onChapterDownloadUpdate(it)
},
{ _, error ->
Timber.e(error)
}
)
observeDownloadsPageSubscription?.let { remove(it) }
observeDownloadsPageSubscription = downloadManager.queue.getProgressObservable()
.observeOn(AndroidSchedulers.mainThread())
.observeOn(Schedulers.io())
.onBackpressureLatest()
.filter { download -> download.manga.id == manga.id }
.observeOn(AndroidSchedulers.mainThread())
.subscribeLatestCache(MangaController::onChapterDownloadUpdate) { _, error ->
Timber.e(error)
}
@ -484,7 +493,7 @@ class MangaPresenter(
db.updateChaptersProgress(chapters).executeAsBlocking()
if (preferences.removeAfterMarkedAsRead()) {
deleteChapters(chapters)
deleteChapters(chapters.filter { it.read })
}
}
}

View File

@ -14,6 +14,9 @@ class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: Att
private val binding: ChapterDownloadViewBinding
private var state = Download.State.NOT_DOWNLOADED
private var progress = 0
private var downloadIconAnimator: ObjectAnimator? = null
private var isAnimating = false
@ -23,6 +26,17 @@ class ChapterDownloadView @JvmOverloads constructor(context: Context, attrs: Att
}
fun setState(state: Download.State, progress: Int = 0) {
val isDirty = this.state.value != state.value || this.progress != progress
this.state = state
this.progress = progress
if (isDirty) {
updateLayout()
}
}
private fun updateLayout() {
binding.downloadIconBorder.isVisible = state == Download.State.NOT_DOWNLOADED
binding.downloadIcon.isVisible = state == Download.State.NOT_DOWNLOADED || state == Download.State.DOWNLOADING

View File

@ -17,7 +17,7 @@ class ChaptersSettingsSheet(
private val router: Router,
private val presenter: MangaPresenter,
onGroupClickListener: (ExtendedNavigationView.Group) -> Unit
) : TabbedBottomSheetDialog(router) {
) : TabbedBottomSheetDialog(router.activity!!) {
val filters: Filter
private val sort: Sort

View File

@ -2,13 +2,13 @@ package eu.kanade.tachiyomi.ui.manga.track
import android.annotation.SuppressLint
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.TrackItemBinding
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
import uy.kohesive.injekt.injectLazy
import java.text.DateFormat
class TrackHolder(private val binding: TrackItemBinding, adapter: TrackAdapter) : BaseViewHolder(binding.root) {
class TrackHolder(private val binding: TrackItemBinding, adapter: TrackAdapter) : RecyclerView.ViewHolder(binding.root) {
private val preferences: PreferencesHelper by injectLazy()

View File

@ -55,24 +55,24 @@ class TrackSearchAdapter(context: Context) :
.into(binding.trackSearchCover)
}
if (track.publishing_status.isBlank()) {
binding.trackSearchStatus.isVisible = false
binding.trackSearchStatusResult.isVisible = false
} else {
val hasStatus = track.publishing_status.isNotBlank()
binding.trackSearchStatus.isVisible = hasStatus
binding.trackSearchStatusResult.isVisible = hasStatus
if (hasStatus) {
binding.trackSearchStatusResult.text = track.publishing_status.capitalize()
}
if (track.publishing_type.isBlank()) {
binding.trackSearchType.isVisible = false
binding.trackSearchTypeResult.isVisible = false
} else {
val hasType = track.publishing_type.isNotBlank()
binding.trackSearchType.isVisible = hasType
binding.trackSearchTypeResult.isVisible = hasType
if (hasType) {
binding.trackSearchTypeResult.text = track.publishing_type.capitalize()
}
if (track.start_date.isBlank()) {
binding.trackSearchStart.isVisible = false
binding.trackSearchStartResult.isVisible = false
} else {
val hasStartDate = track.start_date.isNotBlank()
binding.trackSearchStart.isVisible = hasStartDate
binding.trackSearchStartResult.isVisible = hasStartDate
if (hasStartDate) {
binding.trackSearchStartResult.text = track.start_date
}
}

View File

@ -1,13 +1,12 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
@ -65,7 +64,7 @@ class TrackSheet(
val track = adapter.getItem(position)?.track ?: return
if (track.tracking_url.isNotBlank()) {
controller.activity?.startActivity(Intent(Intent.ACTION_VIEW, track.tracking_url.toUri()))
controller.openInBrowser(track.tracking_url)
}
}

View File

@ -1,10 +1,8 @@
package eu.kanade.tachiyomi.ui.more
import android.app.Dialog
import android.content.Intent
import android.os.Build
import android.os.Bundle
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.preference.PreferenceScreen
import com.afollestad.materialdialogs.MaterialDialog
@ -15,6 +13,7 @@ import eu.kanade.tachiyomi.data.updater.UpdateResult
import eu.kanade.tachiyomi.data.updater.UpdaterService
import eu.kanade.tachiyomi.data.updater.github.GithubUpdateChecker
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.setting.SettingsController
import eu.kanade.tachiyomi.util.lang.launchNow
import eu.kanade.tachiyomi.util.lang.toDateTimestampString
@ -76,19 +75,15 @@ class AboutController : SettingsController() {
} else {
"https://github.com/tachiyomiorg/tachiyomi/releases/tag/v${BuildConfig.VERSION_NAME}"
}
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
openInBrowser(url)
}
}
if (BuildConfig.DEBUG) {
preference {
key = "pref_about_notices"
titleRes = R.string.notices
onClick {
val intent = Intent(Intent.ACTION_VIEW, "https://github.com/tachiyomiorg/tachiyomi/blob/master/PREVIEW_RELEASE_NOTES.md".toUri())
startActivity(intent)
openInBrowser("https://github.com/tachiyomiorg/tachiyomi/blob/master/PREVIEW_RELEASE_NOTES.md")
}
}
}
@ -97,47 +92,46 @@ class AboutController : SettingsController() {
preference {
key = "pref_about_website"
titleRes = R.string.website
val url = "https://tachiyomi.org"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
"https://tachiyomi.org".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_twitter"
title = "Twitter"
"https://twitter.com/tachiyomiorg".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_discord"
title = "Discord"
val url = "https://discord.gg/tachiyomi"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
"https://discord.gg/tachiyomi".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_github"
title = "GitHub"
val url = "https://github.com/tachiyomiorg/tachiyomi"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
"https://github.com/tachiyomiorg/tachiyomi".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_label_extensions"
titleRes = R.string.label_extensions
val url = "https://github.com/tachiyomiorg/tachiyomi-extensions"
summary = url
onClick {
val intent = Intent(Intent.ACTION_VIEW, url.toUri())
startActivity(intent)
"https://github.com/tachiyomiorg/tachiyomi-extensions".also {
summary = it
onClick { openInBrowser(it) }
}
}
preference {
key = "pref_about_licenses"
titleRes = R.string.licenses
onClick {
LibsBuilder()
.withActivityTitle(activity!!.getString(R.string.licenses))

View File

@ -78,7 +78,7 @@ class MoreController :
}
}
preference {
titleRes = R.string.label_categories
titleRes = R.string.categories
iconRes = R.drawable.ic_label_24dp
iconTint = tintColor
onClick {
@ -151,6 +151,6 @@ class MoreController :
}
companion object {
private const val URL_HELP = "https://tachiyomi.org/help/"
const val URL_HELP = "https://tachiyomi.org/help/"
}
}

View File

@ -3,30 +3,29 @@ package eu.kanade.tachiyomi.ui.reader
import android.annotation.SuppressLint
import android.annotation.TargetApi
import android.app.ProgressDialog
import android.content.ClipData
import android.content.Context
import android.content.Intent
import android.content.pm.ActivityInfo
import android.content.res.Configuration
import android.graphics.Bitmap
import android.graphics.Color
import android.os.Build
import android.os.Bundle
import android.view.Gravity
import android.view.KeyEvent
import android.view.Menu
import android.view.MenuItem
import android.view.MotionEvent
import android.view.View
import android.view.WindowManager
import android.view.animation.Animation
import android.view.animation.AnimationUtils
import android.widget.SeekBar
import android.widget.Toast
import androidx.core.view.ViewCompat
import androidx.core.view.WindowInsetsCompat
import androidx.core.view.isVisible
import androidx.core.view.setPadding
import androidx.lifecycle.lifecycleScope
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView
import com.google.android.material.snackbar.Snackbar
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
@ -34,14 +33,20 @@ import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.asImmediateFlow
import eu.kanade.tachiyomi.data.preference.toggle
import eu.kanade.tachiyomi.databinding.ReaderActivityBinding
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.AddToLibraryFirst
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Error
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter.SetAsCoverResult.Success
import eu.kanade.tachiyomi.ui.reader.model.ReaderChapter
import eu.kanade.tachiyomi.ui.reader.model.ReaderPage
import eu.kanade.tachiyomi.ui.reader.model.ViewerChapters
import eu.kanade.tachiyomi.ui.reader.setting.OrientationType
import eu.kanade.tachiyomi.ui.reader.setting.ReaderSettingsSheet
import eu.kanade.tachiyomi.ui.reader.setting.ReadingModeType
import eu.kanade.tachiyomi.ui.reader.viewer.BaseViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.L2RPagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.pager.R2LPagerViewer
@ -54,8 +59,8 @@ import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.view.defaultBar
import eu.kanade.tachiyomi.util.view.hideBar
import eu.kanade.tachiyomi.util.view.isDefaultBar
import eu.kanade.tachiyomi.util.view.setTooltip
import eu.kanade.tachiyomi.util.view.showBar
import eu.kanade.tachiyomi.util.view.snack
import eu.kanade.tachiyomi.widget.SimpleAnimationListener
import eu.kanade.tachiyomi.widget.SimpleSeekBarListener
import kotlinx.coroutines.delay
@ -76,6 +81,16 @@ import kotlin.math.abs
@RequiresPresenter(ReaderPresenter::class)
class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>() {
companion object {
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
return Intent(context, ReaderActivity::class.java).apply {
putExtra("manga", manga.id)
putExtra("chapter", chapter.id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
}
private val preferences: PreferencesHelper by injectLazy()
/**
@ -108,22 +123,9 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
@Suppress("DEPRECATION")
private var progressDialog: ProgressDialog? = null
companion object {
@Suppress("unused")
const val LEFT_TO_RIGHT = 1
const val RIGHT_TO_LEFT = 2
const val VERTICAL = 3
const val WEBTOON = 4
const val VERTICAL_PLUS = 5
private var menuToggleToast: Toast? = null
fun newIntent(context: Context, manga: Manga, chapter: Chapter): Intent {
return Intent(context, ReaderActivity::class.java).apply {
putExtra("manga", manga.id)
putExtra("chapter", chapter.id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
}
}
private var readingModeToast: Toast? = null
/**
* Called when the activity is created. Initializes the presenter and configuration.
@ -173,6 +175,8 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
viewer?.destroy()
viewer = null
config = null
menuToggleToast?.cancel()
readingModeToast?.cancel()
progressDialog?.dismiss()
progressDialog = null
}
@ -236,18 +240,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
presenter.bookmarkCurrentChapter(false)
invalidateOptionsMenu()
}
R.id.action_settings -> ReaderSettingsSheet(this).show()
R.id.action_custom_filter -> {
val sheet = ReaderColorFilterSheet(this)
// Remove dimmed backdrop so changes can be previewed
.apply { window?.setDimAmount(0f) }
// Hide toolbars while sheet is open for better preview
sheet.setOnDismissListener { setMenuVisibility(true) }
setMenuVisibility(false)
sheet.show()
}
}
return super.onOptionsItemSelected(item)
}
@ -293,7 +285,6 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
* Initializes the reader menu. It sets up click listeners and the initial visibility.
*/
private fun initializeMenu() {
// Set toolbar
setSupportActionBar(binding.toolbar)
supportActionBar?.setDisplayHomeAsUpEnabled(true)
binding.toolbar.setNavigationOnClickListener {
@ -313,6 +304,18 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
insets
}
binding.toolbar.setOnClickListener {
presenter.manga?.id?.let { id ->
startActivity(
Intent(this, MainActivity::class.java).apply {
action = MainActivity.SHORTCUT_MANGA
putExtra(MangaController.MANGA_EXTRA, id)
addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
}
)
}
}
// Init listeners on bottom menu
binding.pageSeekbar.setOnSeekBarChangeListener(
object : SimpleSeekBarListener() {
@ -342,15 +345,105 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
}
}
initBottomShortcuts()
// Set initial visibility
setMenuVisibility(menuVisible)
}
private fun initBottomShortcuts() {
// Reading mode
with(binding.actionReadingMode) {
setTooltip(R.string.viewer)
setOnClickListener {
val newReadingMode =
ReadingModeType.getNextReadingMode(presenter.getMangaViewer(resolveDefault = false))
presenter.setMangaViewer(newReadingMode.prefValue)
menuToggleToast?.cancel()
if (!preferences.showReadingMode()) {
menuToggleToast = toast(newReadingMode.stringRes)
}
}
}
// Rotation
with(binding.actionRotation) {
setTooltip(R.string.pref_rotation_type)
setOnClickListener {
val newOrientation =
OrientationType.getNextOrientation(preferences.rotation().get(), resources)
preferences.rotation().set(newOrientation.prefValue)
setOrientation(newOrientation.flag)
menuToggleToast?.cancel()
menuToggleToast = toast(newOrientation.stringRes)
}
}
preferences.rotation().asImmediateFlow { updateRotationShortcut(it) }
.launchIn(lifecycleScope)
// Crop borders
with(binding.actionCropBorders) {
setTooltip(R.string.pref_crop_borders)
setOnClickListener {
val isPagerType = ReadingModeType.isPagerType(presenter.getMangaViewer())
if (isPagerType) {
preferences.cropBorders().toggle()
} else {
preferences.cropBordersWebtoon().toggle()
}
}
}
updateCropBordersShortcut()
listOf(preferences.cropBorders(), preferences.cropBordersWebtoon())
.forEach { pref ->
pref.asFlow()
.onEach { updateCropBordersShortcut() }
.launchIn(lifecycleScope)
}
// Settings sheet
with(binding.actionSettings) {
setTooltip(R.string.action_settings)
setOnClickListener {
ReaderSettingsSheet(this@ReaderActivity).show()
}
}
}
private fun updateRotationShortcut(preference: Int) {
val orientation = OrientationType.fromPreference(preference, resources)
binding.actionRotation.setImageResource(orientation.iconRes)
}
private fun updateCropBordersShortcut() {
val isPagerType = ReadingModeType.isPagerType(presenter.getMangaViewer())
val enabled = if (isPagerType) {
preferences.cropBorders().get()
} else {
preferences.cropBordersWebtoon().get()
}
binding.actionCropBorders.setImageResource(
if (enabled) {
R.drawable.ic_crop_24dp
} else {
R.drawable.ic_crop_off_24dp
}
)
}
/**
* Sets the visibility of the menu according to [visible] and with an optional parameter to
* [animate] the views.
*/
private fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
fun setMenuVisibility(visible: Boolean, animate: Boolean = true) {
menuVisible = visible
if (visible) {
if (preferences.fullscreen().get()) {
@ -365,7 +458,7 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
toolbarAnimation.setAnimationListener(
object : SimpleAnimationListener() {
override fun onAnimationStart(animation: Animation) {
// Fix status bar being translucent the first time it's opened.
// Fix status bar being translucent the first time it's opened.
window.addFlags(WindowManager.LayoutParams.FLAG_DRAWS_SYSTEM_BAR_BACKGROUNDS)
}
}
@ -421,12 +514,16 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
*/
fun setManga(manga: Manga) {
val prevViewer = viewer
val viewerMode = ReadingModeType.fromPreference(presenter.getMangaViewer(resolveDefault = false))
binding.actionReadingMode.setImageResource(viewerMode.iconRes)
val newViewer = when (presenter.getMangaViewer()) {
RIGHT_TO_LEFT -> R2LPagerViewer(this)
VERTICAL -> VerticalPagerViewer(this)
WEBTOON -> WebtoonViewer(this)
VERTICAL_PLUS -> WebtoonViewer(this, isContinuous = false)
else -> L2RPagerViewer(this)
ReadingModeType.LEFT_TO_RIGHT.prefValue -> L2RPagerViewer(this)
ReadingModeType.VERTICAL.prefValue -> VerticalPagerViewer(this)
ReadingModeType.WEBTOON.prefValue -> WebtoonViewer(this)
ReadingModeType.CONTINUOUS_VERTICAL.prefValue -> WebtoonViewer(this, isContinuous = false)
else -> R2LPagerViewer(this)
}
// Destroy previous viewer if there was one
@ -438,20 +535,30 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
binding.viewerContainer.addView(newViewer.getView())
if (preferences.showReadingMode()) {
showReadingModeSnackbar(presenter.getMangaViewer())
showReadingModeToast(presenter.getMangaViewer())
}
binding.toolbar.title = manga.title
binding.pageSeekbar.isRTL = newViewer is R2LPagerViewer
if (newViewer is R2LPagerViewer) {
binding.leftChapter.setTooltip(R.string.action_next_chapter)
binding.rightChapter.setTooltip(R.string.action_previous_chapter)
} else {
binding.leftChapter.setTooltip(R.string.action_previous_chapter)
binding.rightChapter.setTooltip(R.string.action_next_chapter)
}
binding.pleaseWait.isVisible = true
binding.pleaseWait.startAnimation(AnimationUtils.loadAnimation(this, R.anim.fade_in_long))
}
private fun showReadingModeSnackbar(mode: Int) {
private fun showReadingModeToast(mode: Int) {
val strings = resources.getStringArray(R.array.viewers_selector)
binding.root.snack(strings[mode], Snackbar.LENGTH_SHORT)
readingModeToast?.cancel()
readingModeToast = toast(strings[mode]) {
it.setGravity(Gravity.CENTER_VERTICAL or Gravity.CENTER_HORIZONTAL, 0, 0)
}
}
/**
@ -595,10 +702,11 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
val manga = presenter.manga ?: return
val chapter = page.chapter.chapter
val stream = file.getUriCompat(this)
val uri = file.getUriCompat(this)
val intent = Intent(Intent.ACTION_SEND).apply {
putExtra(Intent.EXTRA_TEXT, getString(R.string.share_page_info, manga.title, chapter.name, page.number))
putExtra(Intent.EXTRA_STREAM, stream)
putExtra(Intent.EXTRA_STREAM, uri)
clipData = ClipData.newRawUri(null, uri)
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
type = "image/*"
}
@ -650,6 +758,16 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
)
}
/**
* Forces the user preferred [orientation] on the activity.
*/
private fun setOrientation(orientation: Int) {
val newOrientation = OrientationType.fromPreference(orientation, resources)
if (newOrientation.flag != requestedOrientation) {
requestedOrientation = newOrientation.flag
}
}
/**
* Class that handles the user preferences of the reader.
*/
@ -703,38 +821,11 @@ class ReaderActivity : BaseRxActivity<ReaderActivityBinding, ReaderPresenter>()
.launchIn(lifecycleScope)
}
/**
* Forces the user preferred [orientation] on the activity.
*/
private fun setOrientation(orientation: Int) {
val newOrientation = when (orientation) {
// Lock in current orientation
2 -> {
val currentOrientation = resources.configuration.orientation
if (currentOrientation == Configuration.ORIENTATION_PORTRAIT) {
ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
} else {
ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
}
}
// Lock in portrait
3 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT
// Lock in landscape
4 -> ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE
// Rotation free
else -> ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
}
if (newOrientation != requestedOrientation) {
requestedOrientation = newOrientation
}
}
/**
* Sets the visibility of the bottom page indicator according to [visible].
*/
fun setPageNumberVisibility(visible: Boolean) {
binding.pageNumber.visibility = if (visible) View.VISIBLE else View.INVISIBLE
binding.pageNumber.isVisible = visible
}
/**

View File

@ -0,0 +1,119 @@
package eu.kanade.tachiyomi.ui.reader
import android.content.Context
import android.graphics.Canvas
import android.graphics.Color
import android.graphics.Paint
import android.util.AttributeSet
import android.view.MotionEvent
import android.view.View
import android.view.ViewPropertyAnimator
import androidx.core.content.ContextCompat
import androidx.core.view.isVisible
import eu.kanade.tachiyomi.ui.reader.viewer.ViewerNavigation
import kotlin.math.abs
class ReaderNavigationOverlayView(context: Context, attributeSet: AttributeSet) : View(context, attributeSet) {
private var viewPropertyAnimator: ViewPropertyAnimator? = null
private var navigation: ViewerNavigation? = null
fun setNavigation(navigation: ViewerNavigation, showOnStart: Boolean) {
if (!showOnStart && this.navigation == null) {
this.navigation = navigation
isVisible = false
return
}
this.navigation = navigation
invalidate()
if (isVisible) return
viewPropertyAnimator = animate()
.alpha(1f)
.setDuration(FADE_DURATION)
.withStartAction {
isVisible = true
}
.withEndAction {
viewPropertyAnimator = null
}
viewPropertyAnimator?.start()
}
private val regionPaint = Paint()
private val textPaint = Paint().apply {
textAlign = Paint.Align.CENTER
color = Color.WHITE
textSize = 64f
}
private val textBorderPaint = Paint().apply {
textAlign = Paint.Align.CENTER
color = Color.BLACK
textSize = 64f
style = Paint.Style.STROKE
strokeWidth = 8f
}
override fun onDraw(canvas: Canvas?) {
if (navigation == null) return
navigation?.regions?.forEach { region ->
val rect = region.rectF
canvas?.save()
// Scale rect from 1f,1f to screen width and height
canvas?.scale(width.toFloat(), height.toFloat())
regionPaint.color = ContextCompat.getColor(context, region.type.colorRes)
canvas?.drawRect(rect, regionPaint)
canvas?.restore()
// Don't want scale anymore because it messes with drawText
canvas?.save()
// Translate origin to rect start (left, top)
canvas?.translate((width * rect.left), (height * rect.top))
// Calculate center of rect width on screen
val x = width * (abs(rect.left - rect.right) / 2)
// Calculate center of rect height on screen
val y = height * (abs(rect.top - rect.bottom) / 2)
canvas?.drawText(context.getString(region.type.nameRes), x, y, textBorderPaint)
canvas?.drawText(context.getString(region.type.nameRes), x, y, textPaint)
canvas?.restore()
}
}
override fun performClick(): Boolean {
super.performClick()
if (viewPropertyAnimator == null && isVisible) {
viewPropertyAnimator = animate()
.alpha(0f)
.setDuration(FADE_DURATION)
.withEndAction {
isVisible = false
viewPropertyAnimator = null
}
viewPropertyAnimator?.start()
}
return true
}
override fun onTouchEvent(event: MotionEvent?): Boolean {
// Hide overlay if user start tapping or swiping
performClick()
return super.onTouchEvent(event)
}
}
private const val FADE_DURATION = 1000L

View File

@ -489,9 +489,9 @@ class ReaderPresenter(
/**
* Returns the viewer position used by this manga or the default one.
*/
fun getMangaViewer(): Int {
fun getMangaViewer(resolveDefault: Boolean = true): Int {
val manga = manga ?: return preferences.defaultViewer()
return if (manga.viewer == 0) preferences.defaultViewer() else manga.viewer
return if (resolveDefault && manga.viewer == 0) preferences.defaultViewer() else manga.viewer
}
/**
@ -559,7 +559,7 @@ class ReaderPresenter(
val destDir = File(
Environment.getExternalStorageDirectory().absolutePath +
File.separator + Environment.DIRECTORY_PICTURES +
File.separator + "Tachiyomi"
File.separator + context.getString(R.string.app_name)
)
// Copy file in background.

View File

@ -1,156 +0,0 @@
package eu.kanade.tachiyomi.ui.reader
import android.os.Bundle
import android.widget.CompoundButton
import android.widget.Spinner
import androidx.annotation.ArrayRes
import androidx.core.view.isVisible
import androidx.core.widget.NestedScrollView
import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.ReaderSettingsSheetBinding
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerViewer
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonViewer
import eu.kanade.tachiyomi.widget.IgnoreFirstSpinnerListener
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
import uy.kohesive.injekt.injectLazy
/**
* Sheet to show reader and viewer preferences.
*/
class ReaderSettingsSheet(private val activity: ReaderActivity) : BaseBottomSheetDialog(activity) {
private val preferences: PreferencesHelper by injectLazy()
private val binding = ReaderSettingsSheetBinding.inflate(activity.layoutInflater, null, false)
init {
val scroll = NestedScrollView(activity)
scroll.addView(binding.root)
setContentView(scroll)
}
/**
* Called when the sheet is created. It initializes the listeners and values of the preferences.
*/
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
initGeneralPreferences()
when (activity.viewer) {
is PagerViewer -> initPagerPreferences()
is WebtoonViewer -> initWebtoonPreferences()
}
}
/**
* Init general reader preferences.
*/
private fun initGeneralPreferences() {
binding.viewer.onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
activity.presenter.setMangaViewer(position)
val mangaViewer = activity.presenter.getMangaViewer()
if (mangaViewer == ReaderActivity.WEBTOON || mangaViewer == ReaderActivity.VERTICAL_PLUS) {
initWebtoonPreferences()
} else {
initPagerPreferences()
}
}
binding.viewer.setSelection(activity.presenter.manga?.viewer ?: 0, false)
binding.rotationMode.bindToPreference(preferences.rotation(), 1)
binding.backgroundColor.bindToIntPreference(preferences.readerTheme(), R.array.reader_themes_values)
binding.showPageNumber.bindToPreference(preferences.showPageNumber())
binding.fullscreen.bindToPreference(preferences.fullscreen())
binding.keepscreen.bindToPreference(preferences.keepScreenOn())
binding.longTap.bindToPreference(preferences.readWithLongTap())
binding.alwaysShowChapterTransition.bindToPreference(preferences.alwaysShowChapterTransition())
binding.pageTransitions.bindToPreference(preferences.pageTransitions())
// If the preference is explicitly disabled, that means the setting was configured since there is a cutout
if (activity.hasCutout || !preferences.cutoutShort().get()) {
binding.cutoutShort.isVisible = true
binding.cutoutShort.bindToPreference(preferences.cutoutShort())
}
}
/**
* Init the preferences for the pager reader.
*/
private fun initPagerPreferences() {
binding.webtoonPrefsGroup.root.isVisible = false
binding.pagerPrefsGroup.root.isVisible = true
binding.pagerPrefsGroup.tappingPrefsGroup.isVisible = preferences.readWithTapping().get()
binding.pagerPrefsGroup.tappingInverted.bindToPreference(preferences.pagerNavInverted())
binding.pagerPrefsGroup.pagerNav.bindToPreference(preferences.navigationModePager())
binding.pagerPrefsGroup.scaleType.bindToPreference(preferences.imageScaleType(), 1)
binding.pagerPrefsGroup.zoomStart.bindToPreference(preferences.zoomStart(), 1)
binding.pagerPrefsGroup.cropBorders.bindToPreference(preferences.cropBorders())
}
/**
* Init the preferences for the webtoon reader.
*/
private fun initWebtoonPreferences() {
binding.pagerPrefsGroup.root.isVisible = false
binding.webtoonPrefsGroup.root.isVisible = true
binding.webtoonPrefsGroup.tappingPrefsGroup.isVisible = preferences.readWithTapping().get()
binding.webtoonPrefsGroup.tappingInverted.bindToPreference(preferences.webtoonNavInverted())
binding.webtoonPrefsGroup.webtoonNav.bindToPreference(preferences.navigationModeWebtoon())
binding.webtoonPrefsGroup.cropBordersWebtoon.bindToPreference(preferences.cropBordersWebtoon())
binding.webtoonPrefsGroup.webtoonSidePadding.bindToIntPreference(preferences.webtoonSidePadding(), R.array.webtoon_side_padding_values)
}
/**
* Binds a checkbox or switch view with a boolean preference.
*/
private fun CompoundButton.bindToPreference(pref: Preference<Boolean>) {
isChecked = pref.get()
setOnCheckedChangeListener { _, isChecked -> pref.set(isChecked) }
}
/**
* Binds a spinner to an int preference with an optional offset for the value.
*/
private fun Spinner.bindToPreference(pref: Preference<Int>, offset: Int = 0) {
onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
pref.set(position + offset)
}
setSelection(pref.get() - offset, false)
}
/**
* Binds a spinner to an enum preference.
*/
private inline fun <reified T : Enum<T>> Spinner.bindToPreference(pref: Preference<T>) {
val enumConstants = T::class.java.enumConstants
onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
enumConstants?.get(position)?.let { pref.set(it) }
}
enumConstants?.indexOf(pref.get())?.let { setSelection(it, false) }
}
/**
* Binds a spinner to an int preference. The position of the spinner item must
* correlate with the [intValues] resource item (in arrays.xml), which is a <string-array>
* of int values that will be parsed here and applied to the preference.
*/
private fun Spinner.bindToIntPreference(pref: Preference<Int>, @ArrayRes intValuesResource: Int) {
val intValues = resources.getStringArray(intValuesResource).map { it.toIntOrNull() }
onItemSelectedListener = IgnoreFirstSpinnerListener { position ->
pref.set(intValues[position]!!)
}
setSelection(intValues.indexOf(pref.get()), false)
}
}

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