Compare commits

...

293 Commits

Author SHA1 Message Date
c87ba6231d Release 0.10.7 2020-12-13 21:02:28 -05:00
c5ca739b49 Fix loading fallback thumbnails in browse view (closes #4127) 2020-12-13 20:47:48 -05:00
00fe4cdf2d Minor code cleanup 2020-12-13 18:11:18 -05:00
69be3e1e87 Complete migration off of Kotlin synthetics 2020-12-13 18:01:51 -05:00
2cb3984d68 Show MAL relogin message on update, localize error message 2020-12-13 17:52:21 -05:00
5901978889 Use view binding for date headers 2020-12-13 17:47:29 -05:00
8bf1cf3cc5 Update to coroutines 1.4.2
Should fix crashes on some devices. See https://github.com/Kotlin/kotlinx.coroutines/issues/2371
2020-12-13 17:18:59 -05:00
f6af1184bc Reword NSFW settings section 2020-12-13 17:18:38 -05:00
4880741b8b More crash fixes 2020-12-13 17:18:28 -05:00
e8627800fe Remove bundled fallback file picker 2020-12-13 12:42:10 -05:00
907fbb94a2 Require WebView 86+ 2020-12-13 11:27:05 -05:00
fd2028557e Some crash fixes 2020-12-13 11:00:46 -05:00
91fa1ec6b2 Suppress some deprecation warnings 2020-12-12 23:50:28 -05:00
628c525599 Disable release postprocessing (Proguard)
Caused the following error for someone:

Detected problmes with app native libraries (please consult log for detail):
libavcoded.so: text relocations
libswresample.so:test relocations
2020-12-12 23:50:05 -05:00
bbc00768f0 Note that toggling NSFW sources requires a restart 2020-12-12 17:27:11 -05:00
5b09461ccf Break out NSFW hiding/labeling into separate settings 2020-12-12 17:20:54 -05:00
1a439ecece Remove source overwrite logic since built-in sources no longer exist 2020-12-12 16:15:18 -05:00
836aec4396 Flip left/right key events for Webtoon viewer (fixes #4111) 2020-12-12 15:50:56 -05:00
0b5dec9bab Revert "Hide incomplete NSFW source labelling settings"
This reverts commit a3b1690d38.
2020-12-12 11:32:55 -05:00
fd56123267 [SKIP CI] Update issue templates 2020-12-12 11:32:44 -05:00
45ca470789 Release 0.10.6 2020-12-12 11:29:21 -05:00
a3b1690d38 Hide incomplete NSFW source labelling settings 2020-12-12 11:28:50 -05:00
a3bad75899 Force MAL logout on update 2020-12-12 11:26:42 -05:00
d1aaafbfff Update Gradle 2020-12-12 11:26:29 -05:00
93d4af99bf Weblate translations (#4078)
Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@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: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: HaruSasaki <aiqusubaru@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: Kurocon <weblate@kurocon.nl>
Co-authored-by: Mario Kevin D. A <programas013@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
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: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Woodyx <shiposhouyou@gmail.com>
Co-authored-by: Yassin <yassinelaoud@gmail.com>
Co-authored-by: f0roots <f0rootss@gmail.com>
Co-authored-by: sunbeams001 <sunbeams001@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
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/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/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/fr/
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/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/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/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translation: Tachiyomi/Strings

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: Ava <Sasu.ruotsalainen@live.fi>
Co-authored-by: C201 <derasetad@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: HaruSasaki <aiqusubaru@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Mario Kevin D. A <programas013@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
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: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Woodyx <shiposhouyou@gmail.com>
Co-authored-by: Yassin <yassinelaoud@gmail.com>
Co-authored-by: f0roots <f0rootss@gmail.com>
Co-authored-by: sunbeams001 <sunbeams001@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2020-12-12 10:53:04 -05:00
c950595fe3 Revert "Distinguish between no results and loading when sorting global search results"
This reverts commit 2be9871d05.
2020-12-11 21:58:04 -05:00
8ffd3a8ed2 Don't scroll to top when navigating back/forward in WebView 2020-12-10 09:15:09 -05:00
b6e246c6b2 Add text selection shortcut to invoke global search (closes #4058) 2020-12-08 22:48:04 -05:00
59859e124f Update to Kotlin 1.4.21 and kotlinter 3.3.0 2020-12-08 22:31:48 -05:00
2bb7a33bc3 Use WebView auth flow for MAL (fixes #4100) 2020-12-08 22:21:23 -05:00
c2b8fea291 Update libraries, some lints (#4099)
* Update some plugins

* Fix some miscellaneous lints
2020-12-08 22:21:08 -05:00
08ab7f6aa0 Update to Kotlin 1.4.20 2020-12-07 22:43:18 -05:00
560f0bba5c Use more up-to-date fork of junrar 2020-12-07 22:13:53 -05:00
722437a022 Update AndroidX and Material Components dependencies 2020-12-02 17:02:12 -05:00
8a44b1dabe Add Galician locale 2020-11-29 17:14:05 -05:00
b39191ff50 Fix annotations package name 2020-11-29 17:08:20 -05:00
9814d20404 Weblate translations (#3997)
Co-authored-by: AXEL IVAN GARCIA BERNAL <ga419623@uaeh.edu.mx>
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: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Aria Moradi <aria.moradi007@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: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: HaruSasaki <aiqusubaru@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: Johkum <jacobomur@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Milo Ivir <mail@milotype.de>
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: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Simon Mattila <simon.mattila@protonmail.com>
Co-authored-by: The Pumpkin God <sbh13112002@gmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Tristan Garnier <espeletpro@gmail.com>
Co-authored-by: Woodyx <shiposhouyou@gmail.com>
Co-authored-by: Yassin El Aoud <yassinelaoud@gmail.com>
Co-authored-by: george k <norhorn@gmail.com>
Co-authored-by: Николаев Павел Дмитриевич <pavliknikolaev128@gmail.com>
Co-authored-by: Роман <Rozhenkov69@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/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/es/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/
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/fr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/gl/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/he/
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/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/
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/sah/
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/
Translation: Tachiyomi/Strings

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: AXEL IVAN GARCIA BERNAL <ga419623@uaeh.edu.mx>
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: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Andreas E <andreas.everos@gmail.com>
Co-authored-by: Aria Moradi <aria.moradi007@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: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eric <spice2wolf@gmail.com>
Co-authored-by: HaruSasaki <aiqusubaru@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: J. Lavoie <j.lavoie@net-c.ca>
Co-authored-by: Johkum <jacobomur@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Milo Ivir <mail@milotype.de>
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: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Simon Mattila <simon.mattila@protonmail.com>
Co-authored-by: The Pumpkin God <sbh13112002@gmail.com>
Co-authored-by: Tooster <max@polarczyk.pl>
Co-authored-by: Tristan Garnier <espeletpro@gmail.com>
Co-authored-by: Woodyx <shiposhouyou@gmail.com>
Co-authored-by: Yassin El Aoud <yassinelaoud@gmail.com>
Co-authored-by: george k <norhorn@gmail.com>
Co-authored-by: Николаев Павел Дмитриевич <pavliknikolaev128@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2020-11-29 17:08:01 -05:00
6664dfb048 Use regular minified extensions JSON instead of explicitly handling gzip encoding
Turns out GitHub *does* do gzip encoding by default, it just doesn't show up in the header logs for some reason...
2020-11-29 16:01:16 -05:00
3133a63cf8 Fix crash when a preference doesn't have a title (e.g. info preference) 2020-11-29 15:01:13 -05:00
c9c0f3d014 Enhance incognito mode (#4073)
* When in Incognito Mode don't set lastUsedSource

* When in Incognito Mode don't save chapter progress

Still allows tracking and mark as read when reaching last page

* When in Incognito Mode don't mark as read (overwritten if hasTrackers)
2020-11-29 11:15:15 -05:00
ff66f307dd Tweak default dark theme colors (#4074) 2020-11-29 11:14:12 -05:00
e048d66f74 Revert edge to edge
This was a bigger headache than it was worth. To note, phone landscape wasn't considered, so the navbar overlaps the content on the side. Additionally, the ability to programatically control the navbar scrim is missing, so it'd have to manually be added.
2020-11-29 11:08:06 -05:00
66e3fa7df8 Replace more Kotlin synthetics 2020-11-29 11:07:06 -05:00
019a0f31c7 Replace more Kotlin synthetics 2020-11-28 15:54:53 -05:00
749c2071af Replace Kotlin synthetic views in viewholders 2020-11-28 15:24:40 -05:00
322d66d282 Replace more usages of Kotlin synthetic views 2020-11-28 14:56:57 -05:00
aa98cd0da0 Fix keyboard sometimes affecting bottom inset padding 2020-11-28 13:02:15 -05:00
c8316c7254 Move NSFW source setting into Browse settings 2020-11-28 12:43:17 -05:00
6b9180844d Fall back to filename in backup creation notification if path not available 2020-11-27 21:38:26 -05:00
c0e4863229 Fix restore in Android 11 2020-11-27 21:36:42 -05:00
2be9871d05 Distinguish between no results and loading when sorting global search results 2020-11-27 21:09:44 -05:00
776f6a9a16 More bottom inset padding fixes 2020-11-27 21:05:06 -05:00
10163aab21 Use jcenter to get 1.x dependencies 2020-11-26 22:39:26 -05:00
60b2a4ea9d Handle bottom insets in FAB controllers too 2020-11-22 23:08:41 -05:00
56e1e3e205 Further abstract how bottom navbar padding is applied 2020-11-22 22:57:58 -05:00
0f805cd45e Handle bottom navbar padding when drawing edge to edge 2020-11-22 22:48:38 -05:00
1d7c692e89 Create extension function for applying system insets 2020-11-22 22:30:25 -05:00
38bc8ec6b4 Add global search progress bar (closes #3727) 2020-11-22 16:54:19 -05:00
2154e3aa2d Remove 10 results per source limit from global search (still limited to 1 page) 2020-11-22 16:40:53 -05:00
56c19e57a9 Add help shortcut for source migration (closes #3951) 2020-11-22 16:30:20 -05:00
d548c690d6 Fix transparent navbar when bottom sheets are opened (fixes #3848) 2020-11-22 15:49:57 -05:00
3fa70dade3 Use ViewCompat for setting MainActivity inset paddings 2020-11-22 15:13:12 -05:00
368c30a2cc More restore code cleanup, remove some SY-specific logic 2020-11-22 12:43:23 -05:00
5539e4591f Update Firebase 2020-11-22 12:40:01 -05:00
781971ee81 Set preview/experimental compiler flags instead of using annotations 2020-11-22 11:43:16 -05:00
1140316d1b Pad bottom of MoreController so bottom nav doesn't overlap content 2020-11-21 18:11:17 -05:00
cf6c48744a Some more code cleanup 2020-11-21 15:30:04 -05:00
eed6db8e92 Default automatic legacy backups to true for now 2020-11-21 15:12:04 -05:00
858664bfd7 Some more code cleanup 2020-11-21 15:09:56 -05:00
eceac4d6e3 Merge restore entry points, avoid unnecessary new strings 2020-11-21 12:35:04 -05:00
47a172df1f Remove "full_" from v3 backup default file names 2020-11-21 12:22:46 -05:00
f2c0732c40 Minor cleanup 2020-11-21 12:22:25 -05:00
682fae12b6 Make a protobuf based backup system (#3936)
* Make a protobuf based backup system

* Cleanup

* More cleanup

* Fix restores always loading the full backup restore, even when legacy restore was used

* Make offline the default

(cherry picked from commit f6fd8a8ddb90869f3e28fd8fcd81a2125f8e0527)

* Find chapter based on the url

(cherry picked from commit 326dc2700944a60da381d82cd9782c5f0d335902)

* Dont break after finding one chapter

(cherry picked from commit f91d1af37398619cf371e4920b60f6d309799c74)

* Also apply changes to online restore

(cherry picked from commit e7c16cd0d14ea5d50ce4a9a3dfa8ca768be702f2)

* Rewrite backup categories

(cherry picked from commit f4200e2146a9c540675767206ed4664894aa1216)

* Dedupe some code, move over read and bookmarks properly

(cherry picked from commit d9ce86aca66945c831670a1523d8bc69966312df)

* Move some functions to the abstract backup manager

(cherry picked from commit b0c658741a2f506bc31823f1f0347772bc119d2e)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/full/FullBackupManager.kt
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt

* Fix some backup duplication issues

(cherry picked from commit a4a1c2827c4537d2d07a0cb589dc1c3be1d65185)

# Conflicts:
#	app/src/main/java/eu/kanade/tachiyomi/data/backup/legacy/LegacyBackupManager.kt

* Fix a missed bundleOf

* So glad this wasnt merged before now, everything should be working with this commit
2020-11-20 22:34:24 -05:00
a150762c63 Fix manga info description scrim on Android 5 2020-11-20 22:28:06 -05:00
2695bdddf8 Fix non-transparent navigation bar 2020-11-20 22:28:06 -05:00
c9b1a425a7 add support for S Pen actions (#4029) 2020-11-20 22:25:24 -05:00
122b2b1a8e Draw under navbar in Android 9+ 2020-11-15 13:30:49 -05:00
2351c1b426 Update AGP for AS 4.1.1 2020-11-15 13:14:12 -05:00
558c4ada06 [SKIP CI] Use non-fork version of APK signing action 2020-11-12 19:32:14 -05:00
779fd9c61a Add workflow steps for creating releases 2020-11-12 19:04:18 -05:00
0b5128dfd1 Remove TravisCI build config 2020-11-12 18:49:27 -05:00
c0519e8670 Remove a stray trailing dash 2020-11-11 13:40:07 -05:00
fd545db1bd Update dependencies 2020-11-11 13:39:50 -05:00
6991c224b2 Tweak manga info header 2020-11-07 19:22:20 -05:00
7dc70c9eab Add 1.x page model converters 2020-11-07 19:22:01 -05:00
e32445f2cf Hide locale subtitle for local source 2020-11-07 19:21:42 -05:00
8aa6486bf7 Update to serialization 1.0.1 and coroutines 1.4.1 2020-11-04 09:36:00 -05:00
d21c147203 Singleton instance of Json serializer 2020-11-01 15:12:16 -05:00
9b10e851d1 Fix Bangumi login (fixes #3994) 2020-11-01 15:00:21 -05:00
6675508b24 Update dependencies 2020-10-31 10:14:25 -04:00
7310ec4fe4 Use 1.x API to fetch manga details in browse view 2020-10-31 10:10:52 -04:00
b1ce3693ed Change list view font size back to 16sp (#3978)
Make list view font size bigger again
2020-10-30 22:21:55 -04:00
deb1ed5623 Add Uzbek locale 2020-10-30 17:55:23 -04:00
0902d7cca9 Weblate translations (#3895)
Co-authored-by: Adam <bessaa935@gmail.com>
Co-authored-by: Ashraf Is <rediancool@gmail.com>
Co-authored-by: Ashraf Isl <rediancool@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Credits125Cre <credits125@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eva Godoy <godoytraduccion@gmail.com>
Co-authored-by: Hara Desu <aqjbgr09@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Jozef Hollý <j2.00ghz@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Luka Paun <croluxgame@gmail.com>
Co-authored-by: Lyaiya <hipsnafoha@outlook.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mubarek Seyd Juhar <mubareksej@gmail.com>
Co-authored-by: Nico <nicknicola94@gmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Pxeexe <johongira192@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Ruud Schouten <ruud.schouten@outlook.com>
Co-authored-by: Salem Almashaikhi <salemnabil1@outlook.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: f0roots <f0rootss@gmail.com>
Co-authored-by: opb <myniceaddress@outlook.com>
Co-authored-by: İlle <derasetad@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/am/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ar/
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/cs/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/
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/ko/
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/ro/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/
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/Strings

Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Adam <bessaa935@gmail.com>
Co-authored-by: Ashraf Is <rediancool@gmail.com>
Co-authored-by: C201 <derasetad@gmail.com>
Co-authored-by: Credits125Cre <credits125@gmail.com>
Co-authored-by: Eduard Ereza Martínez <eduard@ereza.cat>
Co-authored-by: Eva Godoy <godoytraduccion@gmail.com>
Co-authored-by: Hara Desu <aqjbgr09@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Luka Paun <croluxgame@gmail.com>
Co-authored-by: Lyaiya <hipsnafoha@outlook.com>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mubarek Seyd Juhar <mubareksej@gmail.com>
Co-authored-by: Nico <nicknicola94@gmail.com>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: Pxeexe <johongira192@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Ruud Schouten <ruud.schouten@outlook.com>
Co-authored-by: Salem Almashaikhi <salemnabil1@outlook.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: f0roots <f0rootss@gmail.com>
Co-authored-by: opb <myniceaddress@outlook.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2020-10-30 17:52:47 -04:00
95ec903862 Fix Bangumi API response deserialization (fixes #3977) 2020-10-29 17:51:18 -04:00
2ab6af6471 Consume and extend 1.x Source API
TODO: make the rest of the app actually call the 1.x functions
2020-10-26 10:52:28 -04:00
9493577de2 Replace language code in Global Search with secondary text (#3972)
Replace language code in Global Search with secondary text
2020-10-25 15:25:58 -04:00
837ce62844 Janky workaround for collapsed global search covers 2020-10-24 13:31:29 -04:00
2860bbfb12 Minor dependency updates 2020-10-24 13:17:14 -04:00
a2b1acd70f Update Global search layout to match Library and Browse (#3892)
* Update Global Search card item to be comfortable with rounded corners

* Remove card background
2020-10-24 13:07:06 -04:00
f1350bc33e Fix missing chapter warning when chapter number is not recognized (#3928)
* Fix missing chapter warning when chapter number is not recognized

* Add case where ch number is recognized as zero but has no ch number

Yes this will ignore ch 0

* Use RegEx to double check if ch. name contains potential ch. number
2020-10-24 13:06:23 -04:00
6af0eb4068 Fix Shikimori login (closes #3966) 2020-10-23 18:02:48 -04:00
538c168641 Use default height for extended FAB (closes #3949) 2020-10-18 10:21:03 -04:00
832a4fa68e Lower minWidth for extended FAB (fixes #3939) 2020-10-13 17:38:56 -04:00
ccb727529d Add Proguard rules for kotlinx.serialization 2020-10-13 17:34:39 -04:00
ed41604f56 Show locale in sources list items 2020-10-12 17:35:08 -04:00
9f05d563f9 Darken the description colors (#3858)
* Darken the description colors

* Restore UpdatesHolder.kt

* Use the same color as the Extensions subtitles
2020-10-12 16:59:44 -04:00
72d114d46a Use secondary color for manga info header action button default state 2020-10-12 16:59:05 -04:00
99b96d80d0 Auto-formatting 2020-10-12 16:58:48 -04:00
67418ba853 Update AGP for Android Studio 4.1 2020-10-12 16:49:11 -04:00
60755d0c26 Use some more KTX and Compat stuff (#3933)
* Use more KTX extensions

* Update Android Studio

* Use more Compat classes
2020-10-12 16:46:25 -04:00
a689e4e041 Trust user-added CAs (closes https://github.com/inorichi/tachiyomi-extensions/issues/4581) 2020-10-11 16:03:24 -04:00
f5aa36c787 Migrate to kotlinx.serialization for Bangumi 2020-10-11 15:58:59 -04:00
f8d82cb052 Migrate to kotlinx.serialization for Shikimori 2020-10-11 15:58:43 -04:00
980feb6c96 Migrate to kotlinx.serialization for download store and deleter 2020-10-11 15:26:57 -04:00
e7d6605490 Migrate to kotlinx.serialization for Anilist 2020-10-11 15:26:30 -04:00
7a476abb53 Migrate to kotlinx.serialization for extensions and update fetching 2020-10-11 14:53:02 -04:00
19581792fc [SKIP CI] Remove dev build logic from Travis CI config (#3873) 2020-10-11 12:31:33 -04:00
aadc6a56cd [SKIP CI] Replace dead link in README with correct one (#3929) 2020-10-11 12:13:27 -04:00
b88e444cbc Bump minimum WebView version to 84 2020-10-10 16:35:08 -04:00
6c792d2821 Update dependencies 2020-10-10 16:34:06 -04:00
9afb445620 Update AboutLibraries 2020-10-10 16:30:26 -04:00
efc951191d History fixes (#3921)
(cherry picked from commit 776a4b2a2414f5f35b28f5d8c830baca6351e123)
2020-10-10 16:05:29 -04:00
a249373bf5 Cancel previous CI workflow runs 2020-10-08 21:28:07 -04:00
e1eb030b18 Use eskatos/gradle-command-action instead of manually handling caching 2020-10-08 21:24:59 -04:00
6aea0f48ed skip ci in commit message condition check (#3902)
* skip ci condition check

* always Validate Gradle Wrapper

* naming workflow jobs
2020-10-08 21:24:14 -04:00
e637f22540 Only run build check workflow for pushes to dev or master branches 2020-10-04 18:26:17 -04:00
842295348e Add Amharic locale 2020-10-04 13:10:40 -04:00
2992a0f4d8 Translations (Continuous) (#3886)
* Weblate translations

Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alessandro Zangrandi <alessandro@mzit.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
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: Félix D <felix.desmaretz@protonmail.com>
Co-authored-by: Hara Desu <aqjbgr09@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Josh Ciardi <jbciardi@gmail.com>
Co-authored-by: Juan Aranda <juan50055005@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mubarek Juhar <mubareksej@gmail.com>
Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Co-authored-by: Nin Gun <luis_noxer@hotmail.es>
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: Redya Rachmandanu <redyarachmandanu@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Salem Almashaikhi <salemnabil1@outlook.com>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Simon Mattila <simon.mattila@protonmail.com>
Co-authored-by: Yassin El Aoud <yassinelaoud@gmail.com>
Co-authored-by: abidin toumi <abidin24@disroot.org>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: f0roots <f0rootss@gmail.com>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: İlle <derasetad@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/am/
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/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/en_US/
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/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/kn/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
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/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/Strings

* Deleted translation using Weblate (Belarusian)

* Deleted translation using Weblate (Bengali (Bangladesh))

* Deleted translation using Weblate (Spanish (Mexico))

* Deleted translation using Weblate (Tigrinya)

* Deleted translation using Weblate (English (United States))

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: Alessandro Zangrandi <alessandro@mzit.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Allan Nordhøy <epost@anotheragency.no>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
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: Félix D <felix.desmaretz@protonmail.com>
Co-authored-by: Hara Desu <aqjbgr09@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Josh Ciardi <jbciardi@gmail.com>
Co-authored-by: Juan Aranda <juan50055005@gmail.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Lzmxya <lzmxya@gmail.com>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Mubarek Juhar <mubareksej@gmail.com>
Co-authored-by: Nikita Epifanov <nikgreens@protonmail.com>
Co-authored-by: Nin Gun <luis_noxer@hotmail.es>
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: Redya Rachmandanu <redyarachmandanu@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Salem Almashaikhi <salemnabil1@outlook.com>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Shashank Pujari <shashankppujari@gmail.com>
Co-authored-by: Simon Mattila <simon.mattila@protonmail.com>
Co-authored-by: Yassin El Aoud <yassinelaoud@gmail.com>
Co-authored-by: abidin toumi <abidin24@disroot.org>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: f0roots <f0rootss@gmail.com>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: ssantos <ssantos@web.de>
Co-authored-by: İlle <derasetad@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2020-10-04 12:55:18 -04:00
e8f5963a57 Workflow improvements (#3891)
* add caching steps to build_check workflow

* add gradle.properites for github runner

* refactor ci-gradle.properties

* specify ndk versiion in build.gradle

* remove ndk install and cache steps from build_check

* moved runner files to separate folder

* refactor build_check.yml
2020-10-04 12:44:04 -04:00
4cbe497770 Prevent tag search if browsing from latest (sort of closes #3625) 2020-10-03 13:04:38 -04:00
76a53097b1 Update issue_closer.yml (#3885)
Limit the issue_closer workflow to run only when the issue is opened, edited or reopened
2020-10-03 12:58:22 -04:00
0904692f15 Set activated states for manga info favorite/tracker buttons 2020-10-03 12:55:51 -04:00
65bacd288b Reorder chapter filters to better match library filters 2020-10-03 12:32:49 -04:00
11ab3b2c2e Remove from queue after read (#3870)
* Add option to remove chapter from download queue after marked as read

* Begone bug

There was a bug when one queued a new download to the downloader. Which resulted in getChapterDownloadOrNull() would not find the corresponding download for the provided chapter

* Fix review comment

* Remove preference and just remove from queue
2020-10-03 12:29:51 -04:00
812368e332 Ability to delete reading history (#3871)
* Added ability to delete history for all manga

* Fix review comments
2020-10-03 10:54:42 -04:00
cf39ae0000 Add banner for Incognito Mode 2020-10-03 10:53:26 -04:00
7194f65203 Show downloaded only banner in all views 2020-10-03 10:48:53 -04:00
4b78ff324d Minor string cleanup for Incognito Mode 2020-10-03 10:44:34 -04:00
79ccfcd553 [Feature Request] Option to turn off History - #3772 (#3855)
* Implementing do not save chapter history preference

* Refactor of labels

* Changing incognito mode from Readers section to More

* Fixing imports and removing unused strings
2020-10-03 10:43:01 -04:00
2df6a4dde8 Update AndroidX dependencies 2020-10-03 10:18:39 -04:00
e88cbc2769 Fix deleting history not refreshing the view (#3882)
Fix deleting history not refreshing the view
2020-10-02 13:19:16 -04:00
25d1c40cda Fix visual issue with bottom padding in filters sheet (#3881) 2020-10-02 09:02:34 -04:00
969b57ade9 Update subsampling-scale-image-view 2020-09-30 22:55:13 -04:00
bddeb86223 Fix color filter sheet content alignment (#3862)
Fix color filter sheet content alignment
2020-09-29 12:27:58 -04:00
b5986b509e Fix chapter filter icon tinting 2020-09-27 19:16:46 -04:00
9d2adcd512 Add infinite history and search history (#3827)
* Add infinite history and search history

* Cleanup code
2020-09-27 18:17:14 -04:00
fb3756420b Use tristate checkboxes for chapters list filters 2020-09-27 18:13:20 -04:00
2eab43a669 Fix scrolled content being visible behind navigation view group 2020-09-27 18:06:27 -04:00
caeab0a63b Clean up bottom nav bar scrolling behavior toggling 2020-09-27 13:42:49 -04:00
7c69b1b649 Tweak navigation view group header style 2020-09-27 13:41:25 -04:00
3784d1a8f2 Add option to hide bottom bar when scrolling (#3758)
* [wip] hardcoded flag for hide on scroll

* add corresponding option in settings

* clean up a little

Co-authored-by: arkon <arkon@users.noreply.github.com>
2020-09-27 13:25:30 -04:00
458e761b45 Add ability to choose what to remove for selected mangas (#3796)
* [wip] started to change manga delete options

* Change dialog for deleting selected manga

* Move array to appropriate file

* Make first option enabled by default

* Use radio buttons to reduce amount of clicks

* Revert "Use radio buttons to reduce amount of clicks"
2020-09-27 13:03:04 -04:00
371b0b2132 Color selected bottom nav item with accent color when theme isn't blue 2020-09-26 19:27:39 -04:00
5d1ca64768 Use same padding as source icons for extensions 2020-09-26 19:23:13 -04:00
972a595c74 Color selected tab with accent color when theme isn't blue 2020-09-26 19:18:58 -04:00
a3c598a3e1 Remove card backgrounds from browse lists 2020-09-26 19:03:46 -04:00
9ce8c5c160 Use background color for list view 2020-09-26 18:33:39 -04:00
79bbc99882 Shorten settings search package name 2020-09-26 18:27:12 -04:00
31867362dc Update Ben Manes versions lib (#3843)
Update Ben Manes versions lib
2020-09-25 08:22:13 -04:00
3bce07e873 Minor settings search code cleanup 2020-09-22 22:33:43 -04:00
766f9e37b5 #3520 Searchable Settings (#3683)
* Adding class stubs for settings search, UI elements.

* -  implement searchable settings
- `SettingsController.setupPreferenceScreen` must return a PreferenceScreen

* Remove unneeded SettingsControllerFactory.

* Set query hint, clean up code smell.

* Add search button to MoreController, stop infinite recursion.

* - initialize SearchResultCollection once in Activity.onCreate

* - implement prefernce highlighting after settings search

* - Ensure all Preferences have a key set or else the highlighting effect will have no effect on it.
- remove ExtensionFilterController and SourceFilterController from settingControllersList in SettingsSearchHelper, since those are related to Extensions and not Settings

* Limiting search to settings menu only, localized breadcrumb string, and code cleanup after code review.

* - moved call to SettingsSearchHelper.initPreferenceSearchResultCollection() into SettingsSearchController

* Code review cleanup and refactoring.

* Inlined non-reused key strings.

* Adding more UI polish, add comments for future enhancements.

* - retain search query when navigating *away* from SettingsSearchController
- keep `searchItem` in `expandActionView` state until user goes back (fixes the empty view in `SettingsSearchSearchController` issue)

Co-authored-by: mpm11011 <markuscicero5@gmail.com>
Co-authored-by: lmj0011 <9396189+lmj0011@users.noreply.github.com>
2020-09-22 22:23:38 -04:00
a9bed90d02 Move manga info summary more/less button toggle 2020-09-22 22:20:29 -04:00
01ad405dd2 Center reader transition text properly (fixes #3819) 2020-09-20 12:56:04 -04:00
8cef35f4a0 Update build status badge in README 2020-09-20 12:45:07 -04:00
274f0edd76 Perform build check on push, move Gradle wrapper validation into build action 2020-09-20 10:39:54 -04:00
88aea311f8 Remove support for backup v1 2020-09-20 10:35:05 -04:00
477aedbffa Minor updates 2020-09-20 10:34:22 -04:00
b898442fe3 Add sah locale (closes #3818) 2020-09-20 10:31:29 -04:00
528c1b90c2 Merge branch 'dev' into dev-settings-search 2020-09-16 14:56:34 -04:00
205c1e5170 Merge pull request #5 from lmj0011/dev-settings-search
more changes
2020-09-16 14:53:24 -04:00
Jay
004e1c98ee Send manga/chapter/page details when sharing a chapter page 2020-09-15 17:59:47 -04:00
7641bb4d0d Fix downloaded only filter 2020-09-15 17:53:01 -04:00
687f3d48ea Tri-state library filters (closes #1814)
Based on https://github.com/inorichi/tachiyomi/pull/2127.

Co-authored-by: hXtreme <hXtreme@users.noreply.github.com>
2020-09-14 17:52:00 -04:00
da5f10a2f1 Clean up global chapter settings code 2020-09-14 16:23:42 -04:00
64050e8266 [feature] add ability to set global filter/sort/display for Manga chapters (#3622)
* - [feature] add ability to set global filter/sort/display for Manga chapters

* - move default chapter settings functionality to overflow menu
- code clean up

* - show confirmation dialog when user selects "Set as Default" option in Chapter Settings

* - hide overflow menu in LibrarySettingsSheet

* - apply default chapter settings if manga is added to Library from a Source's browsing screen

Co-authored-by: arkon <arkon@users.noreply.github.com>
2020-09-14 15:58:34 -04:00
791a7d5a01 Start updating manga info view header design 2020-09-14 15:56:27 -04:00
13930d3706 Revert "Hide parental controls section for release"
This reverts commit 76c795d0d0.
2020-09-14 10:48:22 -04:00
2769e27a2a Release 0.10.5 2020-09-14 10:46:59 -04:00
76c795d0d0 Hide parental controls section for release 2020-09-14 10:46:46 -04:00
b20bced3ca Fix Kotlinter name typo 2020-09-14 10:46:31 -04:00
4f2da9a78f Fix Chinese plurals 2020-09-14 10:46:14 -04:00
8e0ba3650b Translated using Weblate (Bulgarian) (#3646)
Currently translated at 99.4% (574 of 577 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Italian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (French)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Chuvash)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (German)

Currently translated at 100.0% (577 of 577 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (French)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (German)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

Translated using Weblate (Italian)

Currently translated at 98.9% (570 of 576 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (French)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Arabic)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Yakut)

Currently translated at 87.3% (503 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Bulgarian)

Currently translated at 96.7% (557 of 576 strings)

Translated using Weblate (Chuvash)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Vietnamese)

Currently translated at 86.2% (497 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/vi/

Translated using Weblate (Japanese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (German)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Arabic)

Currently translated at 95.4% (550 of 576 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Indonesian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Hindi)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Romanian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Yakut)

Currently translated at 85.2% (491 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Croatian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Sardinian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.6% (574 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chuvash)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Hindi)

Currently translated at 99.3% (572 of 576 strings)

Translated using Weblate (German)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (576 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/

Translated using Weblate (Greek)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese)

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (French)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Latvian)

Currently translated at 33.6% (194 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lv/

Translated using Weblate (Arabic)

Currently translated at 91.8% (529 of 576 strings)

Translated using Weblate (Latvian)

Currently translated at 33.3% (192 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lv/

Translated using Weblate (Latvian)

Currently translated at 33.3% (192 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/lv/

Translated using Weblate (French)

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (Croatian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Japanese)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.7% (569 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/

Translated using Weblate (Indonesian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Croatian)

Currently translated at 99.4% (573 of 576 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Catalan)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Bengali (Bangladesh))

Currently translated at 0.5% (3 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn_BD/

Translated using Weblate (Filipino)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Japanese)

Currently translated at 97.7% (563 of 576 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (576 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/

Translated using Weblate (Turkish)

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Finnish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chuvash)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Hindi)

Currently translated at 99.1% (571 of 576 strings)

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Russian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Polish)

Currently translated at 96.8% (558 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pl/

Translated using Weblate (Malay)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Bengali)

Currently translated at 57.6% (332 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn/

Translated using Weblate (Bengali)

Currently translated at 57.6% (332 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn/

Added translation using Weblate (Bengali (Bangladesh))

Translated using Weblate (Yakut)

Currently translated at 80.2% (462 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Russian)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Bengali)

Currently translated at 58.3% (336 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn/

Translated using Weblate (Sardinian)

Currently translated at 99.6% (574 of 576 strings)

Translated using Weblate (Turkish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.0% (565 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Greek)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chuvash)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Filipino)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (576 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (576 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/

Translated using Weblate (Finnish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Dutch)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Indonesian)

Currently translated at 98.9% (570 of 576 strings)

Translated using Weblate (Spanish)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Chuvash)

Currently translated at 99.6% (574 of 576 strings)

Translated using Weblate (Malay)

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 90.4% (521 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/

Translated using Weblate (Malay)

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (Swedish)

Currently translated at 100.0% (576 of 576 strings)

Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (576 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.8% (575 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (574 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.6% (574 of 576 strings)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 99.4% (573 of 576 strings)

Translated using Weblate (German)

Currently translated at 100.0% (576 of 576 strings)

Co-authored-by: Ajeje Brazorf <lmelonimamo@yahoo.it>
Co-authored-by: Alessandro Jean <alessandrojean@gmail.com>
Co-authored-by: Alessandro Zangrandi <alessandro@mzit.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
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: Gabriel Lebis <gableb@hotmail.fr>
Co-authored-by: George <georgeramzy37@gmail.com>
Co-authored-by: Hara Desu <aqjbgr09@gmail.com>
Co-authored-by: Hosted Weblate <hosted@weblate.org>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: RealKC <mitrut.e.super@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Whod <whodizhod@gmail.com>
Co-authored-by: Yassin El Aoud <yassinelaoud@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: İlle <derasetad@gmail.com>
Co-authored-by: Роман <Rozhenkov69@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/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/es/
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/ms/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/
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/tr/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/
Translation: Tachiyomi/Strings

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: Alessandro Zangrandi <alessandro@mzit.it>
Co-authored-by: Alex <linuxrf@gmail.com>
Co-authored-by: Ava <Sasu.ruotsalainen@live.fi>
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: Gabriel Lebis <gableb@hotmail.fr>
Co-authored-by: George <georgeramzy37@gmail.com>
Co-authored-by: Hara Desu <aqjbgr09@gmail.com>
Co-authored-by: Huang Zhiyi <hzy980512@126.com>
Co-authored-by: Kurocon <weblate@kurocon.nl>
Co-authored-by: Marco Santos <enum.scima@gmail.com>
Co-authored-by: Matteo Gaeta <matteo.gaeta.1998@gmail.com>
Co-authored-by: Milo Ivir <mail@milotype.de>
Co-authored-by: Paulo Pinho <kebrus@gmail.com>
Co-authored-by: Pitpe11 <giorgos2550@gmail.com>
Co-authored-by: RealKC <mitrut.e.super@gmail.com>
Co-authored-by: Rostyslav <info@ubilling.net.ua>
Co-authored-by: Samuel Carvalho de Araújo <samuelnegro12345@gmail.com>
Co-authored-by: Whod <whodizhod@gmail.com>
Co-authored-by: Yassin El Aoud <yassinelaoud@gmail.com>
Co-authored-by: darkbeast13 <nikhil15mps@gmail.com>
Co-authored-by: monolifed <monolifed@protonmail.com>
Co-authored-by: İlle <derasetad@gmail.com>
Co-authored-by: Роман <Rozhenkov69@gmail.com>
2020-09-14 10:44:21 -04:00
76f6fe4601 Use Kolinter Gradle plugin for linting instead of ktlint directly 2020-09-13 18:48:20 -04:00
ca1373f36b Check GitHub for preview release updates instead of inorichi's server 2020-09-13 10:53:51 -04:00
fc6c2e083d Update preview build links in README 2020-09-12 18:49:01 -04:00
c0789cd6ba Use background color for some lists 2020-09-12 15:40:40 -04:00
af47103707 Replace deprecated system window insets usage 2020-09-12 15:39:51 -04:00
c466baaa25 Remove list dividers 2020-09-12 15:39:26 -04:00
670294a427 Update OkHttp and Conscrypt 2020-09-11 21:55:44 -04:00
21ddae6a86 Update to Kotlin 1.4.10 2020-09-10 18:08:41 -04:00
9f260c3513 Always show missing chapter warning if there are missing chapters (#3755)
* Always show missing chapter warning if there are missing chapters

* Change function parameter names
2020-09-07 16:40:05 -04:00
18061d1077 - retain search query when navigating *away* from SettingsSearchController
- keep `searchItem` in `expandActionView` state until user goes back (fixes the empty view in `SettingsSearchSearchController` issue)
2020-09-06 23:59:41 -05:00
381c061ebc Adding more UI polish, add comments for future enhancements. 2020-09-06 11:32:03 -04:00
d37341d7d0 Inlined non-reused key strings. 2020-09-05 16:20:45 -04:00
a5098e5b5b Code review cleanup and refactoring. 2020-09-05 14:28:41 -04:00
b55d394a1f Fix text alignment in transition view when no more chapters available 2020-09-05 10:31:49 -04:00
5e2e177aa9 Add spacing on top of sources/extensions/migrate lists (#3751) 2020-09-04 16:28:22 -04:00
86e59977de Refactor common chapter transition views into separate view 2020-09-04 16:25:08 -04:00
66baf01e43 Localize "No chapters found" error 2020-09-04 15:21:09 -04:00
7a33e198dc Add missing chapter warning (#3745)
* Add missing chapter warning

* Flip calculation instead of flipping variables

* Change logic

* Change tint based on reader theme

* Add missing chapter warning to WebtoonTransitionHolder

* Add chapter warning between current/finished and prev/next

* Fix mix up of TextViews

* Fix review comments
2020-09-03 22:21:19 -04:00
4b493ebbaf Change sources sort to case-insensitive (#3743) 2020-09-03 22:20:27 -04:00
565e8cf00b Remove unused string, fix improperly formatted Slovak string 2020-09-03 22:19:28 -04:00
738a3999b4 Update Conscrypt 2020-09-03 22:18:56 -04:00
6f4b84c8fb Merge pull request #4 from lmj0011/dev-settings-search
moved call to SettingsSearchHelper.initPreferenceSearchResultCollection() into SettingsSearchController
2020-09-03 11:29:06 -04:00
29ab99aa1f - moved call to SettingsSearchHelper.initPreferenceSearchResultCollection() into SettingsSearchController 2020-08-30 22:21:32 -05:00
d53719b79e Limiting search to settings menu only, localized breadcrumb string, and code cleanup after code review. 2020-08-30 16:35:21 -04:00
f10fe8bf02 Merge pull request #3 from lmj0011/dev-settings-search-highlighter
implement preference highlighting after settings search
2020-08-30 12:19:32 -04:00
d7cfe1990c Merge pull request #2 from lmj0011/dev-settings-search
initialize SearchResultCollection once in Activity.onCreate
2020-08-30 12:19:04 -04:00
8bedc8f456 Move share manga button to toolbar menu 2020-08-29 17:13:02 -04:00
d9000f6fd1 Update dependencies 2020-08-29 17:12:50 -04:00
50c7b32b00 - Ensure all Preferences have a key set or else the highlighting effect will have no effect on it.
- remove ExtensionFilterController and SourceFilterController from settingControllersList in SettingsSearchHelper, since those are related to Extensions and not Settings
2020-08-28 20:47:18 -05:00
78072ad285 - implement prefernce highlighting after settings search 2020-08-28 12:52:30 -05:00
437a34b5dc - initialize SearchResultCollection once in Activity.onCreate 2020-08-27 00:53:50 -05:00
3ebea4c305 Add search button to MoreController, stop infinite recursion. 2020-08-26 23:07:59 -04:00
8fe315c354 Set query hint, clean up code smell. 2020-08-26 21:46:36 -04:00
b10b13a339 Merge branch 'dev' into dev-settings-search 2020-08-26 21:00:21 -04:00
5b5ea5ab8a Remove unneeded SettingsControllerFactory. 2020-08-26 20:53:56 -04:00
1a230b3900 Merge pull request #1 from lmj0011/dev-settings-search
implement searchable settings
2020-08-26 20:42:01 -04:00
e90b0aaf8b Adopt OneWayFadeChangeHandler from SY
From d86f3ffad8
2020-08-23 10:42:20 -04:00
fe7c7e72f5 Filter out hidden directories for local source (closes #3706) 2020-08-22 17:33:04 -04:00
9ba11a585f Adopt tab/controller transitions from SY
Original author: @jobobby04
2020-08-22 13:03:39 -04:00
9920ff617b Clean up X-Requested-With change
This only really affects the initial request, subsequent requests may still use the package name.
2020-08-22 12:49:00 -04:00
3f1355c413 Update WebViewActivity.kt (#3617)
This code added is for some extension that blocks tachiyomi, by tricking it that it was sent by a android browser, nothing major changes,
2020-08-22 12:37:21 -04:00
4929e66ecc Update ActionMode styling 2020-08-22 12:36:29 -04:00
02e370c2d8 Remove OkHttp Proguard rules
https://square.github.io/okhttp/r8_proguard/
2020-08-22 10:44:52 -04:00
4c31e3fc5f AndroidX dependency updates 2020-08-22 10:43:02 -04:00
15f49b39b8 Remove Glide Proguard rules (#3702)
* update glide rules in progurad

* remove glide rules since it is implemented by glide it self (through consumerproguardfiles)
2020-08-22 10:42:42 -04:00
8c82b766e3 - implement searchable settings
- `SettingsController.setupPreferenceScreen` must return a PreferenceScreen
2020-08-19 10:28:04 -05:00
4c8665c9f0 Don't enqueue bookmarked chapters for deletion (fixes #3691) 2020-08-18 17:47:07 -04:00
ba67781431 Minor wording edit 2020-08-18 17:40:24 -04:00
4ef25c75b7 Use core-ktx for bolding chapter transition text 2020-08-18 17:40:17 -04:00
3aafc671f8 Dependency updates 2020-08-17 15:36:04 -04:00
967df6f7a3 Update to Kotlin 1.4 2020-08-17 15:24:11 -04:00
22518f173f Adding class stubs for settings search, UI elements. 2020-08-16 19:11:23 -04:00
64bdfabbd8 Revert ktlint update, unrevert Gradle and PR build workflow reverts (#3681) 2020-08-15 16:44:46 -04:00
c8c65ab7b1 Remove broken PR build workflow for now 2020-08-14 17:06:25 -04:00
19cd28b66b Update and clarify message on tracking services (#3663)
* Update and clarify message on tracking services

Relates to #3659

* Update strings.xml
2020-08-13 17:25:18 -04:00
4a136ef2aa Automatic linting fixes 2020-08-13 09:02:15 -04:00
9e7a53cb90 Revert Gradle update
It broke the build.
2020-08-13 09:02:02 -04:00
19a7f37efa Add PR build check action 2020-08-12 22:38:13 -04:00
c3084ac43a Unhide parental controls settings 2020-08-12 22:34:00 -04:00
159146e197 Slight gradle cleanup, plugin updates 2020-08-12 22:33:50 -04:00
67ddf4a5b8 Update gradle wrapper 2020-08-12 22:33:50 -04:00
4b9b53a9b8 Optimize images using ezgif (#3649) 2020-08-12 22:32:17 -04:00
e6f025a9fb Release 0.10.4 2020-08-10 14:34:32 -04:00
aa607e0ecb Update issue templates 2020-08-10 14:34:10 -04:00
65b32ddeb2 Split out NSFW source setting to separate section
Temporarily hidden until feature is ready for stable release.
2020-08-10 14:19:35 -04:00
5e9bdc2690 Fix Chinese plural string 2020-08-10 14:18:50 -04:00
9ce994168a Translated using Weblate (Swedish) (#3580)
Currently translated at 99.8% (573 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/

Translated using Weblate (Finnish)

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/

Translated using Weblate (Swedish)

Currently translated at 99.6% (572 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sv/

Translated using Weblate (Greek)

Currently translated at 99.4% (571 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/

Translated using Weblate (Russian)

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Malay)

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Translated using Weblate (Spanish)

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/

Translated using Weblate (German)

Currently translated at 100.0% (574 of 574 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Dutch)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/

Translated using Weblate (Yakut)

Currently translated at 74.8% (428 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Filipino)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/

Translated using Weblate (Finnish)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/

Translated using Weblate (Russian)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Spanish)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/

Translated using Weblate (Turkish)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Malay)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Translated using Weblate (German)

Currently translated at 100.0% (572 of 572 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Indonesian)

Currently translated at 99.6% (568 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/

Translated using Weblate (Yakut)

Currently translated at 70.3% (401 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 70.3% (401 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Filipino)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/

Translated using Weblate (Chinese (Traditional))

Currently translated at 99.1% (565 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hant/

Translated using Weblate (Finnish)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/

Translated using Weblate (Greek)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/

Translated using Weblate (Turkish)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/

Translated using Weblate (Russian)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Dutch)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/

Translated using Weblate (Malay)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Translated using Weblate (Spanish)

Currently translated at 100.0% (570 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/

Translated using Weblate (German)

Currently translated at 99.4% (567 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Yakut)

Currently translated at 70.3% (401 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 70.3% (401 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 70.3% (401 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.8% (364 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.8% (364 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.5% (362 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.5% (362 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.3% (361 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 63.3% (361 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 62.4% (356 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 62.4% (356 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 61.7% (352 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 61.7% (352 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 61.7% (352 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 61.7% (352 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 61.5% (351 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 60.7% (346 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 60.7% (346 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 60.7% (346 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 60.0% (342 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.9% (336 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.9% (336 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.7% (335 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.7% (335 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.7% (335 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 58.7% (335 of 570 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Malay)

Currently translated at 100.0% (567 of 567 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Translated using Weblate (Russian)

Currently translated at 100.0% (567 of 567 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (567 of 567 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Spanish)

Currently translated at 100.0% (567 of 567 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/

Translated using Weblate (German)

Currently translated at 100.0% (567 of 567 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Yakut)

Currently translated at 49.6% (281 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Portuguese)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/

Translated using Weblate (Yakut)

Currently translated at 32.6% (185 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Yakut)

Currently translated at 10.4% (59 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sah/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Persian)

Currently translated at 96.6% (547 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fa/

Added translation using Weblate (Yakut)

Translated using Weblate (Catalan)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/

Translated using Weblate (Russian)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Spanish)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/

Translated using Weblate (Spanish (Latin America))

Currently translated at 99.8% (565 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/

Translated using Weblate (Spanish (Latin America))

Currently translated at 99.8% (565 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/

Translated using Weblate (Greek)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/

Translated using Weblate (Dutch)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/

Translated using Weblate (Czech)

Currently translated at 64.1% (363 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/

Translated using Weblate (Italian)

Currently translated at 98.7% (559 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/it/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Sardinian)

Currently translated at 99.8% (565 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/

Translated using Weblate (Filipino)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/

Translated using Weblate (Turkish)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/

Translated using Weblate (Indonesian)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/

Translated using Weblate (Finnish)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/

Translated using Weblate (Norwegian Bokmål)

Currently translated at 90.4% (512 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Malay)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Translated using Weblate (German)

Currently translated at 100.0% (566 of 566 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/uk/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Sardinian)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sc/

Translated using Weblate (Chuvash)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cv/

Translated using Weblate (Filipino)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/

Translated using Weblate (Norwegian Bokmål)

Currently translated at 89.3% (505 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/

Translated using Weblate (Spanish (Latin America))

Currently translated at 99.6% (563 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es_419/

Translated using Weblate (Portuguese)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt/

Translated using Weblate (Catalan)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ca/

Translated using Weblate (Greek)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/el/

Translated using Weblate (Filipino)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fil/

Translated using Weblate (Dutch)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nl/

Translated using Weblate (Finnish)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/fi/

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/zh_Hans/

Translated using Weblate (Turkish)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tr/

Translated using Weblate (Japanese)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/

Translated using Weblate (Japanese)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ja/

Translated using Weblate (German)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/de/

Translated using Weblate (Malay)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ms/

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2020-08-10 14:17:35 -04:00
748a720199 Lift toolbar on scroll in extension details and manga controllers 2020-08-10 12:04:51 -04:00
8db34eb3dd Allow annotating SourceFactory with @Nsfw to block all sources within it 2020-08-10 11:54:31 -04:00
b657bba96e Add 18+ warnings in extensions list 2020-08-10 11:53:23 -04:00
dbaac69fad Add option to prevent deleting bookmarked chapters (closes #2082) 2020-08-09 12:18:05 -04:00
b6a1e89535 Minor cleanup 2020-08-09 12:05:04 -04:00
cce919750a Minor rewording of chapter deletion settings 2020-08-09 12:04:56 -04:00
9376b223bb Bubble up sources with results in global search (closes #3598) 2020-08-09 11:58:50 -04:00
6f047fb5aa Update OkHttp 2020-08-09 11:48:08 -04:00
3e6b0117fd Swallow errors when trying to determine available disk space when downloading (closes #3603) 2020-08-09 11:39:18 -04:00
421dfb4a2d Allow partially loading extensions with individually marked NSFW sources 2020-08-08 19:06:52 -04:00
abaca6e676 Option to hide NSFW extensions (closes #1312) 2020-08-08 16:27:55 -04:00
8bab1d9798 Add mark as read/unread to library (closes #156)
Adapted from e51276a1ac
2020-08-08 12:16:37 -04:00
13d31669ac Explicitly depend on core-ktx 2020-08-08 12:10:12 -04:00
c1dfdeb500 Fix MAL 0/10 scores (closes #3623) 2020-08-06 19:14:24 -04:00
dda7e677a5 Dismiss add manga snackbar when leaving controller (closes #3614) 2020-08-06 19:05:07 -04:00
b1fb401f63 Warn before restoring backup if trackers aren't logged in 2020-08-05 22:46:37 -04:00
885ace111e Fix toolbar being expanded when opening preference dialogs 2020-08-04 08:56:30 -04:00
885552b792 Move tracker setting dialogs 2020-08-03 23:04:49 -04:00
4f02872a84 Minor cleanup 2020-08-03 23:03:31 -04:00
ecec1bd102 Revert "Use insetter library for handling inset padding" (fixes #3586)
This reverts commit 3ddd1033c3.
2020-08-03 16:55:41 -04:00
0c07e05a2b Release 0.10.3 2020-08-03 14:18:04 -04:00
060f0682f4 Fix snackbars not being in viewport properly 2020-08-03 14:12:34 -04:00
88032e11df Use dialog to show what's new release info 2020-08-03 14:08:35 -04:00
493c8b0943 Adjust vertical reading mode tap zones (closes #3551)
Basically L shapes, where top/left goes back, bottom/right goes forward, and middle opens the menu.
2020-08-03 12:17:49 -04:00
af2ef0621a Remove Tagalog translations (closes #3579) 2020-08-03 11:20:06 -04:00
095461e31b Explicitly dismiss progress notification on downloader stop 2020-08-03 11:15:33 -04:00
3ddd1033c3 Use insetter library for handling inset padding 2020-08-02 23:09:18 -04:00
912687ac78 Adjust download badge color again 2020-08-02 23:03:24 -04:00
40a9595012 Request gzipped version of extensions repo 2020-08-02 22:55:42 -04:00
12ff37d052 Fix manga title disappearing in toolbar when pushing another controller 2020-08-02 17:46:15 -04:00
4857073f30 Revert "Use AndroidX WebKit library"
This reverts commit 7e7eb9f39f.
2020-08-02 16:12:05 -04:00
cdbefd9191 Release 0.10.2 2020-08-02 14:30:51 -04:00
2e9d89574d Make download badges lighter to improve contrast (closes #3571) 2020-08-02 14:29:48 -04:00
569c99496b Translated using Weblate (Russian) (#3549)
Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/pt_BR/

Translated using Weblate (Indonesian)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/id/

Translated using Weblate (Hindi)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hi/

Translated using Weblate (Spanish)

Currently translated at 100.0% (565 of 565 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/es/

Translated using Weblate (Russian)

Currently translated at 100.0% (564 of 564 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ru/

Translated using Weblate (Georgian)

Currently translated at 9.3% (53 of 564 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ka/

Translated using Weblate (Tagalog)

Currently translated at 73.9% (417 of 564 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/tl/

Translated using Weblate (Serbian)

Currently translated at 79.9% (451 of 564 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/sr/

Translated using Weblate (Thai)

Currently translated at 58.1% (328 of 564 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/th/

Translated using Weblate (Norwegian Bokmål)

Currently translated at 88.6% (500 of 564 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/nb_NO/

Translated using Weblate (Czech)

Currently translated at 64.0% (361 of 564 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/

Translated using Weblate (Korean)

Currently translated at 57.9% (327 of 564 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/ko/

Translated using Weblate (Hungarian)

Currently translated at 35.6% (201 of 564 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/hu/

Translated using Weblate (Bengali)

Currently translated at 60.4% (341 of 564 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/bn/

Translated using Weblate (Czech)

Currently translated at 63.8% (360 of 564 strings)

Translation: Tachiyomi/Strings
Translate-URL: https://hosted.weblate.org/projects/tachiyomi/strings/cs/

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2020-08-02 14:28:33 -04:00
ea3b8767de Fix crash when filter groups contain items with identical names (closes #3568) 2020-08-02 12:52:40 -04:00
8e8c30c1eb Move download warnings/errors to separate notification channel 2020-08-02 12:16:51 -04:00
d921ba81c8 Revert "Downgrade coroutines and flow-preferences"
This reverts commit b47ee8857b.
2020-08-02 11:54:32 -04:00
ad9f646102 Fix downloads not working for custom SD card paths (closes #3564) 2020-08-02 11:52:37 -04:00
2ef277bcef Don't show completed notification if download error notification was shown 2020-08-02 10:53:17 -04:00
9e396e1624 Fix history item icon tint in light blue theme 2020-08-02 10:29:04 -04:00
9708d84e60 Fix dividers in migrate list 2020-08-01 18:28:48 -04:00
4efc195548 Fix last used source pinned status 2020-08-01 18:23:46 -04:00
0d15cbe334 Filter out chapter entries with duplicate URLs (fixes #3552) 2020-08-01 16:35:52 -04:00
435 changed files with 11212 additions and 5769 deletions

View File

@ -2,7 +2,7 @@
I acknowledge that:
- I have updated to the latest version of the app (stable is v0.10.1)
- I have updated to the latest version of the app (stable is v0.10.7)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
@ -10,7 +10,7 @@ I acknowledge that:
---
### Device information
## Device information
* Tachiyomi version: ?
* Android version: ?
* Device: ?

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.1)
- I have updated to the latest version of the app (stable is v0.10.7)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
@ -17,7 +17,7 @@ I acknowledge that:
---
### Device information
## Device information
* Tachiyomi version: ?
* Android version: ?
* Device: ?
@ -32,5 +32,5 @@ This should happen.
### Actual behavior
This happened instead.
### Other details
## Other details
Additional details and attachments.

8
.github/ISSUE_TEMPLATE/config.yml vendored Normal file
View File

@ -0,0 +1,8 @@
blank_issues_enabled: false
contact_links:
- name: Tachiyomi help website
url: https://tachiyomi.org/help/
about: Common questions are answered here.
- name: Tachiyomi extensions GitHub repository
url: https://github.com/inorichi/tachiyomi-extensions
about: Issues about an extension/source/catalogue should be opened here instead.

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.1)
- I have updated to the latest version of the app (stable is v0.10.7)
- I have updated all extensions
- If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions
@ -17,8 +17,8 @@ I acknowledge that:
---
### Why/User Benefit/User Problem
## Why/User Benefit/User Problem
(explain why this feature should be added)
### What/Requirements
## What/Requirements
(explain how this feature would behave)

View File

@ -2,7 +2,7 @@
name: "Extension/source/catalogue issue"
about: "Do not open an issue here. See https://github.com/inorichi/tachiyomi-extensions"
title: "THIS ISSUE IS IN THE WRONG REPO; SEE https://github.com/inorichi/tachiyomi-extensions"
labels: "catalog"
labels: "catalog, invalid"
---
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions
DO NOT OPEN AN ISSUE IN THIS REPO. SEE https://github.com/inorichi/tachiyomi-extensions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 KiB

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,6 @@
org.gradle.daemon=false
org.gradle.jvmargs=-Xmx5120m
org.gradle.workers.max=2
kotlin.incremental=false
kotlin.compiler.execution.strategy=in-process

95
.github/workflows/build.yml vendored Normal file
View File

@ -0,0 +1,95 @@
name: CI
on:
push:
branches:
- dev
tags:
- v*
pull_request:
jobs:
check_wrapper:
name: Validate Gradle Wrapper
runs-on: ubuntu-latest
steps:
- name: Clone repo
uses: actions/checkout@v2
- name: Validate Gradle Wrapper
uses: gradle/wrapper-validation-action@v1
build:
name: Build app
needs: check_wrapper
if: "!startsWith(github.event.head_commit.message, '[SKIP CI]')"
runs-on: ubuntu-latest
steps:
- name: Cancel previous runs
uses: styfle/cancel-workflow-action@0.5.0
with:
access_token: ${{ github.token }}
- name: Clone repo
uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Copy CI gradle.properties
run: |
mkdir -p ~/.gradle
cp .github/runner-files/ci-gradle.properties ~/.gradle/gradle.properties
- name: Build app
uses: eskatos/gradle-command-action@v1
with:
arguments: assembleStandardRelease
wrapper-cache-enabled: true
dependencies-cache-enabled: true
configuration-cache-enabled: true
# Sign APK and create release for tags
- name: Get tag name
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'inorichi/tachiyomi'
id: get_tag_name
run: |
set -x
echo "VERSION_TAG=${GITHUB_REF/refs\/tags\//}" >> $GITHUB_ENV
- name: Sign APK
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'inorichi/tachiyomi'
uses: r0adkll/sign-android-release@v1
with:
releaseDirectory: app/build/outputs/apk/standard/release
signingKeyBase64: ${{ secrets.SIGNING_KEY }}
alias: ${{ secrets.ALIAS }}
keyStorePassword: ${{ secrets.KEY_STORE_PASSWORD }}
keyPassword: ${{ secrets.KEY_PASSWORD }}
- name: Create release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'inorichi/tachiyomi'
id: create_release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ env.VERSION_TAG }}
release_name: Tachiyomi ${{ env.VERSION_TAG }}
draft: true
prerelease: false
- name: Upload APK to release
if: startsWith(github.ref, 'refs/tags/') && github.repository == 'inorichi/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

@ -1,5 +1,8 @@
name: Issue closer
on: [issues]
on:
issues:
types: [opened, edited, reopened]
jobs:
autoclose:
runs-on: ubuntu-latest
@ -31,4 +34,4 @@ jobs:
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."
message: "@${issue.user.login} this issue was automatically closed because the requested information was not filled out."

View File

@ -1,81 +0,0 @@
dist: trusty
language: android
android:
components:
- tools
- platform-tools
- build-tools-29.0.3
- android-29
- extra-android-m2repository
- extra-google-m2repository
- extra-android-support
- extra-google-google_play_services
licenses:
- 'android-sdk-license-.+'
- 'android-sdk-preview-license-.+'
before_install:
- yes | sdkmanager "platforms;android-29" # workaround for accepting the license
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
tar xf secrets.tar;
mv debug.keystore "$HOME/.android";
fi
- mkdir "$ANDROID_HOME/licenses" || true
- echo -e "\n24333f8a63b6825ea9c5514f83c2829b004d1fee" > "$ANDROID_HOME/licenses/android-sdk-license"
- echo -e "\n84831b9409646a918e30573bab4c9c91346d8abd" > "$ANDROID_HOME/licenses/android-sdk-preview-license"
install:
- echo y | sdkmanager "ndk-bundle"
before_script:
- export ANDROID_NDK_HOME=$ANDROID_HOME/ndk-bundle
script: ".travis/build.sh"
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
- rm -fr $HOME/.gradle/caches/*/plugin-resolution/
cache:
directories:
- "$HOME/.gradle/caches/"
- "$HOME/.gradle/wrapper/"
- "$HOME/.android/build-cache"
deploy:
- provider: releases
api_key:
secure: qmS9SyMq8xPDqaY83rvAFyZcvic24lGBj3MFt22RhVJzIXAAN/vqL1R70PnNiCF7CE+R7PaDlBpwjxDMBiuh0QQNc1oX6cgepUbro4/Nt7NFFfCvKXaFdR1cSgYouhuHmy0SS0/alrcfhQ2bPwcm1/vAOiSa8Wu7hsXhCcxbFyEbXZVD11QZmiffEM0py+OeuqOFo2JxZaGRu2z04E/u5TWep1ZEuhFRCC87PGgFqABgg6jYYebQOUZADG/0G8581HTGU0mdwueYsiA35ncRzpV2V8DajEEBd5wOe5d8SyMtE+6Qs5PD9KcXAqGGe4QRmrJMX5EcLQaLZf/Qd5s9SFZVHb1aJIw/y05w4L5dlVpsjx5WuUAYAVg7Ol5UawofFo/hYkYCNmfub67wJQdHSIxPif7V6YeON6RQQMpc5GBYY9eA6ZxhrdA2m7eyoOT3jcbdaVJwC0jMGhn26hkgJfTo1LfAUs85Cs3BrK8w6Poqc/Jb+4Y0NhdGIKgO5tS3vY54cTJVVrQTq4/XmME4ZnzOX3HaOqzfyt/6M4gEQMvaeFksxwoFhocV7wfaCq9ps/Kdq2dl4KwoqRV2WqVaauqzCP4XPSlVLaJqztsw0wboupcaZepWJ2a/6j9IrKo1pEnyeHF5y+k0SUAxL0X8iKZ0uPxsgoVrlNtqXJWNGvA=
file: tachiyomi-v*.apk
file_glob: true
skip_cleanup: true
on:
tags: true
repo: inorichi/tachiyomi
- provider: script
script: ".travis/deploy.sh"
skip_cleanup: true
on:
branch: master
condition: "-z $TRAVIS_TAG"
repo: inorichi/tachiyomi
- provider: script
script: ".travis/deploy.sh"
skip_cleanup: true
on:
branch: dev
condition: "-z $TRAVIS_TAG"
repo: inorichi/tachiyomi
env:
global:
- secure: Ita1+tzo7P5IC2yqU3KgRcXt+5DTpP0103Hx/ECYi42/7rLt+TC7PMjl2yH3Z189+tGwLq0Ol1KJ2Z5Rn3q7EaQgD0+WRkH/ijtrjKoVh7dyItIBp7yowZpA0TJHQ4EZpGSxZakKbIP4di8XMxJ2+5VzEivYUt04LCUpzugemL6b6XOfUmOZykVxV2UDAlPPggklITYBXkHUa0mwJhjS1aPPeeR3PhVXomkqfuOJOKejPXXXJope9fhAnmopHA7ISfjIrTuwDVQJqNSuco+O9kQShmlu0C8pob1vFGPEDvafaDS8SZ9A6gKT1ZfgSUqVmvDbx0WLX8XugBLrQedtZv63esOa1WUyGhgFVpeJjexlszXlhyfP1gH5QbzRr+EiSaagCyjf9II2veLAtU5cFY+nj6KBdKQsazIMRHf8SAQlWASyJYMED/N9RnUFxSf1rnLGqiY2ezjycx4ieFj7vhlbTgyao1GHjjR9cwNuntdMYWhY8+Vc7Fctmzm46xOyyz9oJGdyim76Y4w4MZvQNKeZOBAjdEgX6cXBk15scoM2Vj9ENox+MKZLaKRawXg2U1ujK+bWAQkXiVvPriv05/JtYsNUft8qAsm+69vtohDsUW7Wu0bBIKDL+v0W30ty1PpyNehBB2OquUE7fp53gitOmYl7TyuxktkMY8PXKKU=
- secure: NABCfigMUVM/9TLALYBpQLp/p3rG6MbH5y34/oqCSej/oh2u0nyhFSi8veS0lFpDIcv0TZvxHJXoSA0zeZijb1fUU8jYVNT2azuPWE6Gu7sf0TfBeCvulqbgLMoaA6JuWbEbZwHcxpKHg4vLSMjNk+ZP4v2dffI6A620fxLltxxhTpsYkYYsfKG857CpQtdgN/HqcOaxyvzXFmWWyVWHala1uMcMeXZCwgnlVAqau9o0bsU092txSmHqoesHoAinidSCTCmTlEqp/1AFaYQTbxmnfNC1yLgzxWAlJcS3NWzNo3ellMvKjsiIGn3JJpAjTGcyf3yPsvhs1cY3MZbmJYVyKH5HbnkA5ms6mx0DDJ2UOs5H2dmED82m14+hu62Xb8XN8zAdq+bySNSwgsGzvr1PT74pT4BW1T+D7L1xvUe6k1enZ38GIMJbJPhBybRQazhjKPmXRB30Thxoqe5VqU8UeiXHAEps7JYAWUR1PLZvEYQb6MWurmTxs9be/OTwrUT1LDiqeJZg6XkDGgQwuR2YBaQJHJD17Piq6q1BUX8abhK6wzAAYVqyGvpmUCmQCtHZgenE6ulwcKChzBv4n97OjE21LGWnbNF5ViUhfAbGcKOVufd1chZsfbkJ7a3tHYCfLnxHUIhKvHk26f5Em8h68D0wQkPnkcVVkfh7XpI=
- secure: C93UADV5aR0zhLCLwR6tCyz+fwUYslZbhjBl3PHQp+0boIGS/Be2UQTFHp/NB9mQmhWqbwqHoAVFENZFytV04ePgOuNtMFcjAIfnzm19Am7iRAMFixD45pF/CuYYfLupElkAcSequtAzN0g4G0sQ5KR1hibaDIoz9kfA2YcUAMaZ4T5bhCr8os/xA2nOlmvPDWsRWCFBYkSpnmbsSsgYAhulA/V5bSNAWnp9LPw3CBLibW3WsVP4wuhZAkXznKwn/mHT31kfQlpMH3qNhXpsN9huUkZ/k8QWeakcHJKugung0Z2T1StK8rlI8OrJstVcwueHTa2ses4f5VbhWog/Z8HDkdll9W9RM/QqXjNDtOVBt/SPuhCp4k2rvJixFUxzvSqgSWQvQnbHwjWxIUVVyHtnb0/zc/S9ONZG14TOwB/+Lkgacb85PNszurZ2f3mH0O6slIh1mH+5d9J4+L976ot4nTPlK1OtothagVyKGOrn9HycrPk/MjftIJuElHzo7NEJd/wRPqIb5y12iZN1RSPriU+itg1uSAVP891/o3peJyuqY9WSB7dYwgDJos6dDvbr19emtdyxkQR+eAb5duyH6s4R58wh1kJ1d4zu0X6uSnF4AIc+6teKkN24rSXcqB/hrcojS49jgLy5P0/CVsUbYZPI/tx8E/IJfr8m36E=
- secure: mawwBvllvESc/mp+JHvncq1iUhiC7nyokPgXmOehffc0K3byMLs2L25HjNsU6EnXG9Lcae1cfP8S9bWLquU2C3kpAkLBUpjEbdx7K0654uvs7Rrvb5hcTRHwjzaEVmVaBFX4ROcjUhBYny/Wjj/YENCkSWpkfcMd1esFbVsO+fOLyaAPvrb6auKY7H+pUSqlEwaEnrkYeBBZIHa7KqwL4g5DHbq6K368tjmval/wBzwMB0V8V3dt/ik8RMVDtKPrik4Bu0V9UmXZUIo/a06ii/CM82ekFRh3eUb0DKkdkmYbdH6MBMoLTfQtMa6A4luXaA0oycAnTX3OGB5MWIjK39KhWRavh6ybSIt4aHKoolxzH8Zgmk7xMhFSot/laX5q5IzjZu5KU6F2SmdV0kcQugM8oAjANFySetPvY1q7nZ8pM+NO1xKS/mH0w4vChhdJFD1mw7aCoh8FdeUf0Eym2+pp5Q9uAisWMmNn5XN8/fL5q6PzAxkXmkedfrr1N61FmIL6EKx8qiWpOUNlRRTIMJ4GMhCyckCF6cNxDkBItp52c+Hmkbn+ZEInEyX6gpjYVm3xyEi0Z5kLCi/fMX2nBNczc5BuGLzzmJnITv4ovpeYn2/vPvHbaPgPC4LqDK3AjlpVadMZk/M5Egn+hWY7Mni57CmpZD+SpxUbbsItI0c=
- secure: PJPDkUg1zc57brxUvNpSh+Q3ZEaGpBqZzwDavqslkn0WmjBTLrE6/OG7TFHKNmO+P56qFl+pMEKqThxqR3+4bWEeEx8ykkixDVzxNJMmws+7A7ImJ75iQyB6giMW/4tykVMMHgIPNAdcnI8VOWn0LGHnpFWUd70yoyAGX8s6cspHCKgcuWMA3GS410KJfHpyd0B9/QS7ZyWzSETW7zSPyLPa81SBO95EhOF3TOGZYLt/mBhdtU3YGFs4k9fZ8jDDcm9XmBfqVlUhb8HiZcxJiZDdRvxODERfNnwc47uaJk6+kxGDzIW2uAxrMXXVKkG04GeMOokXoR9kW1Hl2JmoyySLKLZmB7I/XEtVWdzZw16mWi+4zmhjLhfB0phSW+/5I+0VtZZ6jO031J5FL/JqVrcq1ws/aw4QlaOdPUco/x2u4LNHyYYgOi5arD9xSyu6IRy0jCC4Xa1zuqM5adGJX+rZyVfKZ0TxOW661HTxlo8COtkB2i0WR2deZGVN75ooCAEO8DauQoUcFH1OelahmPtzVs1/6ZczuxGdp9ED7ZQq9NHEOsOdUGCj/D79Dm1hWFQsIsslnnGYWitAycNCgEwmlt2Q6fbrv2CJrmLqZ9a9r3AhzxoHn9Qx1GyuyfhZJzm/6Ff2kcOjma2kcz13KUwTxdW+2G5dDCotK3f7aiI=
- secure: FIIZfEEYfjNMKODs33Czh603CYVn6LRrzpFNIiPHYTb8iQWv9qAYhsg4FpHfOjDikokTwb5X/h8G7AX93Z0xKyyDi75ACT11oPeTNTArDdcmdDVlOYBvYHc2Ci7pMW5r8LGejB7Y3mWM8uKyA3oKvneEFutB65vO3JVZvFWrm03Lmqqe7+mA4qNqNqTbN7R7fmk5b7zt7A3DHvDu0JPTbSSUwpso/p2I5WJYjrf71I7YMQwIFLoMfplC1onVA3EFS3lZsF65zE+xVRy34AKa41iZAMbhVDyqUHEnx6L0dwEdn2Z5XLlK0ov1+qLTLlQsBE4Knre6TNkWMfktk7MKA+ch8RYxvEYLODhQkIrOkLSNWhZPhdaT+xD4fr0RCKSHo6uWRC4aofsJx8wSqb8ZL4j2zopUp9VisMOI202UEnvFDBtOkVGJSxxYbFjifIB7NCJBn788w+3k+k4IbOg537VdyoK2PMBR8/TDdjImWhWHY1i7+345ejwmzHL7ZPfb6GTNnQTWkajT77/n6Yk41twR5vvegOSTKuuO++WN/pUks4PGqtcQe9fnSfx2OcOq1ofLiG+JDorJ7z8kHSG13wHLq+QYMDayQbyJEYpDzmn/w3Ou1s2o0a7A41+cIkRzAgH9y3v4lgjp9GcMP2S74ZPA7OecWbFSexM7tL/dYxY=
- secure: DKCGc4E9PKeTX68r9pbbNg5qITsN0bApQ1m0x8xdEoi8GLRKVMYNn6ahoAxvy1YsBXC9Zlt5++gLmUV1I1JyDMyJXMr/lZrp4oarW0xWpTBmn3HzOph/K2W4i/fTGgMFieumPEbQIFOnU3JSjK6UJB8qVGEXD2OqS7A//EdrGDbAYVDL3ZTKE6JUlTNHgaKaNHhn+Dq4aBLTSYPwlLyqo+WNBVUUCKCHOq62ULF8MpX5YGaPFNxKYzircV7HpF1hCbV31dmpkeYT9xztra5V0SIBM27jAcQqGmtHH2mhx1sLu+gjhFJbbtY6cggA9EedzYYLDx/NPmgfyuOJfyVbSwTF3vhDUYfskqc1THWpwOSKO0Ry+8/xYb9crxg+FSwuI5hnfkIFk9woBvRGBhjto3/1buMNY9dSFiWtEbN6Let8e747l0wIGJCpJxSeh7vn7F1mWjixhf9GX1+V9BrUvGTd3XJDNb9cVnafYa1RTj8BLteA4HBza7Z9R3dvG4YWp16L/94UuaTzgAQfERLTZGopQth/hsaVTlYesJmJLF70lGM+W83y3YuNkSaX1zQ5FAIvp7oH0O16t7ISm6GprUFwN2Uox7AAbPZdWHxJbly+D+yCFNcqS3Bz9mV3YCLo690Sy1ePNHr+nCseVfBMo7OYyavSS/EjPWfEy65Wq04=

View File

@ -1,20 +0,0 @@
#!/bin/bash
git fetch --unshallow #required for commit count
if [ -z "$TRAVIS_TAG" ]; then
./gradlew clean assembleStandardDebug
COMMIT_COUNT=$(git rev-list --count HEAD)
export ARTIFACT="tachiyomi-r${COMMIT_COUNT}.apk"
mv app/build/outputs/apk/standard/debug/app-standard-debug.apk $ARTIFACT
else
./gradlew clean assembleStandardRelease
TOOLS="$(ls -d ${ANDROID_HOME}/build-tools/* | tail -1)"
export ARTIFACT="tachiyomi-${TRAVIS_TAG}.apk"
${TOOLS}/zipalign -v -p 4 app/build/outputs/apk/standard/release/app-standard-release-unsigned.apk app-aligned.apk
${TOOLS}/apksigner sign --ks $STORE_PATH --ks-key-alias $STORE_ALIAS --ks-pass env:STORE_PASS --key-pass env:KEY_PASS --out $ARTIFACT app-aligned.apk
fi

View File

@ -1,15 +0,0 @@
#!/bin/bash
pattern="tachiyomi-r*"
files=( $pattern )
export ARTIFACT="${files[0]}"
if [ -z "$ARTIFACT" ]; then
echo "Artifact not found"
exit 1
fi
export SSHOPTIONS="-o UserKnownHostsFile=/dev/null -o StrictHostKeyChecking=no -i ${DEPLOY_KEY}"
scp $SSHOPTIONS $ARTIFACT $DEPLOY_USER@$DEPLOY_HOST:builds/
ssh $SSHOPTIONS $DEPLOY_USER@$DEPLOY_HOST ln -sf $ARTIFACT builds/latest

Binary file not shown.

View File

@ -1,6 +1,6 @@
| Build | Stable | Weekly Preview | Contribute | Support Server |
|-------|----------|---------|------------|---------|
| [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/inorichi/tachiyomi/releases) | [![latest weekly build](https://img.shields.io/badge/download-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest) | [![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/inorichi/tachiyomi/workflows/CI/badge.svg?branch=dev&event=push) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=download)](https://github.com/inorichi/tachiyomi/releases) | [![latest weekly build](https://img.shields.io/github/v/release/tachiyomiorg/android-app-preview.svg?maxAge=3600&label=download)](https://github.com/tachiyomiorg/android-app-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) |
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
@ -23,7 +23,7 @@ Features include:
## Download
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases).
If you want to try new features before they get to the stable release, you can download the preview version [here](http://tachiyomi.kanade.eu/latest).
If you want to try new features before they get to the stable release, you can download the preview version [here](https://github.com/tachiyomiorg/android-app-preview/releases).
## Issues, Feature Requests and Contributing
@ -31,7 +31,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<details><summary>Issues</summary>
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
1. **Before reporting a new issue, take a look at the [FAQ](https://tachiyomi.org/help/faq/), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/tachiyomi)
</details>
@ -58,7 +58,7 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
* Write a detailed issue, explaining what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed)
Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions, they do not belong in this repository.
Source requests should be created at https://github.com/inorichi/tachiyomi-extensions, they do not belong in this repository.
</details>
## FAQ

View File

@ -5,8 +5,9 @@ import java.text.SimpleDateFormat
apply plugin: 'com.android.application'
apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'kotlin-parcelize'
apply plugin: 'kotlinx-serialization'
apply plugin: 'com.github.zellius.shortcut-helper'
shortcutHelper.filePath = './shortcuts.xml'
@ -34,14 +35,15 @@ ext {
android {
compileSdkVersion AndroidConfig.compileSdk
buildToolsVersion AndroidConfig.buildTools
ndkVersion AndroidConfig.ndk
defaultConfig {
applicationId "eu.kanade.tachiyomi"
minSdkVersion AndroidConfig.minSdk
targetSdkVersion AndroidConfig.targetSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 47
versionName "0.10.1"
versionCode 53
versionName "0.10.7"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -65,13 +67,13 @@ android {
applicationIdSuffix ".debug"
}
release {
postprocessing {
obfuscate false
optimizeCode true
removeUnusedCode false
removeUnusedResources true
proguardFiles 'proguard-rules.pro'
}
// postprocessing {
// obfuscate false
// optimizeCode true
// removeUnusedCode false
// removeUnusedResources true
// proguardFiles 'proguard-rules.pro'
// }
}
}
@ -121,40 +123,37 @@ android {
}
}
androidExtensions {
experimental = true
}
dependencies {
// AndroidX libraries
implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
implementation 'androidx.biometric:biometric:1.0.1'
implementation 'androidx.browser:browser:1.2.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.0.0-rc1'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.recyclerview:recyclerview:1.2.0-alpha05'
implementation 'androidx.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
implementation 'androidx.webkit:webkit:1.3.0-rc01'
// Source models and interfaces from Tachiyomi 1.x
implementation 'tachiyomi.sourceapi:source-api:1.1'
final lifecycle_version = '2.3.0-alpha06'
// AndroidX libraries
implementation 'androidx.annotation:annotation:1.2.0-alpha01'
implementation 'androidx.appcompat:appcompat:1.3.0-alpha02'
implementation 'androidx.biometric:biometric-ktx:1.2.0-alpha01'
implementation 'androidx.browser:browser:1.3.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:2.1.0-alpha1'
implementation 'androidx.coordinatorlayout:coordinatorlayout:1.1.0'
implementation 'androidx.core:core-ktx:1.5.0-alpha05'
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.swiperefreshlayout:swiperefreshlayout:1.2.0-alpha01'
final lifecycle_version = '2.3.0-beta01'
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// Job scheduling
final work_version = '2.4.0'
implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
implementation "androidx.work:work-runtime-ktx:2.5.0-beta02"
// UI library
implementation 'com.google.android.material:material:1.3.0-alpha02'
implementation 'com.google.android.material:material:1.3.0-alpha04'
standardImplementation 'com.google.firebase:firebase-core:17.4.4'
standardImplementation 'com.google.firebase:firebase-core:18.0.0'
// ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1'
@ -163,22 +162,26 @@ dependencies {
implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
// Network client
final okhttp_version = '4.8.0'
final okhttp_version = '4.10.0-RC1'
implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
implementation 'com.squareup.okio:okio:2.7.0'
implementation 'com.squareup.okio:okio:2.9.0'
// TLS 1.3 support for Android < 10
implementation 'org.conscrypt:conscrypt-android:2.4.0'
implementation 'org.conscrypt:conscrypt-android:2.5.1'
// REST
final retrofit_version = '2.9.0'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.jakewharton.retrofit:retrofit2-kotlinx-serialization-converter:0.8.0"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
// JSON
final kotlin_serialization_version = '1.0.1'
implementation "org.jetbrains.kotlinx:kotlinx-serialization-json:$kotlin_serialization_version"
implementation "org.jetbrains.kotlinx:kotlinx-serialization-protobuf:$kotlin_serialization_version"
implementation 'com.google.code.gson:gson:2.8.6'
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
@ -188,19 +191,19 @@ dependencies {
// Disk
implementation 'com.jakewharton:disklrucache:2.0.2'
implementation 'com.github.inorichi:unifile:e9ee588'
implementation 'com.github.inorichi:junrar-android:634c1f5'
implementation 'com.github.junrar:junrar:7.4.0'
// HTML parser
implementation 'org.jsoup:jsoup:1.13.1'
// Database
implementation 'androidx.sqlite:sqlite:2.1.0'
implementation 'androidx.sqlite:sqlite-ktx:2.1.0'
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
implementation 'io.requery:sqlite-android:3.32.2'
implementation 'io.requery:sqlite-android:3.33.0'
// Preferences
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
implementation 'com.github.tfcporciuncula.flow-preferences:flow-preferences:1.3.3'
// Model View Presenter
final nucleus_version = '3.0.0'
@ -216,7 +219,7 @@ dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:bff2806'
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:6caf219'
// Logging
implementation 'com.jakewharton.timber:timber:4.7.1'
@ -232,9 +235,7 @@ dependencies {
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
implementation 'eu.davidea:flexible-adapter:5.1.0'
implementation 'eu.davidea:flexible-adapter-ui:1.0.0'
implementation 'com.nononsenseapps:filepicker:2.5.2'
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.1.0'
implementation 'com.github.mthli:Slice:v1.3'
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
implementation 'com.github.carlosesco:DirectionalViewPager:a844dbca0a'
@ -260,13 +261,11 @@ dependencies {
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version"
// Licenses
final aboutlibraries_version = '8.3.0'
implementation "com.mikepenz:aboutlibraries-core:$aboutlibraries_version"
implementation "com.mikepenz:aboutlibraries:$aboutlibraries_version"
implementation "com.mikepenz:aboutlibraries:$BuildPluginsVersion.ABOUTLIB_PLUGIN"
// Tests
testImplementation 'junit:junit:4.13'
testImplementation 'org.assertj:assertj-core:3.12.2'
testImplementation 'org.assertj:assertj-core:3.16.1'
testImplementation 'org.mockito:mockito-core:1.10.19'
final robolectric_version = '3.1.4'
@ -274,48 +273,50 @@ dependencies {
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-reflect:$BuildPluginsVersion.KOTLIN"
// Do not update until we bump to Kotlin 1.4, see https://github.com/Kotlin/kotlinx.coroutines/issues/2049
final coroutines_version = '1.3.6'
final coroutines_version = '1.4.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
// For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.4'
// Debug tool; see https://fbflipper.com/
// debugImplementation 'com.facebook.flipper:flipper:0.50.0'
// debugImplementation 'com.facebook.soloader:soloader:0.9.0'
}
buildscript {
ext.kotlin_version = '1.3.72'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$BuildPluginsVersion.KOTLIN"
}
}
repositories {
mavenCentral()
jcenter()
}
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
tasks.withType(AbstractKotlinCompile).all {
kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.Experimental"]
kotlinOptions.freeCompilerArgs += [
"-Xopt-in=kotlin.Experimental",
"-Xopt-in=kotlin.RequiresOptIn",
"-Xuse-experimental=kotlin.ExperimentalStdlibApi",
"-Xuse-experimental=kotlinx.coroutines.FlowPreview",
"-Xuse-experimental=kotlinx.coroutines.ExperimentalCoroutinesApi",
"-Xuse-experimental=kotlinx.serialization.ExperimentalSerializationApi",
]
}
// Duplicating Hebrew string assets due to some locale code issues on different devices
task copyResources(type: Copy) {
task copyHebrewStrings(type: Copy) {
from './src/main/res/values-he'
into './src/main/res/values-iw'
include '**/*'
}
preBuild.dependsOn(ktlintFormat, copyResources)
preBuild.dependsOn(formatKotlin, copyHebrewStrings)
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
apply plugin: 'com.google.gms.google-services'

View File

@ -23,21 +23,6 @@
<init>();
}
# OkHttp
-dontwarn okhttp3.**
-dontwarn okio.**
-dontwarn javax.annotation.**
-dontwarn retrofit2.Platform$Java8
# Glide specific rules #
# https://github.com/bumptech/glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.AppGlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES;
public *;
}
# RxJava 1.1.0
-dontwarn sun.misc.**
@ -71,3 +56,24 @@
-keep class * implements com.google.gson.TypeAdapterFactory
-keep class * implements com.google.gson.JsonSerializer
-keep class * implements com.google.gson.JsonDeserializer
## kotlinx.serialization ##
-keepattributes *Annotation*, InnerClasses
-dontnote kotlinx.serialization.AnnotationsKt # core serialization annotations
-keepclassmembers class kotlinx.serialization.json.** {
*** Companion;
}
-keepclasseswithmembers class kotlinx.serialization.json.** {
kotlinx.serialization.KSerializer serializer(...);
}
-keep,includedescriptorclasses class eu.kanade.tachiyomi.**$$serializer { *; }
-keepclassmembers class eu.kanade.tachiyomi.** {
*** Companion;
}
-keepclasseswithmembers class eu.kanade.tachiyomi.** {
kotlinx.serialization.KSerializer serializer(...);
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

After

Width:  |  Height:  |  Size: 2.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 5.9 KiB

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.9 KiB

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.3 KiB

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 4.0 KiB

After

Width:  |  Height:  |  Size: 2.9 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 5.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 10 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.7 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 18 KiB

After

Width:  |  Height:  |  Size: 13 KiB

View File

@ -25,7 +25,7 @@
android:requestLegacyExternalStorage="true"
android:roundIcon="@mipmap/ic_launcher_round"
android:theme="@style/Theme.Tachiyomi.Light"
android:usesCleartextTraffic="true">
android:networkSecurityConfig="@xml/network_security_config">
<activity
android:name=".ui.main.MainActivity"
android:launchMode="singleTop"
@ -42,7 +42,8 @@
<activity
android:name=".ui.main.DeepLinkActivity"
android:launchMode="singleTask"
android:theme="@android:style/Theme.NoDisplay">
android:theme="@android:style/Theme.NoDisplay"
android:label="@string/process_text_action_name">
<intent-filter>
<action android:name="android.intent.action.SEARCH" />
<action android:name="com.google.android.gms.actions.SEARCH_ACTION" />
@ -53,6 +54,11 @@
<action android:name="eu.kanade.tachiyomi.SEARCH" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.PROCESS_TEXT" />
<category android:name="android.intent.category.DEFAULT" />
<data android:mimeType="text/plain" />
</intent-filter>
<meta-data
android:name="android.app.searchable"
@ -60,17 +66,20 @@
</activity>
<activity
android:name=".ui.reader.ReaderActivity"
android:launchMode="singleTask" />
android:launchMode="singleTask">
<intent-filter>
<action android:name="com.samsung.android.support.REMOTE_ACTION" />
</intent-filter>
<meta-data android:name="com.samsung.android.support.REMOTE_ACTION"
android:resource="@xml/s_pen_actions"/>
</activity>
<activity
android:name=".ui.security.BiometricUnlockActivity"
android:theme="@style/Theme.Splash" />
<activity
android:name=".ui.webview.WebViewActivity"
android:configChanges="uiMode|orientation|screenSize" />
<activity
android:name=".widget.CustomLayoutPickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme" />
<activity
android:name=".ui.setting.track.AnilistLoginActivity"
android:label="Anilist">
@ -85,6 +94,9 @@
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".ui.setting.track.MyAnimeListLoginActivity"
android:configChanges="uiMode|orientation|screenSize" />
<activity
android:name=".ui.setting.track.ShikimoriLoginActivity"
android:label="Shikimori">

View File

@ -13,7 +13,6 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper
import java.security.Security
import org.acra.ACRA
import org.acra.annotation.AcraCore
import org.acra.annotation.AcraHttpSender
@ -24,6 +23,7 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.injectLazy
import uy.kohesive.injekt.registry.default.DefaultRegistrar
import java.security.Security
@AcraCore(
buildConfigClass = BuildConfig::class,

View File

@ -13,6 +13,7 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.launch
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingleton
@ -44,6 +45,8 @@ class AppModule(val app: Application) : InjektModule {
addSingletonFactory { Gson() }
addSingletonFactory { Json { ignoreUnknownKeys = true } }
// Asynchronously init expensive components for a faster cold start
GlobalScope.launch { get<PreferencesHelper>() }

View File

@ -1,11 +1,19 @@
package eu.kanade.tachiyomi
import androidx.core.content.edit
import androidx.preference.PreferenceManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferenceKeys
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.ui.library.LibrarySort
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
object Migrations {
@ -18,13 +26,13 @@ object Migrations {
*/
fun upgrade(preferences: PreferencesHelper): Boolean {
val context = preferences.context
val oldVersion = preferences.lastVersionCode().get()
// Cancel app updater job for debug builds that don't include it
if (BuildConfig.DEBUG && !BuildConfig.INCLUDE_UPDATER) {
UpdaterJob.cancelTask(context)
}
val oldVersion = preferences.lastVersionCode().get()
if (oldVersion < BuildConfig.VERSION_CODE) {
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
@ -85,12 +93,40 @@ object Migrations {
}
if (oldVersion < 44) {
// Reset sorting preference if using removed sort by source
@Suppress("DEPRECATION")
if (preferences.librarySortingMode().get() == LibrarySort.SOURCE) {
preferences.librarySortingMode().set(LibrarySort.ALPHA)
}
}
if (oldVersion < 52) {
// Migrate library filters to tri-state versions
val prefs = PreferenceManager.getDefaultSharedPreferences(context)
fun convertBooleanPrefToTriState(key: String): Int {
val oldPrefValue = prefs.getBoolean(key, false)
return if (oldPrefValue) ExtendedNavigationView.Item.TriStateGroup.State.INCLUDE.value
else ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value
}
prefs.edit {
putInt(PreferenceKeys.filterDownloaded, convertBooleanPrefToTriState("pref_filter_downloaded_key"))
remove("pref_filter_downloaded_key")
putInt(PreferenceKeys.filterUnread, convertBooleanPrefToTriState("pref_filter_unread_key"))
remove("pref_filter_unread_key")
putInt(PreferenceKeys.filterCompleted, convertBooleanPrefToTriState("pref_filter_completed_key"))
remove("pref_filter_completed_key")
}
// Force MAL log out due to login flow change
val trackManager = Injekt.get<TrackManager>()
if (trackManager.myAnimeList.isLogged) {
trackManager.myAnimeList.logout()
context.toast(R.string.myanimelist_relogin)
}
}
return true
}
return false
}
}

View File

@ -0,0 +1,5 @@
package eu.kanade.tachiyomi.annotations
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.CLASS)
annotation class Nsfw

View File

@ -0,0 +1,88 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import rx.Observable
import uy.kohesive.injekt.injectLazy
abstract class AbstractBackupManager(protected val context: Context) {
internal val databaseHelper: DatabaseHelper by injectLazy()
internal val sourceManager: SourceManager by injectLazy()
internal val trackManager: TrackManager by injectLazy()
protected val preferences: PreferencesHelper by injectLazy()
abstract fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String?
/**
* Returns manga
*
* @return [Manga], null if not found
*/
internal fun getMangaFromDatabase(manga: Manga): Manga? =
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
/**
* [Observable] that fetches chapter information
*
* @param source source of manga
* @param manga manga that needs updating
* @param chapters list of chapters in the backup
* @return [Observable] that contains manga
*/
internal fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return source.fetchChapterList(manga)
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
.doOnNext { (first) ->
if (first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id }
updateChapters(chapters)
}
}
}
/**
* Returns list containing manga from library
*
* @return [Manga] from library
*/
protected fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking()
/**
* Inserts manga and returns id
*
* @return id of [Manga], null if not found
*/
internal fun insertManga(manga: Manga): Long? =
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
/**
* Inserts list of chapters
*/
protected fun insertChapters(chapters: List<Chapter>) {
databaseHelper.insertChapters(chapters).executeAsBlocking()
}
/**
* Updates a list of chapters
*/
protected fun updateChapters(chapters: List<Chapter>) {
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
}
/**
* Return number of backups.
*
* @return number of backups selected by user
*/
protected fun numberOfBackups(): Int = preferences.numberOfBackups().get()
}

View File

@ -0,0 +1,138 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
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.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import kotlinx.coroutines.Job
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
abstract class AbstractBackupRestore<T : AbstractBackupManager>(protected val context: Context, protected val notifier: BackupNotifier) {
protected val db: DatabaseHelper by injectLazy()
protected val trackManager: TrackManager by injectLazy()
var job: Job? = null
protected lateinit var backupManager: T
protected var restoreAmount = 0
protected var restoreProgress = 0
/**
* Mapping of source ID to source name from backup data
*/
protected var sourceMapping: Map<Long, String> = emptyMap()
protected val errors = mutableListOf<Pair<Date, String>>()
abstract fun performRestore(uri: Uri): Boolean
fun restoreBackup(uri: Uri): Boolean {
val startTime = System.currentTimeMillis()
restoreProgress = 0
errors.clear()
if (!performRestore(uri)) {
return false
}
val endTime = System.currentTimeMillis()
val time = endTime - startTime
val logFile = writeErrorLog()
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
return true
}
/**
* [Observable] that fetches chapter information
*
* @param source source of manga
* @param manga manga that needs updating
* @return [Observable] that contains manga
*/
internal fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
// If there's any error, return empty update and continue.
.onErrorReturn {
val errorMessage = if (it is NoChaptersException) {
context.getString(R.string.no_chapters_error)
} else {
it.message
}
errors.add(Date() to "${manga.title} - $errorMessage")
Pair(emptyList(), emptyList())
}
}
/**
* [Observable] that refreshes tracking information
* @param manga manga that needs updating.
* @param tracks list containing tracks from restore file.
* @return [Observable] that contains updated track item
*/
internal fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
return Observable.from(tracks)
.flatMap { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
track
}
} else {
errors.add(Date() to "${manga.title} - ${context.getString(R.string.tracker_not_logged_in, service?.name)}")
Observable.empty()
}
}
}
/**
* Called to update dialog in [BackupConst]
*
* @param progress restore progress
* @param amount total restoreAmount of manga
* @param title title of restored manga
*/
internal fun showRestoreProgress(
progress: Int,
amount: Int,
title: String
) {
notifier.showRestoreProgress(title, progress, amount)
}
internal fun writeErrorLog(): File {
try {
if (errors.isNotEmpty()) {
val destFile = File(context.externalCacheDir, "tachiyomi_restore.txt")
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
destFile.bufferedWriter().use { out ->
errors.forEach { (date, message) ->
out.write("[${sdf.format(date)}] $message\n")
}
}
return destFile
}
} catch (e: Exception) {
// Empty
}
return File("")
}
}

View File

@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import uy.kohesive.injekt.injectLazy
abstract class AbstractBackupRestoreValidator {
protected val sourceManager: SourceManager by injectLazy()
protected val trackManager: TrackManager by injectLazy()
abstract fun validate(context: Context, uri: Uri): Results
data class Results(val missingSources: List<String>, val missingTrackers: List<String>)
}

View File

@ -7,4 +7,9 @@ object BackupConst {
private const val NAME = "BackupRestoreServices"
const val EXTRA_URI = "$ID.$NAME.EXTRA_URI"
const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
const val EXTRA_MODE = "$ID.$NAME.EXTRA_MODE"
const val EXTRA_TYPE = "$ID.$NAME.EXTRA_TYPE"
const val BACKUP_TYPE_LEGACY = 0
const val BACKUP_TYPE_FULL = 1
}

View File

@ -4,11 +4,13 @@ import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.content.ContextCompat
import androidx.core.net.toUri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
@ -46,17 +48,14 @@ class BackupCreateService : Service() {
* @param uri path of Uri
* @param flags determines what to backup
*/
fun start(context: Context, uri: Uri, flags: Int) {
fun start(context: Context, uri: Uri, flags: Int, type: Int) {
if (!isRunning(context)) {
val intent = Intent(context, BackupCreateService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri)
putExtra(BackupConst.EXTRA_FLAGS, flags)
putExtra(BackupConst.EXTRA_TYPE, type)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
ContextCompat.startForegroundService(context, intent)
}
}
}
@ -66,7 +65,6 @@ class BackupCreateService : Service() {
*/
private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var backupManager: BackupManager
private lateinit var notifier: BackupNotifier
override fun onCreate() {
@ -105,7 +103,11 @@ class BackupCreateService : Service() {
try {
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
backupManager = BackupManager(this)
val backupType = intent.getIntExtra(BackupConst.EXTRA_TYPE, BackupConst.BACKUP_TYPE_LEGACY)
val backupManager = when (backupType) {
BackupConst.BACKUP_TYPE_FULL -> FullBackupManager(this)
else -> LegacyBackupManager(this)
}
val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
val unifile = UniFile.fromUri(this, backupFileUri)

View File

@ -7,21 +7,25 @@ import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.backup.full.FullBackupManager
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupManager
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import java.util.concurrent.TimeUnit
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class BackupCreatorJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>()
val backupManager = BackupManager(context)
val uri = preferences.backupsDirectory().get().toUri()
val flags = BackupCreateService.BACKUP_ALL
return try {
backupManager.createBackup(uri, flags, true)
FullBackupManager(context).createBackup(uri, flags, true)
if (preferences.createLegacyBackup().get()) {
LegacyBackupManager(context).createBackup(uri, flags, true)
}
Result.success()
} catch (e: Exception) {
Result.failure()
@ -36,8 +40,10 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
val interval = prefInterval ?: preferences.backupInterval().get()
if (interval > 0) {
val request = PeriodicWorkRequestBuilder<BackupCreatorJob>(
interval.toLong(), TimeUnit.HOURS,
10, TimeUnit.MINUTES
interval.toLong(),
TimeUnit.HOURS,
10,
TimeUnit.MINUTES
)
.addTag(TAG)
.build()

View File

@ -11,11 +11,11 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.util.concurrent.TimeUnit
import uy.kohesive.injekt.injectLazy
internal class BackupNotifier(private val context: Context) {
class BackupNotifier(private val context: Context) {
private val preferences: PreferencesHelper by injectLazy()
@ -65,10 +65,7 @@ internal class BackupNotifier(private val context: Context) {
with(completeNotificationBuilder) {
setContentTitle(context.getString(R.string.backup_created))
if (unifile.filePath != null) {
setContentText(unifile.filePath)
}
setContentText(unifile.filePath ?: unifile.name)
// Clear old actions if they exist
if (mActions.isNotEmpty()) {
@ -149,7 +146,7 @@ internal class BackupNotifier(private val context: Context) {
val uri = destFile.getUriCompat(context)
addAction(
R.drawable.nnf_ic_file_folder,
R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log),
NotificationReceiver.openErrorLogPendingActivity(context, uri)
)

View File

@ -4,50 +4,22 @@ import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.Manga
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.data.backup.full.FullBackupRestore
import eu.kanade.tachiyomi.data.backup.legacy.LegacyBackupRestore
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import java.io.File
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
import kotlinx.coroutines.CoroutineExceptionHandler
import kotlinx.coroutines.GlobalScope
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
/**
* Restores backup from a JSON file.
* Restores backup.
*/
class BackupRestoreService : Service() {
@ -68,16 +40,14 @@ class BackupRestoreService : Service() {
* @param context context of application
* @param uri path of Uri
*/
fun start(context: Context, uri: Uri) {
fun start(context: Context, uri: Uri, mode: Int, online: Boolean?) {
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) }
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
ContextCompat.startForegroundService(context, intent)
}
}
@ -98,35 +68,9 @@ class BackupRestoreService : Service() {
*/
private lateinit var wakeLock: PowerManager.WakeLock
private var job: Job? = null
/**
* The progress of a backup restore
*/
private var restoreProgress = 0
/**
* Amount of manga in Json file (needed for restore)
*/
private var restoreAmount = 0
/**
* Mapping of source ID to source name from backup data
*/
private var sourceMapping: Map<Long, String> = emptyMap()
/**
* List containing errors
*/
private val errors = mutableListOf<Pair<Date, String>>()
private lateinit var backupManager: BackupManager
private var backupRestore: AbstractBackupRestore<*>? = null
private lateinit var notifier: BackupNotifier
private val db: DatabaseHelper by injectLazy()
private val trackManager: TrackManager by injectLazy()
override fun onCreate() {
super.onCreate()
@ -147,7 +91,7 @@ class BackupRestoreService : Service() {
}
private fun destroyJob() {
job?.cancel()
backupRestore?.job?.cancel()
if (wakeLock.isHeld) {
wakeLock.release()
}
@ -168,299 +112,32 @@ 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.
job?.cancel()
backupRestore?.job?.cancel()
backupRestore = when (mode) {
BackupConst.BACKUP_TYPE_FULL -> FullBackupRestore(this, notifier, online)
else -> LegacyBackupRestore(this, notifier)
}
val handler = CoroutineExceptionHandler { _, exception ->
Timber.e(exception)
writeErrorLog()
backupRestore?.writeErrorLog()
notifier.showRestoreError(exception.message)
stopSelf(startId)
}
job = GlobalScope.launch(handler) {
if (!restoreBackup(uri)) {
backupRestore?.job = GlobalScope.launch(handler) {
if (backupRestore?.restoreBackup(uri) == false) {
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
}
}
job?.invokeOnCompletion {
backupRestore?.job?.invokeOnCompletion {
stopSelf(startId)
}
return START_NOT_STICKY
}
/**
* Restores data from backup file.
*
* @param uri backup file to restore
*/
private fun restoreBackup(uri: Uri): Boolean {
val startTime = System.currentTimeMillis()
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser.parseReader(reader).asJsonObject
// Get parser version
val version = json.get(VERSION)?.asInt ?: 1
// Initialize manager
backupManager = BackupManager(this, version)
val mangasJson = json.get(MANGAS).asJsonArray
restoreAmount = mangasJson.size() + 1 // +1 for categories
restoreProgress = 0
errors.clear()
// Restore categories
json.get(CATEGORIES)?.let { restoreCategories(it) }
// Store source mapping for error messages
sourceMapping = BackupRestoreValidator.getSourceMapping(json)
// Restore individual manga
mangasJson.forEach {
if (job?.isActive != true) {
return false
}
restoreManga(it.asJsonObject)
}
val endTime = System.currentTimeMillis()
val time = endTime - startTime
val logFile = writeErrorLog()
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
return true
}
private fun restoreCategories(categoriesJson: JsonElement) {
db.inTransaction {
backupManager.restoreCategories(categoriesJson.asJsonArray)
}
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
}
private fun restoreManga(mangaJson: JsonObject) {
val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
mangaJson.get(CHAPTERS)
?: JsonArray()
)
val categories = backupManager.parser.fromJson<List<String>>(
mangaJson.get(CATEGORIES)
?: JsonArray()
)
val history = backupManager.parser.fromJson<List<DHistory>>(
mangaJson.get(HISTORY)
?: JsonArray()
)
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
mangaJson.get(TRACK)
?: JsonArray()
)
try {
val source = backupManager.sourceManager.get(manga.source)
if (source != null) {
restoreMangaData(manga, source, chapters, categories, history, tracks)
} else {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found_name, sourceName)}")
}
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
}
/**
* 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
* @param tracks tracking data from json
*/
private fun restoreMangaData(
manga: Manga,
source: Source,
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
) {
val dbManga = backupManager.getMangaFromDatabase(manga)
db.inTransaction {
if (dbManga == null) {
// Manga not in database
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
} 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)
}
}
}
/**
* [Observable] that fetches manga information
*
* @param manga manga that needs updating
* @param chapters chapters of manga that needs updating
* @param categories categories that need updating
*/
private fun restoreMangaFetch(
source: Source,
manga: Manga,
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
) {
backupManager.restoreMangaFetchObservable(source, manga)
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
manga
}
.filter { it.id != null }
.flatMap {
chapterFetchObservable(source, it, chapters)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap {
trackingFetchObservable(it, tracks)
}
.subscribe()
}
private fun restoreMangaNoFetch(
source: Source,
backupManga: Manga,
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
) {
Observable.just(backupManga)
.flatMap { manga ->
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
chapterFetchObservable(source, manga, chapters)
.map { manga }
} else {
Observable.just(manga)
}
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap { manga ->
trackingFetchObservable(manga, tracks)
}
.subscribe()
}
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
// Restore categories
backupManager.restoreCategoriesForManga(manga, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(manga, tracks)
}
/**
* [Observable] that fetches chapter information
*
* @param source source of manga
* @param manga manga that needs updating
* @return [Observable] that contains manga
*/
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
// If there's any error, return empty update and continue.
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
Pair(emptyList(), emptyList())
}
}
/**
* [Observable] that refreshes tracking information
* @param manga manga that needs updating.
* @param tracks list containing tracks from restore file.
* @return [Observable] that contains updated track item
*/
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
return Observable.from(tracks)
.flatMap { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
track
}
} else {
errors.add(Date() to "${manga.title} - ${getString(R.string.tracker_not_logged_in, service?.name)}")
Observable.empty()
}
}
}
/**
* Called to update dialog in [BackupConst]
*
* @param progress restore progress
* @param amount total restoreAmount of manga
* @param title title of restored manga
*/
private fun showRestoreProgress(
progress: Int,
amount: Int,
title: String
) {
notifier.showRestoreProgress(title, progress, amount)
}
/**
* Write errors to error log
*/
private fun writeErrorLog(): File {
try {
if (errors.isNotEmpty()) {
val destFile = File(externalCacheDir, "tachiyomi_restore.txt")
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
destFile.bufferedWriter().use { out ->
errors.forEach { (date, message) ->
out.write("[${sdf.format(date)}] $message\n")
}
}
return destFile
}
} catch (e: Exception) {
// Empty
}
return File("")
}
}

View File

@ -1,46 +0,0 @@
package eu.kanade.tachiyomi.data.backup
import android.content.Context
import android.net.Uri
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.models.Backup
object BackupRestoreValidator {
/**
* Checks for critical backup file data.
*
* @throws Exception if version or manga cannot be found.
* @return List of required sources.
*/
fun validate(context: Context, uri: Uri): Map<Long, String> {
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser.parseReader(reader).asJsonObject
val version = json.get(Backup.VERSION)
val mangasJson = json.get(Backup.MANGAS)
if (version == null || mangasJson == null) {
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
}
if (mangasJson.asJsonArray.size() == 0) {
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
}
return getSourceMapping(json)
}
fun getSourceMapping(json: JsonObject): Map<Long, String> {
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
return extensionsMapping.asJsonArray
.map {
val items = it.asString.split(":")
items[0].toLong() to items[1]
}
.toMap()
}
}

View File

@ -0,0 +1,412 @@
package eu.kanade.tachiyomi.data.backup.full
import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.full.models.Backup
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.full.models.BackupChapter
import eu.kanade.tachiyomi.data.backup.full.models.BackupFull
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
import eu.kanade.tachiyomi.data.backup.full.models.BackupSerializer
import eu.kanade.tachiyomi.data.backup.full.models.BackupSource
import eu.kanade.tachiyomi.data.backup.full.models.BackupTracking
import eu.kanade.tachiyomi.data.database.models.Chapter
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.source.Source
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.protobuf.ProtoBuf
import okio.buffer
import okio.gzip
import okio.sink
import rx.Observable
import timber.log.Timber
import kotlin.math.max
@OptIn(ExperimentalSerializationApi::class)
class FullBackupManager(context: Context) : AbstractBackupManager(context) {
val parser = ProtoBuf
/**
* Create backup Json file from database
*
* @param uri path of Uri
* @param isJob backup called from job
*/
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
// Create root object
var backup: Backup? = null
databaseHelper.inTransaction {
val databaseManga = getFavoriteManga()
backup = Backup(
backupManga(databaseManga, flags),
backupCategories(),
backupExtensionInfo(databaseManga)
)
}
try {
val file: UniFile = (
if (isJob) {
// Get dir of file and create
var dir = UniFile.fromUri(context, uri)
dir = dir.createDirectory("automatic")
// Delete older backups
val numberOfBackups = numberOfBackups()
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.proto.gz""")
dir.listFiles { _, filename -> backupRegex.matches(filename) }
.orEmpty()
.sortedByDescending { it.name }
.drop(numberOfBackups - 1)
.forEach { it.delete() }
// Create new file to place backup
dir.createFile(BackupFull.getDefaultFilename())
} else {
UniFile.fromUri(context, uri)
}
)
?: throw Exception("Couldn't create backup file")
val byteArray = parser.encodeToByteArray(BackupSerializer, backup!!)
file.openOutputStream().sink().gzip().buffer().use { it.write(byteArray) }
return file.uri.toString()
} catch (e: Exception) {
Timber.e(e)
throw e
}
}
private fun backupManga(mangas: List<Manga>, flags: Int): List<BackupManga> {
return mangas.map {
backupMangaObject(it, flags)
}
}
private fun backupExtensionInfo(mangas: List<Manga>): List<BackupSource> {
return mangas
.asSequence()
.map { it.source }
.distinct()
.map { sourceManager.getOrStub(it) }
.map { BackupSource.copyFrom(it) }
.toList()
}
/**
* Backup the categories of library
*
* @return list of [BackupCategory] to be backed up
*/
private fun backupCategories(): List<BackupCategory> {
return databaseHelper.getCategories()
.executeAsBlocking()
.map { BackupCategory.copyFrom(it) }
}
/**
* Convert a manga to Json
*
* @param manga manga that gets converted
* @param options options for the backup
* @return [BackupManga] containing manga in a serializable form
*/
private fun backupMangaObject(manga: Manga, options: Int): BackupManga {
// Entry for this manga
val mangaObject = BackupManga.copyFrom(manga)
// Check if user wants chapter information in backup
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
// Backup all the chapters
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
if (chapters.isNotEmpty()) {
mangaObject.chapters = chapters.map { BackupChapter.copyFrom(it) }
}
}
// Check if user wants category information in backup
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
// Backup categories for this manga
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
if (categoriesForManga.isNotEmpty()) {
mangaObject.categories = categoriesForManga.mapNotNull { it.order }
}
}
// Check if user wants track information in backup
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
if (tracks.isNotEmpty()) {
mangaObject.tracking = tracks.map { BackupTracking.copyFrom(it) }
}
}
// Check if user wants history information in backup
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
if (historyForManga.isNotEmpty()) {
val history = historyForManga.mapNotNull { history ->
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
url?.let { BackupHistory(url, history.last_read) }
}
if (history.isNotEmpty()) {
mangaObject.history = history
}
}
}
return mangaObject
}
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
manga.id = dbManga.id
manga.copyFrom(dbManga)
insertManga(manga)
}
/**
* [Observable] that fetches manga information
*
* @param source source of manga
* @param manga manga that needs updating
* @return [Observable] that contains manga
*/
fun restoreMangaFetchObservable(source: Source?, manga: Manga, online: Boolean): Observable<Manga> {
return if (online && source != null) {
source.fetchMangaDetails(manga)
.map { networkManga ->
manga.copyFrom(networkManga)
manga.favorite = manga.favorite
manga.initialized = true
manga.id = insertManga(manga)
manga
}
} else {
Observable.just(manga)
.map {
it.initialized = it.description != null
it.id = insertManga(it)
it
}
}
}
/**
* Restore the categories from Json
*
* @param backupCategories list containing categories
*/
internal fun restoreCategories(backupCategories: List<BackupCategory>) {
// Get categories from file and from db
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
// Iterate over them
backupCategories.map { it.getCategoryImpl() }.forEach { category ->
// Used to know if the category is already in the db
var found = false
for (dbCategory in dbCategories) {
// If the category is already in the db, assign the id to the file's category
// and do nothing
if (category.name == dbCategory.name) {
category.id = dbCategory.id
found = true
break
}
}
// If the category isn't in the db, remove the id and insert a new category
// Store the inserted id in the category
if (!found) {
// Let the db assign the id
category.id = null
val result = databaseHelper.insertCategory(category).executeAsBlocking()
category.id = result.insertedId()?.toInt()
}
}
}
/**
* Restores the categories a manga is in.
*
* @param manga the manga whose categories have to be restored.
* @param categories the categories to restore.
*/
internal fun restoreCategoriesForManga(manga: Manga, categories: List<Int>, backupCategories: List<BackupCategory>) {
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
categories.forEach { backupCategoryOrder ->
backupCategories.firstOrNull {
it.order == backupCategoryOrder
}?.let { backupCategory ->
dbCategories.firstOrNull { dbCategory ->
dbCategory.name == backupCategory.name
}?.let { dbCategory ->
mangaCategoriesToUpdate += MangaCategory.create(manga, dbCategory)
}
}
}
// Update database
if (mangaCategoriesToUpdate.isNotEmpty()) {
databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking()
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
}
}
/**
* Restore history from Json
*
* @param history list containing history to be restored
*/
internal fun restoreHistoryForManga(history: List<BackupHistory>) {
// List containing history to be updated
val historyToBeUpdated = mutableListOf<History>()
for ((url, lastRead) in history) {
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
// Check if history already in database and update
if (dbHistory != null) {
dbHistory.apply {
last_read = max(lastRead, dbHistory.last_read)
}
historyToBeUpdated.add(dbHistory)
} else {
// If not in database create
databaseHelper.getChapter(url).executeAsBlocking()?.let {
val historyToAdd = History.create(it).apply {
last_read = lastRead
}
historyToBeUpdated.add(historyToAdd)
}
}
}
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
}
/**
* Restores the sync of a manga.
*
* @param manga the manga whose sync have to be restored.
* @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>()
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
}
}
if (!isInDatabase) {
// Insert new sync. Let the db assign the id
track.id = null
trackToUpdate.add(track)
}
}
}
// Update database
if (trackToUpdate.isNotEmpty()) {
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
}
}
/**
* 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 {
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]
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
}
}
}
// 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 })
}
}

View File

@ -0,0 +1,205 @@
package eu.kanade.tachiyomi.data.backup.full
import android.content.Context
import android.net.Uri
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.backup.full.models.BackupCategory
import eu.kanade.tachiyomi.data.backup.full.models.BackupHistory
import eu.kanade.tachiyomi.data.backup.full.models.BackupManga
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 kotlinx.serialization.ExperimentalSerializationApi
import okio.buffer
import okio.gzip
import okio.source
import rx.Observable
import java.util.Date
@OptIn(ExperimentalSerializationApi::class)
class FullBackupRestore(context: Context, notifier: BackupNotifier, private val online: Boolean) : AbstractBackupRestore<FullBackupManager>(context, notifier) {
override fun performRestore(uri: Uri): Boolean {
backupManager = FullBackupManager(context)
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
restoreAmount = backup.backupManga.size + 1 // +1 for categories
// Restore categories
if (backup.backupCategories.isNotEmpty()) {
restoreCategories(backup.backupCategories)
}
// Store source mapping for error messages
sourceMapping = backup.backupSources.map { it.sourceId to it.name }.toMap()
// Restore individual manga
backup.backupManga.forEach {
if (job?.isActive != true) {
return false
}
restoreManga(it, backup.backupCategories, online)
}
return true
}
private fun restoreCategories(backupCategories: List<BackupCategory>) {
db.inTransaction {
backupManager.restoreCategories(backupCategories)
}
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
}
private fun restoreManga(backupManga: BackupManga, backupCategories: List<BackupCategory>, online: Boolean) {
val manga = backupManga.getMangaImpl()
val chapters = backupManga.getChaptersImpl()
val categories = backupManga.categories
val history = backupManga.history
val tracks = backupManga.getTrackingImpl()
try {
val source = backupManager.sourceManager.get(manga.source)
if (source != null || !online) {
restoreMangaData(manga, source, chapters, categories, history, tracks, backupCategories, online)
} else {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} - ${context.getString(R.string.source_not_found_name, sourceName)}")
}
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
}
/**
* 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
* @param tracks tracking data from json
*/
private fun restoreMangaData(
manga: Manga,
source: Source?,
chapters: List<Chapter>,
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>,
online: Boolean
) {
val dbManga = backupManager.getMangaFromDatabase(manga)
db.inTransaction {
if (dbManga == null) {
// Manga not in database
restoreMangaFetch(source, manga, chapters, categories, history, tracks, backupCategories, online)
} 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)
}
}
}
/**
* [Observable] that fetches manga information
*
* @param manga manga that needs updating
* @param chapters chapters of manga that needs updating
* @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
) {
backupManager.restoreMangaFetchObservable(source, manga, online)
.doOnError {
errors.add(Date() to "${manga.title} - ${it.message}")
}
.filter { it.id != null }
.flatMap {
if (online && source != null) {
chapterFetchObservable(source, it, chapters)
// Convert to the manga that contains new chapters.
.map { manga }
} else {
backupManager.restoreChaptersForMangaOffline(it, chapters)
Observable.just(manga)
}
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks, backupCategories)
}
.flatMap {
trackingFetchObservable(it, tracks)
}
.subscribe()
}
private fun restoreMangaNoFetch(
source: Source?,
backupManga: Manga,
chapters: List<Chapter>,
categories: List<Int>,
history: List<BackupHistory>,
tracks: List<Track>,
backupCategories: List<BackupCategory>,
online: Boolean
) {
Observable.just(backupManga)
.flatMap { manga ->
if (online && source != null) {
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
chapterFetchObservable(source, manga, chapters)
.map { manga }
} else {
Observable.just(manga)
}
} else {
backupManager.restoreChaptersForMangaOffline(manga, chapters)
Observable.just(manga)
}
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks, backupCategories)
}
.flatMap { manga ->
trackingFetchObservable(manga, tracks)
}
.subscribe()
}
private fun restoreExtraForManga(manga: Manga, categories: List<Int>, history: List<BackupHistory>, tracks: List<Track>, backupCategories: List<BackupCategory>) {
// Restore categories
backupManager.restoreCategoriesForManga(manga, categories, backupCategories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(manga, tracks)
}
}

View File

@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.data.backup.full
import android.content.Context
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.
*
* @throws Exception if manga cannot be found.
* @return List of missing sources or missing trackers.
*/
override fun validate(context: Context, uri: Uri): Results {
val backupManager = FullBackupManager(context)
val backupString = context.contentResolver.openInputStream(uri)!!.source().gzip().buffer().use { it.readByteArray() }
val backup = backupManager.parser.decodeFromByteArray(BackupSerializer, backupString)
if (backup.backupManga.isEmpty()) {
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
}
val sources = backup.backupSources.map { it.sourceId to it.name }.toMap()
val missingSources = sources
.filter { sourceManager.get(it.key) == null }
.values
.sorted()
val trackers = backup.backupManga
.flatMap { it.tracking }
.map { it.syncId }
.distinct()
val missingTrackers = trackers
.mapNotNull { trackManager.getService(it) }
.filter { !it.isLogged }
.map { it.name }
.sorted()
return Results(missingSources, missingTrackers)
}
}

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.data.backup.full.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class Backup(
@ProtoNumber(1) val backupManga: List<BackupManga>,
@ProtoNumber(2) var backupCategories: List<BackupCategory> = emptyList(),
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var backupSources: List<BackupSource> = emptyList(),
)

View File

@ -0,0 +1,33 @@
package eu.kanade.tachiyomi.data.backup.full.models
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
class BackupCategory(
@ProtoNumber(1) var name: String,
@ProtoNumber(2) var order: Int = 0,
// @ProtoNumber(3) val updateInterval: Int = 0, 1.x value not used in 0.x
// Bump by 100 to specify this is a 0.x value
@ProtoNumber(100) var flags: Int = 0,
) {
fun getCategoryImpl(): CategoryImpl {
return CategoryImpl().apply {
name = this@BackupCategory.name
flags = this@BackupCategory.flags
order = this@BackupCategory.order
}
}
companion object {
fun copyFrom(category: Category): BackupCategory {
return BackupCategory(
name = category.name,
order = category.order,
flags = category.flags
)
}
}
}

View File

@ -0,0 +1,56 @@
package eu.kanade.tachiyomi.data.backup.full.models
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BackupChapter(
// in 1.x some of these values have different names
// url is called key in 1.x
@ProtoNumber(1) var url: String,
@ProtoNumber(2) var name: String,
@ProtoNumber(3) var scanlator: String? = null,
@ProtoNumber(4) var read: Boolean = false,
@ProtoNumber(5) var bookmark: Boolean = false,
// lastPageRead is called progress in 1.x
@ProtoNumber(6) var lastPageRead: Int = 0,
@ProtoNumber(7) var dateFetch: Long = 0,
@ProtoNumber(8) var dateUpload: Long = 0,
// chapterNumber is called number is 1.x
@ProtoNumber(9) var chapterNumber: Float = 0F,
@ProtoNumber(10) var sourceOrder: Int = 0,
) {
fun toChapterImpl(): ChapterImpl {
return ChapterImpl().apply {
url = this@BackupChapter.url
name = this@BackupChapter.name
chapter_number = this@BackupChapter.chapterNumber
scanlator = this@BackupChapter.scanlator
read = this@BackupChapter.read
bookmark = this@BackupChapter.bookmark
last_page_read = this@BackupChapter.lastPageRead
date_fetch = this@BackupChapter.dateFetch
date_upload = this@BackupChapter.dateUpload
source_order = this@BackupChapter.sourceOrder
}
}
companion object {
fun copyFrom(chapter: Chapter): BackupChapter {
return BackupChapter(
url = chapter.url,
name = chapter.name,
chapterNumber = chapter.chapter_number,
scanlator = chapter.scanlator,
read = chapter.read,
bookmark = chapter.bookmark,
lastPageRead = chapter.last_page_read,
dateFetch = chapter.date_fetch,
dateUpload = chapter.date_upload,
sourceOrder = chapter.source_order
)
}
}
}

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.data.backup.full.models
import java.text.SimpleDateFormat
import java.util.Date
import java.util.Locale
object BackupFull {
fun getDefaultFilename(): String {
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
return "tachiyomi_$date.proto.gz"
}
}

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.data.backup.full.models
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BackupHistory(
@ProtoNumber(0) var url: String,
@ProtoNumber(1) var lastRead: Long
)

View File

@ -0,0 +1,87 @@
package eu.kanade.tachiyomi.data.backup.full.models
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BackupManga(
// in 1.x some of these values have different names
@ProtoNumber(1) var source: Long,
// url is called key in 1.x
@ProtoNumber(2) var url: String,
@ProtoNumber(3) var title: String = "",
@ProtoNumber(4) var artist: String? = null,
@ProtoNumber(5) var author: String? = null,
@ProtoNumber(6) var description: String? = null,
@ProtoNumber(7) var genre: List<String> = emptyList(),
@ProtoNumber(8) var status: Int = 0,
// thumbnailUrl is called cover in 1.x
@ProtoNumber(9) var thumbnailUrl: String? = null,
// @ProtoNumber(10) val customCover: String = "", 1.x value, not used in 0.x
// @ProtoNumber(11) val lastUpdate: Long = 0, 1.x value, not used in 0.x
// @ProtoNumber(12) val lastInit: Long = 0, 1.x value, not used in 0.x
@ProtoNumber(13) var dateAdded: Long = 0,
@ProtoNumber(14) var viewer: Int = 0,
// @ProtoNumber(15) val flags: Int = 0, 1.x value, not used in 0.x
@ProtoNumber(16) var chapters: List<BackupChapter> = emptyList(),
@ProtoNumber(17) var categories: List<Int> = emptyList(),
@ProtoNumber(18) var tracking: List<BackupTracking> = emptyList(),
// Bump by 100 for values that are not saved/implemented in 1.x but are used in 0.x
@ProtoNumber(100) var favorite: Boolean = true,
@ProtoNumber(101) var chapterFlags: Int = 0,
@ProtoNumber(102) var history: List<BackupHistory> = emptyList(),
) {
fun getMangaImpl(): MangaImpl {
return MangaImpl().apply {
url = this@BackupManga.url
title = this@BackupManga.title
artist = this@BackupManga.artist
author = this@BackupManga.author
description = this@BackupManga.description
genre = this@BackupManga.genre.joinToString()
status = this@BackupManga.status
thumbnail_url = this@BackupManga.thumbnailUrl
favorite = this@BackupManga.favorite
source = this@BackupManga.source
date_added = this@BackupManga.dateAdded
viewer = this@BackupManga.viewer
chapter_flags = this@BackupManga.chapterFlags
}
}
fun getChaptersImpl(): List<ChapterImpl> {
return chapters.map {
it.toChapterImpl()
}
}
fun getTrackingImpl(): List<TrackImpl> {
return tracking.map {
it.getTrackingImpl()
}
}
companion object {
fun copyFrom(manga: Manga): BackupManga {
return BackupManga(
url = manga.url,
title = manga.title,
artist = manga.artist,
author = manga.author,
description = manga.description,
genre = manga.getGenres() ?: emptyList(),
status = manga.status,
thumbnailUrl = manga.thumbnail_url,
favorite = manga.favorite,
source = manga.source,
dateAdded = manga.date_added,
viewer = manga.viewer,
chapterFlags = manga.chapter_flags
)
}
}
}

View File

@ -0,0 +1,6 @@
package eu.kanade.tachiyomi.data.backup.full.models
import kotlinx.serialization.Serializer
@Serializer(forClass = Backup::class)
object BackupSerializer

View File

@ -0,0 +1,20 @@
package eu.kanade.tachiyomi.data.backup.full.models
import eu.kanade.tachiyomi.source.Source
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BackupSource(
@ProtoNumber(0) var name: String = "",
@ProtoNumber(1) var sourceId: Long
) {
companion object {
fun copyFrom(source: Source): BackupSource {
return BackupSource(
name = source.name,
sourceId = source.id
)
}
}
}

View File

@ -0,0 +1,65 @@
package eu.kanade.tachiyomi.data.backup.full.models
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import kotlinx.serialization.Serializable
import kotlinx.serialization.protobuf.ProtoNumber
@Serializable
data class BackupTracking(
// in 1.x some of these values have different types or names
// syncId is called siteId in 1,x
@ProtoNumber(1) var syncId: Int,
// LibraryId is not null in 1.x
@ProtoNumber(2) var libraryId: Long,
@ProtoNumber(3) var mediaId: Int = 0,
// trackingUrl is called mediaUrl in 1.x
@ProtoNumber(4) var trackingUrl: String = "",
@ProtoNumber(5) var title: String = "",
// lastChapterRead is called last read, and it has been changed to a float in 1.x
@ProtoNumber(6) var lastChapterRead: Float = 0F,
@ProtoNumber(7) var totalChapters: Int = 0,
@ProtoNumber(8) var score: Float = 0F,
@ProtoNumber(9) var status: Int = 0,
// startedReadingDate is called startReadTime in 1.x
@ProtoNumber(10) var startedReadingDate: Long = 0,
// finishedReadingDate is called endReadTime in 1.x
@ProtoNumber(11) var finishedReadingDate: Long = 0,
) {
fun getTrackingImpl(): TrackImpl {
return TrackImpl().apply {
sync_id = this@BackupTracking.syncId
media_id = this@BackupTracking.mediaId
library_id = this@BackupTracking.libraryId
title = this@BackupTracking.title
// convert from float to int because of 1.x types
last_chapter_read = this@BackupTracking.lastChapterRead.toInt()
total_chapters = this@BackupTracking.totalChapters
score = this@BackupTracking.score
status = this@BackupTracking.status
started_reading_date = this@BackupTracking.startedReadingDate
finished_reading_date = this@BackupTracking.finishedReadingDate
tracking_url = this@BackupTracking.trackingUrl
}
}
companion object {
fun copyFrom(track: Track): BackupTracking {
return BackupTracking(
syncId = track.sync_id,
mediaId = track.media_id,
// forced not null so its compatible with 1.x backup system
libraryId = track.library_id!!,
title = track.title,
// convert to float for 1.x
lastChapterRead = track.last_chapter_read.toFloat(),
totalChapters = track.total_chapters,
score = track.score,
status = track.status,
startedReadingDate = track.started_reading_date,
finishedReadingDate = track.finished_reading_date,
trackingUrl = track.tracking_url
)
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.backup
package eu.kanade.tachiyomi.data.backup.legacy
import android.content.Context
import android.net.Uri
@ -12,6 +12,7 @@ import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.AbstractBackupManager
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
@ -20,21 +21,20 @@ import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HIST
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
import eu.kanade.tachiyomi.data.backup.models.Backup.EXTENSIONS
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.backup.serializer.CategoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.ChapterTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.HistoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.MangaTypeAdapter
import eu.kanade.tachiyomi.data.backup.serializer.TrackTypeAdapter
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CATEGORIES
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CHAPTERS
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.CURRENT_VERSION
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.EXTENSIONS
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.HISTORY
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGA
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.TRACK
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
import eu.kanade.tachiyomi.data.backup.legacy.serializer.CategoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.legacy.serializer.ChapterTypeAdapter
import eu.kanade.tachiyomi.data.backup.legacy.serializer.HistoryTypeAdapter
import eu.kanade.tachiyomi.data.backup.legacy.serializer.MangaTypeAdapter
import eu.kanade.tachiyomi.data.backup.legacy.serializer.TrackTypeAdapter
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
@ -44,33 +44,17 @@ import eu.kanade.tachiyomi.data.database.models.MangaCategory
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.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import kotlin.math.max
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import kotlin.math.max
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
class LegacyBackupManager(context: Context, version: Int = CURRENT_VERSION) : AbstractBackupManager(context) {
internal val databaseHelper: DatabaseHelper by injectLazy()
internal val sourceManager: SourceManager by injectLazy()
internal val trackManager: TrackManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
/**
* Version of parser
*/
var version: Int = version
var parserVersion: Int = version
private set
/**
* Json Parser
*/
var parser: Gson = initParser()
/**
@ -79,12 +63,11 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* @param version version of parser
*/
internal fun setVersion(version: Int) {
this.version = version
this.parserVersion = version
parser = initParser()
}
private fun initParser(): Gson = when (version) {
1 -> GsonBuilder().create()
private fun initParser(): Gson = when (parserVersion) {
2 ->
GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
@ -93,7 +76,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
.create()
else -> throw Exception("Json version unknown")
else -> throw Exception("Unknown backup version")
}
/**
@ -102,7 +85,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
* @param uri path of Uri
* @param isJob backup called from job
*/
fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
override fun createBackup(uri: Uri, flags: Int, isJob: Boolean): String? {
// Create root object
val root = JsonObject()
@ -122,7 +105,6 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
root[EXTENSIONS] = extensionEntries
databaseHelper.inTransaction {
// Get manga from database
val mangas = getFavoriteManga()
val extensions: MutableSet<String> = mutableSetOf()
@ -149,39 +131,33 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
}
try {
// When BackupCreatorJob
if (isJob) {
// Get dir of file and create
var dir = UniFile.fromUri(context, uri)
dir = dir.createDirectory("automatic")
val file: UniFile = (
if (isJob) {
// Get dir of file and create
var dir = UniFile.fromUri(context, uri)
dir = dir.createDirectory("automatic")
// Delete older backups
val numberOfBackups = numberOfBackups()
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
dir.listFiles { _, filename -> backupRegex.matches(filename) }
.orEmpty()
.sortedByDescending { it.name }
.drop(numberOfBackups - 1)
.forEach { it.delete() }
// Delete older backups
val numberOfBackups = numberOfBackups()
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
dir.listFiles { _, filename -> backupRegex.matches(filename) }
.orEmpty()
.sortedByDescending { it.name }
.drop(numberOfBackups - 1)
.forEach { it.delete() }
// Create new file to place backup
val newFile = dir.createFile(Backup.getDefaultFilename())
?: throw Exception("Couldn't create backup file")
newFile.openOutputStream().bufferedWriter().use {
parser.toJson(root, it)
// Create new file to place backup
dir.createFile(Backup.getDefaultFilename())
} else {
UniFile.fromUri(context, uri)
}
)
?: throw Exception("Couldn't create backup file")
return newFile.uri.toString()
} else {
val file = UniFile.fromUri(context, uri)
?: throw Exception("Couldn't create backup file")
file.openOutputStream().bufferedWriter().use {
parser.toJson(root, it)
}
return file.uri.toString()
file.openOutputStream().bufferedWriter().use {
parser.toJson(root, it)
}
return file.uri.toString()
} catch (e: Exception) {
Timber.e(e)
throw e
@ -290,24 +266,6 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
}
}
/**
* [Observable] that fetches chapter information
*
* @param source source of manga
* @param manga manga that needs updating
* @return [Observable] that contains manga
*/
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return source.fetchChapterList(manga)
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
.doOnNext { pair ->
if (pair.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id }
insertChapters(chapters)
}
}
}
/**
* Restore the categories from Json
*
@ -470,45 +428,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
chapters.filter { it.id != null }
chapters.map { it.manga_id = manga.id }
insertChapters(chapters)
updateChapters(chapters)
return true
}
/**
* Returns manga
*
* @return [Manga], null if not found
*/
internal fun getMangaFromDatabase(manga: Manga): Manga? =
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
/**
* Returns list containing manga from library
*
* @return [Manga] from library
*/
internal fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking()
/**
* Inserts manga and returns id
*
* @return id of [Manga], null if not found
*/
internal fun insertManga(manga: Manga): Long? =
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
/**
* Inserts list of chapters
*/
private fun insertChapters(chapters: List<Chapter>) {
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
}
/**
* Return number of backups.
*
* @return number of backups selected by user
*/
fun numberOfBackups(): Int = preferences.numberOfBackups().get()
}

View File

@ -0,0 +1,209 @@
package eu.kanade.tachiyomi.data.backup.legacy
import android.content.Context
import android.net.Uri
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestore
import eu.kanade.tachiyomi.data.backup.BackupNotifier
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup.MANGAS
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.Manga
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 rx.Observable
import java.util.Date
class LegacyBackupRestore(context: Context, notifier: BackupNotifier) : AbstractBackupRestore<LegacyBackupManager>(context, notifier) {
override fun performRestore(uri: Uri): Boolean {
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser.parseReader(reader).asJsonObject
val version = json.get(Backup.VERSION)?.asInt ?: 1
backupManager = LegacyBackupManager(context, version)
val mangasJson = json.get(MANGAS).asJsonArray
restoreAmount = mangasJson.size() + 1 // +1 for categories
// Restore categories
json.get(Backup.CATEGORIES)?.let { restoreCategories(it) }
// Store source mapping for error messages
sourceMapping = LegacyBackupRestoreValidator.getSourceMapping(json)
// Restore individual manga
mangasJson.forEach {
if (job?.isActive != true) {
return false
}
restoreManga(it.asJsonObject)
}
return true
}
private fun restoreCategories(categoriesJson: JsonElement) {
db.inTransaction {
backupManager.restoreCategories(categoriesJson.asJsonArray)
}
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, context.getString(R.string.categories))
}
private fun restoreManga(mangaJson: JsonObject) {
val manga = backupManager.parser.fromJson<MangaImpl>(
mangaJson.get(
Backup.MANGA
)
)
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
mangaJson.get(Backup.CHAPTERS)
?: JsonArray()
)
val categories = backupManager.parser.fromJson<List<String>>(
mangaJson.get(Backup.CATEGORIES)
?: JsonArray()
)
val history = backupManager.parser.fromJson<List<DHistory>>(
mangaJson.get(Backup.HISTORY)
?: JsonArray()
)
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
mangaJson.get(Backup.TRACK)
?: JsonArray()
)
try {
val source = backupManager.sourceManager.get(manga.source)
if (source != null) {
restoreMangaData(manga, source, chapters, categories, history, tracks)
} else {
val sourceName = sourceMapping[manga.source] ?: manga.source.toString()
errors.add(Date() to "${manga.title} - ${context.getString(R.string.source_not_found_name, sourceName)}")
}
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${e.message}")
}
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
}
/**
* 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
* @param tracks tracking data from json
*/
private fun restoreMangaData(
manga: Manga,
source: Source,
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
) {
val dbManga = backupManager.getMangaFromDatabase(manga)
db.inTransaction {
if (dbManga == null) {
// Manga not in database
restoreMangaFetch(source, manga, chapters, categories, history, tracks)
} 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)
}
}
}
/**
* [Observable] that fetches manga information
*
* @param manga manga that needs updating
* @param chapters chapters of manga that needs updating
* @param categories categories that need updating
*/
private fun restoreMangaFetch(
source: Source,
manga: Manga,
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
) {
backupManager.restoreMangaFetchObservable(source, manga)
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
manga
}
.filter { it.id != null }
.flatMap {
chapterFetchObservable(source, it, chapters)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap {
trackingFetchObservable(it, tracks)
}
.subscribe()
}
private fun restoreMangaNoFetch(
source: Source,
backupManga: Manga,
chapters: List<Chapter>,
categories: List<String>,
history: List<DHistory>,
tracks: List<Track>
) {
Observable.just(backupManga)
.flatMap { manga ->
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
chapterFetchObservable(source, manga, chapters)
.map { manga }
} else {
Observable.just(manga)
}
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap { manga ->
trackingFetchObservable(manga, tracks)
}
.subscribe()
}
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
// Restore categories
backupManager.restoreCategoriesForManga(manga, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(manga, tracks)
}
}

View File

@ -0,0 +1,66 @@
package eu.kanade.tachiyomi.data.backup.legacy
import android.content.Context
import android.net.Uri
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.AbstractBackupRestoreValidator
import eu.kanade.tachiyomi.data.backup.legacy.models.Backup
class LegacyBackupRestoreValidator : AbstractBackupRestoreValidator() {
/**
* Checks for critical backup file data.
*
* @throws Exception if version or manga cannot be found.
* @return List of missing sources or missing trackers.
*/
override fun validate(context: Context, uri: Uri): Results {
val reader = JsonReader(context.contentResolver.openInputStream(uri)!!.bufferedReader())
val json = JsonParser.parseReader(reader).asJsonObject
val version = json.get(Backup.VERSION)
val mangasJson = json.get(Backup.MANGAS)
if (version == null || mangasJson == null) {
throw Exception(context.getString(R.string.invalid_backup_file_missing_data))
}
val mangas = mangasJson.asJsonArray
if (mangas.size() == 0) {
throw Exception(context.getString(R.string.invalid_backup_file_missing_manga))
}
val sources = getSourceMapping(json)
val missingSources = sources
.filter { sourceManager.get(it.key) == null }
.values
.sorted()
val trackers = mangas
.filter { it.asJsonObject.has("track") }
.flatMap { it.asJsonObject["track"].asJsonArray }
.map { it.asJsonObject["s"].asInt }
.distinct()
val missingTrackers = trackers
.mapNotNull { trackManager.getService(it) }
.filter { !it.isLogged }
.map { it.name }
.sorted()
return Results(missingSources, missingTrackers)
}
companion object {
fun getSourceMapping(json: JsonObject): Map<Long, String> {
val extensionsMapping = json.get(Backup.EXTENSIONS) ?: return emptyMap()
return extensionsMapping.asJsonArray
.map {
val items = it.asString.split(":")
items[0].toLong() to items[1]
}
.toMap()
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.backup.models
package eu.kanade.tachiyomi.data.backup.legacy.models
import java.text.SimpleDateFormat
import java.util.Date

View File

@ -1,3 +1,3 @@
package eu.kanade.tachiyomi.data.backup.models
package eu.kanade.tachiyomi.data.backup.legacy.models
data class DHistory(val url: String, val lastRead: Long)

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.backup.serializer
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.backup.serializer
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter

View File

@ -1,8 +1,8 @@
package eu.kanade.tachiyomi.data.backup.serializer
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.backup.legacy.models.DHistory
/**
* JSON Serializer used to write / read [DHistory] to / from json

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.backup.serializer
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.data.backup.serializer
package eu.kanade.tachiyomi.data.backup.legacy.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter

View File

@ -9,13 +9,13 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
import java.io.File
import java.io.IOException
import okhttp3.Response
import okio.buffer
import okio.sink
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.IOException
/**
* Class used to create chapter cache

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.source.model.SManga
import tachiyomi.source.model.MangaInfo
interface Manga : SManga {
@ -98,3 +99,16 @@ interface Manga : SManga {
}
}
}
fun Manga.toMangaInfo(): MangaInfo {
return MangaInfo(
artist = this.artist ?: "",
author = this.author ?: "",
cover = this.thumbnail_url ?: "",
description = this.description ?: "",
genres = this.getGenres() ?: emptyList(),
key = this.url,
status = this.status,
title = this.title
)
}

View File

@ -21,13 +21,16 @@ interface HistoryQueries : DbProvider {
/**
* Returns history of recent manga containing last read chapter
* @param date recent date range
* @param limit the limit of manga to grab
* @param offset offset the db by
* @param search what to search in the db history
*/
fun getRecentManga(date: Date) = db.get()
fun getRecentManga(date: Date, limit: Int = 25, offset: Int = 0, search: String = "") = db.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(
RawQuery.builder()
.query(getRecentMangasQuery())
.args(date.time)
.query(getRecentMangasQuery(search))
.args(date.time, limit, offset)
.observesTables(HistoryTable.TABLE)
.build()
)

View File

@ -83,6 +83,11 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaFlagsPutResolver())
.prepare()
fun updateFlags(mangas: List<Manga>) = db.put()
.objects(mangas)
.withPutResolver(MangaFlagsPutResolver(true))
.prepare()
fun updateLastUpdated(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaLastUpdatedPutResolver())

View File

@ -49,9 +49,8 @@ fun getRecentsQuery() =
* The max_last_read table contains the most recent chapters grouped by manga
* The select statement returns all information of chapters that have the same id as the chapter in max_last_read
* and are read after the given time period
* @return return limit is 25
*/
fun getRecentMangasQuery() =
fun getRecentMangasQuery(search: String = "") =
"""
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
FROM ${Manga.TABLE}
@ -65,9 +64,11 @@ fun getRecentMangasQuery() =
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ?
AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
AND lower(${Manga.TABLE}.${Manga.COL_TITLE}) LIKE '%$search%'
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
LIMIT 25
LIMIT ? OFFSET ?
"""
fun getHistoryByMangaId() =

View File

@ -9,7 +9,7 @@ import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaFlagsPutResolver : PutResolver<Manga>() {
class MangaFlagsPutResolver(private val updateAll: Boolean = false) : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
@ -19,11 +19,21 @@ class MangaFlagsPutResolver : PutResolver<Manga>() {
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToUpdateQuery(manga: Manga): UpdateQuery {
val builder = UpdateQuery.builder()
return if (updateAll) {
builder
.table(MangaTable.TABLE)
.build()
} else {
builder
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
}
}
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_CHAPTER_FLAGS, manga.chapter_flags)

View File

@ -7,10 +7,10 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.SourceManager
import java.util.concurrent.TimeUnit
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
/**
* Cache where we dump the downloads directory from the filesystem. This class is needed because
@ -128,7 +128,7 @@ class DownloadCache(
.orEmpty()
.associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry ->
onlineSources.find { provider.getSourceDirName(it).toLowerCase() == entry.key?.toLowerCase() }?.id
onlineSources.find { provider.getSourceDirName(it).equals(entry.key, ignoreCase = true) }?.id
}
rootDir.files = sourceDirs

View File

@ -8,6 +8,7 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page
@ -24,10 +25,8 @@ import uy.kohesive.injekt.injectLazy
*/
class DownloadManager(private val context: Context) {
/**
* The sources manager.
*/
private val sourceManager by injectLazy<SourceManager>()
private val sourceManager: SourceManager by injectLazy()
private val preferences: PreferencesHelper by injectLazy()
/**
* Downloads provider, used to retrieve the folders where the chapters are or should be stored.
@ -174,6 +173,17 @@ class DownloadManager(private val context: Context) {
return cache.isChapterDownloaded(chapter, manga, skipCache)
}
/**
* Returns the download from queue if the chapter is queued for download
* else it will return null which means that the chapter is not queued for download
*
* @param chapter the chapter to check.
*/
fun getChapterDownloadOrNull(chapter: Chapter): Download? {
return downloader.queue
.firstOrNull { it.chapter.id == chapter.id && it.chapter.manga_id == chapter.manga_id }
}
/**
* Returns the amount of downloaded chapters for a manga.
*
@ -199,14 +209,19 @@ class DownloadManager(private val context: Context) {
* @param manga the manga of the chapters.
* @param source the source of the chapters.
*/
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source) {
queue.remove(chapters)
val chapterDirs = provider.findChapterDirs(chapters, manga, source)
fun deleteChapters(chapters: List<Chapter>, manga: Manga, source: Source): List<Chapter> {
val filteredChapters = getChaptersToDelete(chapters)
queue.remove(filteredChapters)
val chapterDirs = provider.findChapterDirs(filteredChapters, manga, source)
chapterDirs.forEach { it.delete() }
cache.removeChapters(chapters, manga)
cache.removeChapters(filteredChapters, manga)
if (cache.getDownloadCount(manga) == 0) { // Delete manga directory if empty
chapterDirs.firstOrNull()?.parentFile?.delete()
}
return filteredChapters
}
/**
@ -228,7 +243,7 @@ class DownloadManager(private val context: Context) {
* @param manga the manga of the chapters.
*/
fun enqueueDeleteChapters(chapters: List<Chapter>, manga: Manga) {
pendingDeleter.addChapters(chapters, manga)
pendingDeleter.addChapters(getChaptersToDelete(chapters), manga)
}
/**
@ -267,4 +282,12 @@ class DownloadManager(private val context: Context) {
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
}
}
private fun getChaptersToDelete(chapters: List<Chapter>): List<Chapter> {
return if (!preferences.removeBookmarkedChapters()) {
chapters.filterNot { it.bookmark }
} else {
chapters
}
}
}

View File

@ -12,9 +12,8 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager
import java.util.regex.Pattern
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.util.regex.Pattern
/**
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
@ -25,12 +24,22 @@ internal class DownloadNotifier(private val context: Context) {
private val preferences: PreferencesHelper by injectLazy()
private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
private val progressNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
}
}
private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
setAutoCancel(false)
private val completeNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
setAutoCancel(false)
}
}
private val errorNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_ERROR) {
setAutoCancel(false)
}
}
/**
@ -53,7 +62,7 @@ internal class DownloadNotifier(private val context: Context) {
*
* @param id the id of the notification.
*/
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) {
private fun NotificationCompat.Builder.show(id: Int) {
context.notificationManager.notify(id, build())
}
@ -70,8 +79,8 @@ internal class DownloadNotifier(private val context: Context) {
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
* those can only be dismissed by the user.
*/
fun dismiss() {
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
fun dismissProgress() {
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
}
/**
@ -98,7 +107,9 @@ internal class DownloadNotifier(private val context: Context) {
}
val downloadingProgressText = context.getString(
R.string.chapter_downloading_progress, download.downloadedImages, download.pages!!.size
R.string.chapter_downloading_progress,
download.downloadedImages,
download.pages!!.size
)
if (preferences.hideNotificationContent()) {
@ -112,14 +123,15 @@ internal class DownloadNotifier(private val context: Context) {
}
setProgress(download.pages!!.size, download.downloadedImages, false)
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
}
progressNotificationBuilder.show()
}
/**
* Show notification when download is paused.
*/
fun onDownloadPaused() {
fun onPaused() {
with(progressNotificationBuilder) {
setContentTitle(context.getString(R.string.chapter_paused))
setContentText(context.getString(R.string.download_notifier_download_paused))
@ -141,8 +153,9 @@ internal class DownloadNotifier(private val context: Context) {
context.getString(R.string.action_cancel_all),
NotificationReceiver.clearDownloadsPendingBroadcast(context)
)
show(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS)
}
progressNotificationBuilder.show()
// Reset initial values
isDownloading = false
@ -151,18 +164,21 @@ internal class DownloadNotifier(private val context: Context) {
/**
* This function shows a notification to inform download tasks are done.
*/
fun downloadFinished() {
// Create notification
with(completeNotificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(context.getString(R.string.download_notifier_download_finish))
setSmallIcon(android.R.drawable.stat_sys_download_done)
clearActions()
setAutoCancel(true)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
fun onComplete() {
if (!errorThrown) {
// Create notification
with(completeNotificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(context.getString(R.string.download_notifier_download_finish))
setSmallIcon(android.R.drawable.stat_sys_download_done)
clearActions()
setAutoCancel(true)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
}
}
completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
// Reset states to default
errorThrown = false
@ -175,7 +191,7 @@ internal class DownloadNotifier(private val context: Context) {
* @param reason the text to show.
*/
fun onWarning(reason: String) {
with(completeNotificationBuilder) {
with(errorNotificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(reason)
setSmallIcon(android.R.drawable.stat_sys_warning)
@ -183,8 +199,9 @@ internal class DownloadNotifier(private val context: Context) {
clearActions()
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
}
completeNotificationBuilder.show()
// Reset download information
isDownloading = false
@ -199,7 +216,7 @@ internal class DownloadNotifier(private val context: Context) {
*/
fun onError(error: String? = null, chapter: String? = null) {
// Create notification
with(completeNotificationBuilder) {
with(errorNotificationBuilder) {
setContentTitle(
chapter
?: context.getString(R.string.download_notifier_downloader_title)
@ -210,8 +227,9 @@ internal class DownloadNotifier(private val context: Context) {
setAutoCancel(false)
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
}
completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
// Reset download information
errorThrown = true

View File

@ -2,10 +2,12 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import androidx.core.content.edit
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy
/**
@ -15,10 +17,7 @@ import uy.kohesive.injekt.injectLazy
*/
class DownloadPendingDeleter(context: Context) {
/**
* Gson instance to encode and decode chapters.
*/
private val gson by injectLazy<Gson>()
private val json: Json by injectLazy()
/**
* Preferences used to store the list of chapters to delete.
@ -53,7 +52,7 @@ class DownloadPendingDeleter(context: Context) {
val existingEntry = preferences.getString(manga.id!!.toString(), null)
if (existingEntry != null) {
// Existing entry found on preferences, decode json and add the new chapter
val savedEntry = gson.fromJson<Entry>(existingEntry)
val savedEntry = json.decodeFromString<Entry>(existingEntry)
// Append new chapters
val newChapters = savedEntry.chapters.addUniqueById(chapters)
@ -69,7 +68,7 @@ class DownloadPendingDeleter(context: Context) {
}
// Save current state
val json = gson.toJson(newEntry)
val json = json.encodeToString(newEntry)
preferences.edit {
putString(newEntry.manga.id.toString(), json)
}
@ -90,8 +89,8 @@ class DownloadPendingDeleter(context: Context) {
}
lastAddedEntry = null
return entries.associate { entry ->
entry.manga.toModel() to entry.chapters.map { it.toModel() }
return entries.associate { (chapters, manga) ->
manga.toModel() to chapters.map { it.toModel() }
}
}
@ -101,7 +100,7 @@ class DownloadPendingDeleter(context: Context) {
private fun decodeAll(): List<Entry> {
return preferences.all.values.mapNotNull { rawEntry ->
try {
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
(rawEntry as? String)?.let { json.decodeFromString<Entry>(it) }
} catch (e: Exception) {
null
}
@ -124,6 +123,7 @@ class DownloadPendingDeleter(context: Context) {
/**
* Class used to save an entry of chapters with their manga into preferences.
*/
@Serializable
private data class Entry(
val chapters: List<ChapterEntry>,
val manga: MangaEntry
@ -132,16 +132,18 @@ class DownloadPendingDeleter(context: Context) {
/**
* Class used to save an entry for a chapter into preferences.
*/
@Serializable
private data class ChapterEntry(
val id: Long,
val url: String,
val name: String,
val scanlator: String?
val scanlator: String? = null
)
/**
* Class used to save an entry for a manga into preferences.
*/
@Serializable
private data class MangaEntry(
val id: Long,
val url: String,

View File

@ -7,9 +7,9 @@ import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkInfo.State.CONNECTED
import android.net.NetworkInfo.State.DISCONNECTED
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.content.ContextCompat
import com.github.pwittchen.reactivenetwork.library.Connectivity
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import com.jakewharton.rxrelay.BehaviorRelay
@ -47,11 +47,7 @@ class DownloadService : Service() {
*/
fun start(context: Context) {
val intent = Intent(context, DownloadService::class.java)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
ContextCompat.startForegroundService(context, intent)
}
/**
@ -83,7 +79,7 @@ class DownloadService : Service() {
*/
override fun onCreate() {
super.onCreate()
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
startForeground(Notifications.ID_DOWNLOAD_CHAPTER_PROGRESS, getPlaceholderNotification())
wakeLock = acquireWakeLock(javaClass.name)
runningRelay.call(true)
subscriptions = CompositeSubscription()

View File

@ -2,12 +2,15 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import androidx.core.content.edit
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import kotlinx.serialization.Serializable
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import uy.kohesive.injekt.injectLazy
/**
@ -25,11 +28,7 @@ class DownloadStore(
*/
private val preferences = context.getSharedPreferences("active_downloads", Context.MODE_PRIVATE)
/**
* Gson instance to serialize/deserialize downloads.
*/
private val gson: Gson by injectLazy()
private val json: Json by injectLazy()
private val db: DatabaseHelper by injectLazy()
/**
@ -111,7 +110,7 @@ class DownloadStore(
*/
private fun serialize(download: Download): String {
val obj = DownloadObject(download.manga.id!!, download.chapter.id!!, counter++)
return gson.toJson(obj)
return json.encodeToString(obj)
}
/**
@ -121,7 +120,7 @@ class DownloadStore(
*/
private fun deserialize(string: String): DownloadObject? {
return try {
gson.fromJson(string, DownloadObject::class.java)
json.decodeFromString<DownloadObject>(string)
} catch (e: Exception) {
null
}
@ -134,5 +133,6 @@ class DownloadStore(
* @param chapterId the id of the chapter.
* @param order the order of the download in the queue.
*/
@Serializable
data class DownloadObject(val mangaId: Long, val chapterId: Long, val order: Int)
}

View File

@ -22,7 +22,6 @@ import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.ImageUtil
import java.io.File
import kotlinx.coroutines.async
import okhttp3.Response
import rx.Observable
@ -31,6 +30,7 @@ import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File
/**
* This class is the one in charge of downloading chapters.
@ -137,9 +137,10 @@ class Downloader(
} else {
if (notifier.paused) {
notifier.paused = false
notifier.onDownloadPaused()
notifier.onPaused()
} else {
notifier.downloadFinished()
notifier.dismissProgress()
notifier.onComplete()
}
}
}
@ -170,7 +171,7 @@ class Downloader(
.forEach { it.status = Download.NOT_DOWNLOADED }
}
queue.clear()
notifier.dismiss()
notifier.dismissProgress()
}
/**
@ -266,15 +267,16 @@ class Downloader(
* @param download the chapter to be downloaded.
*/
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
val chapterDirname = provider.getChapterDirName(download.chapter)
val mangaDir = provider.getMangaDir(download.manga, download.source)
if (DiskUtil.getAvailableStorageSpace(mangaDir) < MIN_DISK_SPACE) {
val availSpace = DiskUtil.getAvailableStorageSpace(mangaDir)
if (availSpace != -1L && availSpace < MIN_DISK_SPACE) {
download.status = Download.ERROR
notifier.onError(context.getString(R.string.download_insufficient_space), download.chapter.name)
return@defer Observable.just(download)
}
val chapterDirname = provider.getChapterDirName(download.chapter)
val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
val pageListObservable = if (download.pages == null) {

View File

@ -5,9 +5,9 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadStore
import eu.kanade.tachiyomi.source.model.Page
import java.util.concurrent.CopyOnWriteArrayList
import rx.Observable
import rx.subjects.PublishSubject
import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue(
private val store: DownloadStore,

View File

@ -5,12 +5,12 @@ import android.util.Log
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
import timber.log.Timber
import java.io.File
import java.io.FileInputStream
import java.io.FileNotFoundException
import java.io.IOException
import java.io.InputStream
import timber.log.Timber
open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {

View File

@ -14,10 +14,10 @@ import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.isLocal
import java.io.InputStream
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.InputStream
/**
* A class for loading a cover associated with a [Manga] that can be present in our own cache.

View File

@ -14,9 +14,9 @@ import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import eu.kanade.tachiyomi.network.NetworkHelper
import java.io.InputStream
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream
/**
* Class used to update Glide module settings

View File

@ -9,9 +9,9 @@ import androidx.work.WorkManager
import androidx.work.Worker
import androidx.work.WorkerParameters
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import java.util.concurrent.TimeUnit
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
class LibraryUpdateJob(private val context: Context, workerParams: WorkerParameters) :
Worker(context, workerParams) {
@ -45,8 +45,10 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
.build()
val request = PeriodicWorkRequestBuilder<LibraryUpdateJob>(
interval.toLong(), TimeUnit.HOURS,
10, TimeUnit.MINUTES
interval.toLong(),
TimeUnit.HOURS,
10,
TimeUnit.MINUTES
)
.addTag(TAG)
.setConstraints(constraints)

View File

@ -22,9 +22,9 @@ import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.notification
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager
import uy.kohesive.injekt.injectLazy
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import uy.kohesive.injekt.injectLazy
class LibraryUpdateNotifier(private val context: Context) {
@ -109,7 +109,7 @@ class LibraryUpdateNotifier(private val context: Context) {
setContentIntent(errorLogIntent)
addAction(
R.drawable.nnf_ic_file_folder,
R.drawable.ic_folder_24dp,
context.getString(R.string.action_open_log),
errorLogIntent
)
@ -165,8 +165,7 @@ class LibraryUpdateNotifier(private val context: Context) {
// Per-manga notification
if (!preferences.hideNotificationContent()) {
updates.forEach {
val (manga, chapters) = it
updates.forEach { (manga, chapters) ->
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
}
}
@ -198,18 +197,23 @@ class LibraryUpdateNotifier(private val context: Context) {
// Mark chapters as read action
addAction(
R.drawable.ic_glasses_black_24dp, context.getString(R.string.action_mark_as_read),
R.drawable.ic_glasses_black_24dp,
context.getString(R.string.action_mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast(
context,
manga, chapters, Notifications.ID_NEW_CHAPTERS
manga,
chapters,
Notifications.ID_NEW_CHAPTERS
)
)
// View chapters action
addAction(
R.drawable.ic_book_24dp, context.getString(R.string.action_view_chapters),
R.drawable.ic_book_24dp,
context.getString(R.string.action_view_chapters),
NotificationReceiver.openChapterPendingActivity(
context,
manga, Notifications.ID_NEW_CHAPTERS
manga,
Notifications.ID_NEW_CHAPTERS
)
)
}

View File

@ -3,9 +3,10 @@ package eu.kanade.tachiyomi.data.library
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import androidx.core.content.ContextCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
@ -21,20 +22,21 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
import rx.Observable
import rx.Subscription
import rx.schedulers.Schedulers
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.util.concurrent.atomic.AtomicInteger
/**
* This class will take care of updating the chapters of the manga from the library. It can be
@ -70,7 +72,9 @@ class LibraryUpdateService(
*/
enum class Target {
CHAPTERS, // Manga chapters
COVERS, // Manga covers
TRACKING // Tracking metadata
}
@ -111,11 +115,7 @@ class LibraryUpdateService(
putExtra(KEY_TARGET, target)
category?.let { putExtra(KEY_CATEGORY, it.id) }
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
ContextCompat.startForegroundService(context, intent)
return true
}
@ -268,11 +268,16 @@ class LibraryUpdateService(
updateManga(manga)
// If there's any error, return empty update and continue.
.onErrorReturn {
failedUpdates.add(Pair(manga, it.message))
val errorMessage = if (it is NoChaptersException) {
getString(R.string.no_chapters_error)
} else {
it.message
}
failedUpdates.add(Pair(manga, errorMessage))
Pair(emptyList(), emptyList())
}
// Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first.isNotEmpty() }
.filter { (first) -> first.isNotEmpty() }
.doOnNext {
if (manga.shouldDownloadNewChapters(db, preferences)) {
downloadChapters(manga, it.first)
@ -314,7 +319,7 @@ class LibraryUpdateService(
)
}
}
.map { manga -> manga.first }
.map { (first) -> first }
}
private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {

View File

@ -7,7 +7,6 @@ import android.content.Intent
import android.net.Uri
import android.os.Build
import android.os.Handler
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.BackupRestoreService
import eu.kanade.tachiyomi.data.database.DatabaseHelper
@ -26,10 +25,11 @@ import eu.kanade.tachiyomi.util.storage.DiskUtil
import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.system.notificationManager
import eu.kanade.tachiyomi.util.system.toast
import java.io.File
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/**
* Global [BroadcastReceiver] that runs on UI thread
@ -56,19 +56,22 @@ class NotificationReceiver : BroadcastReceiver() {
// Launch share activity and dismiss notification
ACTION_SHARE_IMAGE ->
shareImage(
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
context,
intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
// Delete image from path and dismiss notification
ACTION_DELETE_IMAGE ->
deleteImage(
context, intent.getStringExtra(EXTRA_FILE_LOCATION),
context,
intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
// Share backup file
ACTION_SHARE_BACKUP ->
shareBackup(
context, intent.getParcelableExtra(EXTRA_URI),
context,
intent.getParcelableExtra(EXTRA_URI),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1)
)
ACTION_CANCEL_RESTORE -> cancelRestore(
@ -80,7 +83,8 @@ class NotificationReceiver : BroadcastReceiver() {
// Open reader activity
ACTION_OPEN_CHAPTER -> {
openChapter(
context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
context,
intent.getLongExtra(EXTRA_MANGA_ID, -1),
intent.getLongExtra(EXTRA_CHAPTER_ID, -1)
)
}
@ -155,7 +159,7 @@ class NotificationReceiver : BroadcastReceiver() {
* @param mangaId id of manga
* @param chapterId id of chapter
*/
internal fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
private fun openChapter(context: Context, mangaId: Long, chapterId: Long) {
val db = DatabaseHelper(context)
val manga = db.getManga(mangaId).executeAsBlocking()
val chapter = db.getChapter(chapterId).executeAsBlocking()

View File

@ -32,10 +32,11 @@ object Notifications {
*/
private const val GROUP_DOWNLOADER = "group_downloader"
const val CHANNEL_DOWNLOADER_PROGRESS = "downloader_progress_channel"
const val ID_DOWNLOAD_CHAPTER = -201
const val ID_DOWNLOAD_CHAPTER_PROGRESS = -201
const val CHANNEL_DOWNLOADER_COMPLETE = "downloader_complete_channel"
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
const val CHANNEL_DOWNLOADER_ERROR = "downloader_error_channel"
const val ID_DOWNLOAD_CHAPTER_ERROR = -202
/**
* Notification channel and ids used by the library updater.
@ -81,46 +82,62 @@ object Notifications {
listOf(
NotificationChannel(
CHANNEL_COMMON, context.getString(R.string.channel_common),
CHANNEL_COMMON,
context.getString(R.string.channel_common),
NotificationManager.IMPORTANCE_LOW
),
NotificationChannel(
CHANNEL_LIBRARY, context.getString(R.string.channel_library),
CHANNEL_LIBRARY,
context.getString(R.string.channel_library),
NotificationManager.IMPORTANCE_LOW
).apply {
setShowBadge(false)
},
NotificationChannel(
CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress),
CHANNEL_DOWNLOADER_PROGRESS,
context.getString(R.string.channel_progress),
NotificationManager.IMPORTANCE_LOW
).apply {
group = GROUP_DOWNLOADER
setShowBadge(false)
},
NotificationChannel(
CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete),
CHANNEL_DOWNLOADER_COMPLETE,
context.getString(R.string.channel_complete),
NotificationManager.IMPORTANCE_LOW
).apply {
group = GROUP_DOWNLOADER
setShowBadge(false)
},
NotificationChannel(
CHANNEL_NEW_CHAPTERS, context.getString(R.string.channel_new_chapters),
CHANNEL_DOWNLOADER_ERROR,
context.getString(R.string.channel_errors),
NotificationManager.IMPORTANCE_LOW
).apply {
group = GROUP_DOWNLOADER
setShowBadge(false)
},
NotificationChannel(
CHANNEL_NEW_CHAPTERS,
context.getString(R.string.channel_new_chapters),
NotificationManager.IMPORTANCE_DEFAULT
),
NotificationChannel(
CHANNEL_UPDATES_TO_EXTS, context.getString(R.string.channel_ext_updates),
CHANNEL_UPDATES_TO_EXTS,
context.getString(R.string.channel_ext_updates),
NotificationManager.IMPORTANCE_DEFAULT
),
NotificationChannel(
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_progress),
CHANNEL_BACKUP_RESTORE_PROGRESS,
context.getString(R.string.channel_progress),
NotificationManager.IMPORTANCE_LOW
).apply {
group = GROUP_BACKUP_RESTORE
setShowBadge(false)
},
NotificationChannel(
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_complete),
CHANNEL_BACKUP_RESTORE_COMPLETE,
context.getString(R.string.channel_complete),
NotificationManager.IMPORTANCE_HIGH
).apply {
group = GROUP_BACKUP_RESTORE

View File

@ -13,6 +13,8 @@ object PreferenceKeys {
const val confirmExit = "pref_confirm_exit"
const val hideBottomBar = "pref_hide_bottom_bar_on_scroll"
const val rotation = "pref_rotation_type_key"
const val enableTransitions = "pref_enable_transitions_key"
@ -97,6 +99,8 @@ object PreferenceKeys {
const val removeAfterMarkedAsRead = "pref_remove_after_marked_as_read_key"
const val removeBookmarkedChapters = "pref_remove_bookmarked"
const val libraryUpdateInterval = "pref_library_update_interval_key"
const val libraryUpdateRestriction = "library_update_restriction"
@ -107,16 +111,20 @@ object PreferenceKeys {
const val downloadedOnly = "pref_downloaded_only"
const val filterDownloaded = "pref_filter_downloaded_key"
const val filterDownloaded = "pref_filter_library_downloaded"
const val filterUnread = "pref_filter_unread_key"
const val filterUnread = "pref_filter_library_unread"
const val filterCompleted = "pref_filter_completed_key"
const val filterCompleted = "pref_filter_library_completed"
const val librarySortingMode = "library_sorting_mode"
const val automaticExtUpdates = "automatic_ext_updates"
const val showNsfwSource = "show_nsfw_source"
const val showNsfwExtension = "show_nsfw_extension"
const val labelNsfwExtension = "label_nsfw_extension"
const val startScreen = "start_screen"
const val useBiometricLock = "use_biometric_lock"
@ -161,6 +169,22 @@ object PreferenceKeys {
const val enableDoh = "enable_doh"
const val defaultChapterFilterByRead = "default_chapter_filter_by_read"
const val defaultChapterFilterByDownloaded = "default_chapter_filter_by_downloaded"
const val defaultChapterFilterByBookmarked = "default_chapter_filter_by_bookmarked"
const val defaultChapterSortBySourceOrNumber = "default_chapter_sort_by_source_or_number" // and upload date
const val defaultChapterSortByAscendingOrDescending = "default_chapter_sort_by_ascending_or_descending"
const val defaultChapterDisplayByNameOrNumber = "default_chapter_display_by_name_or_number"
const val incognitoMode = "incognito_mode"
const val createLegacyBackup = "create_legacy_backup"
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"

View File

@ -2,23 +2,26 @@ package eu.kanade.tachiyomi.data.preference
import android.content.Context
import android.os.Environment
import androidx.core.content.edit
import androidx.core.net.toUri
import androidx.preference.PreferenceManager
import com.tfcporciuncula.flow.FlowSharedPreferences
import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.widget.ExtendedNavigationView
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import java.io.File
import java.text.DateFormat
import java.text.SimpleDateFormat
import java.util.Locale
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
@OptIn(ExperimentalCoroutinesApi::class)
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
@ -57,6 +60,8 @@ class PreferencesHelper(val context: Context) {
fun confirmExit() = prefs.getBoolean(Keys.confirmExit, false)
fun hideBottomBar() = flowPrefs.getBoolean(Keys.hideBottomBar, true)
fun useBiometricLock() = flowPrefs.getBoolean(Keys.useBiometricLock, false)
fun lockAppAfter() = flowPrefs.getInt(Keys.lockAppAfter, 0)
@ -71,7 +76,7 @@ class PreferencesHelper(val context: Context) {
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
fun clear() = prefs.edit().clear().apply()
fun clear() = prefs.edit { clear() }
fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
@ -158,10 +163,10 @@ class PreferencesHelper(val context: Context) {
fun trackPassword(sync: TrackService) = prefs.getString(Keys.trackPassword(sync.id), "")
fun setTrackCredentials(sync: TrackService, username: String, password: String) {
prefs.edit()
.putString(Keys.trackUsername(sync.id), username)
.putString(Keys.trackPassword(sync.id), password)
.apply()
prefs.edit {
putString(Keys.trackUsername(sync.id), username)
putString(Keys.trackPassword(sync.id), password)
}
}
fun trackToken(sync: TrackService) = flowPrefs.getString(Keys.trackToken(sync.id), "")
@ -187,6 +192,8 @@ class PreferencesHelper(val context: Context) {
fun removeAfterMarkedAsRead() = prefs.getBoolean(Keys.removeAfterMarkedAsRead, false)
fun removeBookmarkedChapters() = prefs.getBoolean(Keys.removeBookmarkedChapters, false)
fun libraryUpdateInterval() = flowPrefs.getInt(Keys.libraryUpdateInterval, 24)
fun libraryUpdateRestriction() = prefs.getStringSet(Keys.libraryUpdateRestriction, setOf("wifi"))
@ -205,11 +212,11 @@ class PreferencesHelper(val context: Context) {
fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true)
fun filterDownloaded() = flowPrefs.getBoolean(Keys.filterDownloaded, false)
fun filterDownloaded() = flowPrefs.getInt(Keys.filterDownloaded, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
fun filterUnread() = flowPrefs.getBoolean(Keys.filterUnread, false)
fun filterUnread() = flowPrefs.getInt(Keys.filterUnread, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
fun filterCompleted() = flowPrefs.getBoolean(Keys.filterCompleted, false)
fun filterCompleted() = flowPrefs.getInt(Keys.filterCompleted, ExtendedNavigationView.Item.TriStateGroup.State.IGNORE.value)
fun librarySortingMode() = flowPrefs.getInt(Keys.librarySortingMode, 0)
@ -217,6 +224,10 @@ class PreferencesHelper(val context: Context) {
fun automaticExtUpdates() = flowPrefs.getBoolean(Keys.automaticExtUpdates, true)
fun showNsfwSource() = flowPrefs.getBoolean(Keys.showNsfwSource, true)
fun showNsfwExtension() = flowPrefs.getBoolean(Keys.showNsfwExtension, true)
fun labelNsfwExtension() = prefs.getBoolean(Keys.labelNsfwExtension, true)
fun extensionUpdatesCount() = flowPrefs.getInt("ext_updates_count", 0)
fun lastExtCheck() = flowPrefs.getLong("last_ext_check", 0)
@ -244,4 +255,33 @@ class PreferencesHelper(val context: Context) {
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false)
fun lastSearchQuerySearchSettings() = flowPrefs.getString("last_search_query", "")
fun filterChapterByRead() = prefs.getInt(Keys.defaultChapterFilterByRead, Manga.SHOW_ALL)
fun filterChapterByDownloaded() = prefs.getInt(Keys.defaultChapterFilterByDownloaded, Manga.SHOW_ALL)
fun filterChapterByBookmarked() = prefs.getInt(Keys.defaultChapterFilterByBookmarked, Manga.SHOW_ALL)
fun sortChapterBySourceOrNumber() = prefs.getInt(Keys.defaultChapterSortBySourceOrNumber, Manga.SORTING_SOURCE)
fun displayChapterByNameOrNumber() = prefs.getInt(Keys.defaultChapterDisplayByNameOrNumber, Manga.DISPLAY_NAME)
fun sortChapterByAscendingOrDescending() = prefs.getInt(Keys.defaultChapterSortByAscendingOrDescending, Manga.SORT_DESC)
fun incognitoMode() = flowPrefs.getBoolean(Keys.incognitoMode, false)
fun createLegacyBackup() = flowPrefs.getBoolean(Keys.createLegacyBackup, true)
fun setChapterSettingsDefault(manga: Manga) {
prefs.edit {
putInt(Keys.defaultChapterFilterByRead, manga.readFilter)
putInt(Keys.defaultChapterFilterByDownloaded, manga.downloadedFilter)
putInt(Keys.defaultChapterFilterByBookmarked, manga.bookmarkedFilter)
putInt(Keys.defaultChapterSortBySourceOrNumber, manga.sorting)
putInt(Keys.defaultChapterDisplayByNameOrNumber, manga.displayMode)
putInt(Keys.defaultChapterSortByAscendingOrDescending, if (manga.sortDescending()) Manga.SORT_DESC else Manga.SORT_ASC)
}
}
}

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.preference
import android.content.SharedPreferences
import androidx.core.content.edit
import androidx.preference.PreferenceDataStore
class SharedPreferencesDataStore(private val prefs: SharedPreferences) : PreferenceDataStore() {
@ -10,7 +11,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
}
override fun putBoolean(key: String?, value: Boolean) {
prefs.edit().putBoolean(key, value).apply()
prefs.edit {
putBoolean(key, value)
}
}
override fun getInt(key: String?, defValue: Int): Int {
@ -18,7 +21,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
}
override fun putInt(key: String?, value: Int) {
prefs.edit().putInt(key, value).apply()
prefs.edit {
putInt(key, value)
}
}
override fun getLong(key: String?, defValue: Long): Long {
@ -26,7 +31,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
}
override fun putLong(key: String?, value: Long) {
prefs.edit().putLong(key, value).apply()
prefs.edit {
putLong(key, value)
}
}
override fun getFloat(key: String?, defValue: Float): Float {
@ -34,7 +41,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
}
override fun putFloat(key: String?, value: Float) {
prefs.edit().putFloat(key, value).apply()
prefs.edit {
putFloat(key, value)
}
}
override fun getString(key: String?, defValue: String?): String? {
@ -42,7 +51,9 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
}
override fun putString(key: String?, value: String?) {
prefs.edit().putString(key, value).apply()
prefs.edit {
putString(key, value)
}
}
override fun getStringSet(key: String?, defValues: MutableSet<String>?): MutableSet<String>? {
@ -50,6 +61,8 @@ class SharedPreferencesDataStore(private val prefs: SharedPreferences) : Prefere
}
override fun putStringSet(key: String?, values: MutableSet<String>?) {
prefs.edit().putStringSet(key, values).apply()
prefs.edit {
putStringSet(key, values)
}
}
}

View File

@ -2,11 +2,13 @@ package eu.kanade.tachiyomi.data.track.anilist
import android.content.Context
import android.graphics.Color
import com.google.gson.Gson
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
@ -33,7 +35,7 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
override val name = "AniList"
private val gson: Gson by injectLazy()
private val json: Json by injectLazy()
private val interceptor by lazy { AnilistInterceptor(this, getPassword()) }
@ -197,12 +199,12 @@ class Anilist(private val context: Context, id: Int) : TrackService(id) {
}
fun saveOAuth(oAuth: OAuth?) {
preferences.trackToken(this).set(gson.toJson(oAuth))
preferences.trackToken(this).set(json.encodeToString(oAuth))
}
fun loadOAuth(): OAuth? {
return try {
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
} catch (e: Exception) {
null
}

View File

@ -2,26 +2,34 @@ package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri
import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.jsonObject
import com.github.salomonbrys.kotson.nullInt
import com.github.salomonbrys.kotson.nullString
import com.github.salomonbrys.kotson.obj
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.asObservableSuccess
import java.util.Calendar
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.buildJsonObject
import kotlinx.serialization.json.contentOrNull
import kotlinx.serialization.json.int
import kotlinx.serialization.json.intOrNull
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import kotlinx.serialization.json.long
import kotlinx.serialization.json.put
import kotlinx.serialization.json.putJsonObject
import okhttp3.MediaType.Companion.toMediaTypeOrNull
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.RequestBody.Companion.toRequestBody
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.util.Calendar
class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
private val json: Json by injectLazy()
private val jsonMime = "application/json; charset=utf-8".toMediaTypeOrNull()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
@ -35,15 +43,14 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val variables = jsonObject(
"mangaId" to track.media_id,
"progress" to track.last_chapter_read,
"status" to track.toAnilistStatus()
)
val payload = jsonObject(
"query" to query,
"variables" to variables
)
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("mangaId", track.media_id)
put("progress", track.last_chapter_read)
put("status", track.toAnilistStatus())
}
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
@ -57,8 +64,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = JsonParser.parseString(responseBody).obj
track.library_id = response["data"]["SaveMediaListEntry"]["id"].asLong
val response = json.decodeFromString<JsonObject>(responseBody)
track.library_id = response["data"]!!.jsonObject["SaveMediaListEntry"]!!.jsonObject["id"]!!.jsonPrimitive.long
track
}
}
@ -74,16 +81,15 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val variables = jsonObject(
"listId" to track.library_id,
"progress" to track.last_chapter_read,
"status" to track.toAnilistStatus(),
"score" to track.score.toInt()
)
val payload = jsonObject(
"query" to query,
"variables" to variables
)
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("listId", track.library_id)
put("progress", track.last_chapter_read)
put("status", track.toAnilistStatus())
put("score", track.score.toInt())
}
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
@ -122,13 +128,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val variables = jsonObject(
"query" to search
)
val payload = jsonObject(
"query" to query,
"variables" to variables
)
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("query", search)
}
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
@ -141,11 +146,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = JsonParser.parseString(responseBody).obj
val data = response["data"]!!.obj
val page = data["Page"].obj
val media = page["media"].array
val entries = media.map { jsonToALManga(it.obj) }
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val page = data["Page"]!!.jsonObject
val media = page["media"]!!.jsonArray
val entries = media.map { jsonToALManga(it.jsonObject) }
entries.map { it.toTrack() }
}
}
@ -182,14 +187,13 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val variables = jsonObject(
"id" to userid,
"manga_id" to track.media_id
)
val payload = jsonObject(
"query" to query,
"variables" to variables
)
val payload = buildJsonObject {
put("query", query)
putJsonObject("variables") {
put("id", userid)
put("manga_id", track.media_id)
}
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
@ -202,11 +206,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = JsonParser.parseString(responseBody).obj
val data = response["data"]!!.obj
val page = data["Page"].obj
val media = page["mediaList"].array
val entries = media.map { jsonToALUserManga(it.obj) }
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val page = data["Page"]!!.jsonObject
val media = page["mediaList"]!!.jsonArray
val entries = media.map { jsonToALUserManga(it.jsonObject) }
entries.firstOrNull()?.toTrack()
}
}
@ -232,9 +236,9 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|}
|}
|""".trimMargin()
val payload = jsonObject(
"query" to query
)
val payload = buildJsonObject {
put("query", query)
}
val body = payload.toString().toRequestBody(jsonMime)
val request = Request.Builder()
.url(apiUrl)
@ -247,10 +251,10 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
val response = JsonParser.parseString(responseBody).obj
val data = response["data"]!!.obj
val viewer = data["Viewer"].obj
Pair(viewer["id"].asInt, viewer["mediaListOptions"]["scoreFormat"].asString)
val response = json.decodeFromString<JsonObject>(responseBody)
val data = response["data"]!!.jsonObject
val viewer = data["Viewer"]!!.jsonObject
Pair(viewer["id"]!!.jsonPrimitive.int, viewer["mediaListOptions"]!!.jsonObject["scoreFormat"]!!.jsonPrimitive.content)
}
}
@ -258,12 +262,12 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
val date = try {
val date = Calendar.getInstance()
date.set(
struct["startDate"]["year"].nullInt ?: 0,
struct["startDate"]!!.jsonObject["year"]!!.jsonPrimitive.intOrNull ?: 0,
(
struct["startDate"]["month"].nullInt
struct["startDate"]!!.jsonObject["month"]!!.jsonPrimitive.intOrNull
?: 0
) - 1,
struct["startDate"]["day"].nullInt ?: 0
struct["startDate"]!!.jsonObject["day"]!!.jsonPrimitive.intOrNull ?: 0
)
date.timeInMillis
} catch (_: Exception) {
@ -271,14 +275,25 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
}
return ALManga(
struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].nullString.orEmpty(),
date, struct["chapters"].nullInt ?: 0
struct["id"]!!.jsonPrimitive.int,
struct["title"]!!.jsonObject["romaji"]!!.jsonPrimitive.content,
struct["coverImage"]!!.jsonObject["large"]!!.jsonPrimitive.content,
struct["description"]!!.jsonPrimitive.contentOrNull,
struct["type"]!!.jsonPrimitive.content,
struct["status"]!!.jsonPrimitive.contentOrNull ?: "",
date,
struct["chapters"]!!.jsonPrimitive.intOrNull ?: 0
)
}
private fun jsonToALUserManga(struct: JsonObject): ALUserManga {
return ALUserManga(struct["id"].asLong, struct["status"].asString, struct["scoreRaw"].asInt, struct["progress"].asInt, jsonToALManga(struct["media"].obj))
return ALUserManga(
struct["id"]!!.jsonPrimitive.long,
struct["status"]!!.jsonPrimitive.content,
struct["scoreRaw"]!!.jsonPrimitive.int,
struct["progress"]!!.jsonPrimitive.int,
jsonToALManga(struct["media"]!!.jsonObject)
)
}
companion object {

View File

@ -4,9 +4,9 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import uy.kohesive.injekt.injectLazy
import java.text.SimpleDateFormat
import java.util.Locale
import uy.kohesive.injekt.injectLazy
data class ALManga(
val media_id: Int,
@ -63,7 +63,7 @@ data class ALUserManga(
"DROPPED" -> Anilist.DROPPED
"PLANNING" -> Anilist.PLANNING
"REPEATING" -> Anilist.REPEATING
else -> throw NotImplementedError("Unknown status")
else -> throw NotImplementedError("Unknown status: $list_status")
}
}
@ -74,7 +74,7 @@ fun Track.toAnilistStatus() = when (status) {
Anilist.DROPPED -> "DROPPED"
Anilist.PLANNING -> "PLANNING"
Anilist.REPEATING -> "REPEATING"
else -> throw NotImplementedError("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}
private val preferences: PreferencesHelper by injectLazy()
@ -102,5 +102,5 @@ fun Track.toAnilistScore(): String = when (preferences.anilistScoreType().get())
}
// 10 point decimal
"POINT_10_DECIMAL" -> (score / 10).toString()
else -> throw Exception("Unknown score type")
else -> throw NotImplementedError("Unknown score type")
}

View File

@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.data.track.anilist
import kotlinx.serialization.Serializable
@Serializable
data class OAuth(
val access_token: String,
val token_type: String,

View File

@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.data.track.bangumi
import kotlinx.serialization.Serializable
@Serializable
data class Avatar(
val large: String? = "",
val medium: String? = "",

View File

@ -2,11 +2,13 @@ package eu.kanade.tachiyomi.data.track.bangumi
import android.content.Context
import android.graphics.Color
import com.google.gson.Gson
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
@ -15,9 +17,9 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
override val name = "Bangumi"
private val gson: Gson by injectLazy()
private val json: Json by injectLazy()
private val interceptor by lazy { BangumiInterceptor(this, gson) }
private val interceptor by lazy { BangumiInterceptor(this) }
private val api by lazy { BangumiApi(client, interceptor) }
@ -112,13 +114,12 @@ class Bangumi(private val context: Context, id: Int) : TrackService(id) {
}
fun saveToken(oauth: OAuth?) {
val json = gson.toJson(oauth)
preferences.trackToken(this).set(json)
preferences.trackToken(this).set(json.encodeToString(oauth))
}
fun restoreToken(): OAuth? {
return try {
gson.fromJson(preferences.trackToken(this).get(), OAuth::class.java)
json.decodeFromString<OAuth>(preferences.trackToken(this).get())
} catch (e: Exception) {
null
}

View File

@ -2,27 +2,31 @@ package eu.kanade.tachiyomi.data.track.bangumi
import android.net.Uri
import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.obj
import com.google.gson.Gson
import com.google.gson.JsonObject
import com.google.gson.JsonParser
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.network.POST
import eu.kanade.tachiyomi.network.asObservableSuccess
import java.net.URLEncoder
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonObject
import kotlinx.serialization.json.float
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive
import okhttp3.CacheControl
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.Request
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.net.URLEncoder
class BangumiApi(private val client: OkHttpClient, interceptor: BangumiInterceptor) {
private val gson: Gson by injectLazy()
private val json: Json by injectLazy()
private val authClient = client.newBuilder().addInterceptor(interceptor).build()
fun addLibManga(track: Track): Observable<Track> {
@ -92,36 +96,32 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
if (responseBody.contains("\"code\":404")) {
responseBody = "{\"results\":0,\"list\":[]}"
}
val response = JsonParser.parseString(responseBody).obj["list"]?.array
response?.filter { it.obj["type"].asInt == 1 }?.map { jsonToSearch(it.obj) }
val response = json.decodeFromString<JsonObject>(responseBody)["list"]?.jsonArray
response?.filter { it.jsonObject["type"]?.jsonPrimitive?.int == 1 }?.map { jsonToSearch(it.jsonObject) }
}
}
private fun jsonToSearch(obj: JsonObject): TrackSearch {
return TrackSearch.create(TrackManager.BANGUMI).apply {
media_id = obj["id"].asInt
title = obj["name_cn"].asString
cover_url = obj["images"].obj["common"].asString
summary = obj["name"].asString
tracking_url = obj["url"].asString
media_id = obj["id"]!!.jsonPrimitive.int
title = obj["name_cn"]!!.jsonPrimitive.content
cover_url = obj["images"]!!.jsonObject["common"]!!.jsonPrimitive.content
summary = obj["name"]!!.jsonPrimitive.content
tracking_url = obj["url"]!!.jsonPrimitive.content
}
}
private fun jsonToTrack(mangas: JsonObject): Track {
return Track.create(TrackManager.BANGUMI).apply {
title = mangas["name"].asString
media_id = mangas["id"].asInt
score = if (mangas["rating"] != null) {
if (mangas["rating"].isJsonObject) {
mangas["rating"].obj["score"].asFloat
} else {
0f
}
} else {
title = mangas["name"]!!.jsonPrimitive.content
media_id = mangas["id"]!!.jsonPrimitive.int
score = try {
mangas["rating"]!!.jsonObject["score"]!!.jsonPrimitive.float
} catch (_: Exception) {
0f
}
status = Bangumi.DEFAULT_STATUS
tracking_url = mangas["url"].asString
tracking_url = mangas["url"]!!.jsonPrimitive.content
}
}
@ -137,7 +137,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.map { netResponse ->
// get comic info
val responseBody = netResponse.body?.string().orEmpty()
jsonToTrack(JsonParser.parseString(responseBody).obj)
jsonToTrack(json.decodeFromString(responseBody))
}
}
@ -154,7 +154,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
.asObservableSuccess()
.map { netResponse ->
val resp = netResponse.body?.string()
val coll = gson.fromJson(resp, Collection::class.java)
val coll = json.decodeFromString<Collection>(resp!!)
track.status = coll.status?.id!!
track.last_chapter_read = coll.ep_status!!
track
@ -167,7 +167,7 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
if (responseBody.isEmpty()) {
throw Exception("Null Response")
}
gson.fromJson(responseBody, OAuth::class.java)
json.decodeFromString<OAuth>(responseBody)
}
}

View File

@ -1,23 +1,27 @@
package eu.kanade.tachiyomi.data.track.bangumi
import com.google.gson.Gson
import kotlinx.serialization.decodeFromString
import kotlinx.serialization.json.Json
import okhttp3.FormBody
import okhttp3.Interceptor
import okhttp3.Response
import uy.kohesive.injekt.injectLazy
class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
class BangumiInterceptor(val bangumi: Bangumi) : Interceptor {
private val json: Json by injectLazy()
/**
* OAuth object used for authenticated requests.
*/
private var oauth: OAuth? = bangumi.restoreToken()
fun addTocken(tocken: String, oidFormBody: FormBody): FormBody {
fun addToken(token: String, oidFormBody: FormBody): FormBody {
val newFormBody = FormBody.Builder()
for (i in 0 until oidFormBody.size) {
newFormBody.add(oidFormBody.name(i), oidFormBody.value(i))
}
newFormBody.add("access_token", tocken)
newFormBody.add("access_token", token)
return newFormBody.build()
}
@ -29,7 +33,7 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
if (currAuth.isExpired()) {
val response = chain.proceed(BangumiApi.refreshTokenRequest(currAuth.refresh_token!!))
if (response.isSuccessful) {
newAuth(gson.fromJson(response.body!!.string(), OAuth::class.java))
newAuth(json.decodeFromString<OAuth>(response.body!!.string()))
} else {
response.close()
}
@ -42,7 +46,7 @@ class BangumiInterceptor(val bangumi: Bangumi, val gson: Gson) : Interceptor {
.addQueryParameter("access_token", currAuth.access_token).build()
)
.build() else originalRequest.newBuilder()
.post(addTocken(currAuth.access_token, originalRequest.body as FormBody))
.post(addToken(currAuth.access_token, originalRequest.body as FormBody))
.header("User-Agent", "Tachiyomi")
.build()

View File

@ -8,7 +8,7 @@ fun Track.toBangumiStatus() = when (status) {
Bangumi.ON_HOLD -> "on_hold"
Bangumi.DROPPED -> "dropped"
Bangumi.PLANNING -> "wish"
else -> throw NotImplementedError("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}
fun toTrackStatus(status: String) = when (status) {
@ -17,6 +17,5 @@ fun toTrackStatus(status: String) = when (status) {
"on_hold" -> Bangumi.ON_HOLD
"dropped" -> Bangumi.DROPPED
"wish" -> Bangumi.PLANNING
else -> throw Exception("Unknown status")
else -> throw NotImplementedError("Unknown status: $status")
}

View File

@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.data.track.bangumi
import kotlinx.serialization.Serializable
@Serializable
data class Collection(
val `private`: Int? = 0,
val comment: String? = "",

View File

@ -1,9 +1,12 @@
package eu.kanade.tachiyomi.data.track.bangumi
import kotlinx.serialization.Serializable
@Serializable
data class OAuth(
val access_token: String,
val token_type: String,
val created_at: Long,
val created_at: Long = System.currentTimeMillis() / 1000,
val expires_in: Long,
val refresh_token: String?,
val user_id: Long?

View File

@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.data.track.bangumi
import kotlinx.serialization.Serializable
@Serializable
data class Status(
val id: Int? = 0,
val name: String? = "",

View File

@ -1,5 +1,8 @@
package eu.kanade.tachiyomi.data.track.bangumi
import kotlinx.serialization.Serializable
@Serializable
data class User(
val avatar: Avatar? = Avatar(),
val id: Int? = 0,

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