Compare commits

...

345 Commits

Author SHA1 Message Date
b8a98ef5e4 Release 0.10.0 2020-08-01 11:35:03 -04:00
c8fa90f473 Translated using Weblate (French) (#3528)
Currently translated at 99.8% (563 of 564 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.7% (557 of 564 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Japanese)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Marathi)

Currently translated at 41.4% (234 of 564 strings)

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

Translated using Weblate (Swedish)

Currently translated at 100.0% (564 of 564 strings)

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

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 (Spanish)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.8% (552 of 564 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (564 of 564 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Malay)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (564 of 564 strings)

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

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 (Spanish)

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (564 of 564 strings)

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

Translated using Weblate (Filipino)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Czech)

Currently translated at 63.9% (360 of 563 strings)

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

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2020-08-01 10:54:38 -04:00
b47ee8857b Downgrade coroutines and flow-preferences 2020-07-31 15:31:42 -04:00
131dfa62c4 Temporarily revert to stable version of androidx.biometric (closes #3425) 2020-07-31 15:01:28 -04:00
6a5af438dd Temporarily unrevert crop borders unification (closes #3487)
Reverts 1920568057
2020-07-31 14:59:34 -04:00
ccc0a61158 Shift WebView checks to necessary places only to allow for basic usage 2020-07-31 14:27:35 -04:00
e990ad25eb Fix tap region for manga summary 2020-07-31 12:58:13 -04:00
98a4d1e763 Actually fix library search properly 2020-07-31 12:54:23 -04:00
f762598c5c Fix library search query being lost when returning (closes #3473) 2020-07-31 12:18:47 -04:00
ec56c27071 More core-ktx usages 2020-07-31 10:29:32 -04:00
eb0e0a1952 Use Kotlin extensions for preference editing 2020-07-30 23:04:50 -04:00
01a837fde6 Make source options dialog into a controller to retain state 2020-07-30 22:46:35 -04:00
b9488645d4 Hide cutout option when appropriate in reader settings sheet (closes #2982) 2020-07-30 22:38:10 -04:00
7a94b477cb Update ConstraintLayout 2020-07-30 22:22:44 -04:00
99710b45d1 Fix download status updates not appearing in chapters list (fixes #3358) 2020-07-30 22:22:37 -04:00
3813743e3d Split download notifications into progress and complete channels 2020-07-29 21:57:43 -04:00
9bb2334b69 Replace VectorDrawableCompat.create() with AppCompatResources.getDrawable()
Fixes crash when loading pin icon in Android 5/6.
2020-07-29 18:36:28 -04:00
7e73ede47a [Feature Request] - Download Complete Remidner #3475 (#3527)
* [Feat] Show a download complete notification channel when all downloads are completed. Auto cancels when onclick and navigate to download screen.

* [Feat] Update the download message string to shorten the length.
2020-07-29 17:31:45 -04:00
b0106aa420 Fix getting stuck in chapter loop when chapters have identical URLs 2020-07-27 15:59:15 -04:00
33e5fea96c Explicitly destroy webview on activity destroy 2020-07-26 17:47:43 -04:00
f0a1dcd120 Prevent spamming updates with newly favorited manga 2020-07-26 17:16:00 -04:00
26d5a87bef Fix source item flashing when pinning 2020-07-26 17:09:56 -04:00
52ae208df3 Show lang code in source long press dialog 2020-07-26 16:31:31 -04:00
34aaa7fb0a Remove explicit source browse button, tint pin icon when pinned 2020-07-26 16:28:58 -04:00
a8c784355c Fix Chinese plurals 2020-07-26 16:19:51 -04:00
0aed93becf Don't show chapter number in history item when unknown 2020-07-26 16:17:27 -04:00
1ea0804209 Translated using Weblate (Croatian) (#3421)
Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.5% (555 of 563 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Latvian)

Currently translated at 34.6% (195 of 563 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (563 of 563 strings)

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

Translated using Weblate (Latvian)

Currently translated at 27.4% (154 of 562 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Latvian)

Currently translated at 26.6% (150 of 562 strings)

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

Translated using Weblate (Latvian)

Currently translated at 25.2% (142 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Latvian)

Currently translated at 21.8% (123 of 562 strings)

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

Translated using Weblate (Filipino)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Italian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Romanian)

Currently translated at 99.8% (561 of 562 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.3% (553 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.3% (553 of 562 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (561 of 562 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 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% (562 of 562 strings)

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

Translated using Weblate (Georgian)

Currently translated at 6.9% (39 of 562 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 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% (562 of 562 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% (562 of 562 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% (562 of 562 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Filipino)

Currently translated at 97.8% (550 of 562 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Vietnamese)

Currently translated at 87.5% (492 of 562 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Swedish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Polish)

Currently translated at 99.8% (561 of 562 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Italian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Filipino)

Currently translated at 56.0% (315 of 562 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Norwegian Bokmål)

Currently translated at 90.0% (506 of 562 strings)

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

Translated using Weblate (Greek)

Currently translated at 99.6% (560 of 562 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 99.2% (558 of 562 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (561 of 562 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (562 of 562 strings)

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

Translated using Weblate (German)

Currently translated at 99.8% (561 of 562 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (560 of 560 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (560 of 560 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (559 of 559 strings)

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

Translated using Weblate (Italian)

Currently translated at 99.6% (557 of 559 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (559 of 559 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (559 of 559 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (559 of 559 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (558 of 558 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (558 of 558 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (558 of 558 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (557 of 557 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (557 of 557 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (557 of 557 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (557 of 557 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (555 of 556 strings)

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

Translated using Weblate (Swedish)

Currently translated at 99.4% (553 of 556 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Korean)

Currently translated at 58.6% (326 of 556 strings)

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

Translated using Weblate (Italian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 36.6% (204 of 556 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Bengali)

Currently translated at 61.8% (344 of 556 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 86.8% (483 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 77.3% (430 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 75.7% (421 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 67.9% (378 of 556 strings)

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

Translated using Weblate (Japanese)

Currently translated at 99.8% (555 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 67.8% (377 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 65.6% (365 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 64.5% (359 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 59.7% (332 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 98.0% (545 of 556 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.4% (542 of 556 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.3% (541 of 556 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 17.4% (97 of 556 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Hindi)

Currently translated at 99.8% (555 of 556 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (555 of 556 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 99.6% (554 of 556 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (556 of 556 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (556 of 556 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Czech)

Currently translated at 65.2% (362 of 555 strings)

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

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2020-07-26 16:17:12 -04:00
a52fbb012a Add pin icon to sources list (closes #2862) 2020-07-25 18:25:30 -04:00
2dc47352f8 Add operator functions for handling set preferences 2020-07-25 18:07:10 -04:00
e95a5be21d Show message when searching with no pinned sources 2020-07-25 17:14:34 -04:00
abd69d4f91 Fix weird backstack behaviour after clearing database
Shouldn't affect anything since controllers are recreated when entering different sections.
2020-07-25 16:45:27 -04:00
d749e309f8 Minor optimizations for local source dir lookups 2020-07-25 14:20:47 -04:00
e4d075fb91 Update dependencies 2020-07-25 13:07:56 -04:00
71c6c71081 Lazily find chapter directories 2020-07-25 12:54:17 -04:00
d2b14bcfc4 Inline extension functions 2020-07-25 12:12:44 -04:00
2c04c81bd1 Increase dismiss timeout for what's new snackbar 2020-07-25 12:07:37 -04:00
dd66c83c50 Use some more core-ktx extensions 2020-07-25 12:07:20 -04:00
9e51d82154 Replace custom visibility extension functions 2020-07-25 12:00:27 -04:00
bdc441a5be Hide manga title in toolbar when at top 2020-07-25 11:34:44 -04:00
2dcb73700b Update Material Components 2020-07-25 11:05:05 -04:00
ee01686ae4 Change to J2k style downloads while keeping support for the old ones (#3513)
* Change to J2k style downloads while keeping support for the old ones

* Tweaks based on comments in the PR

* Add scanlator to download pending deleter chapter data class

* Inline the foreach functions

* Make the rename function do less lookups

* More rename downloaded chapter tweaks

* Downloader conflict fixing
2020-07-25 10:59:33 -04:00
9a55cf880e More consistent library list view padding (closes #3509) 2020-07-25 10:52:23 -04:00
6742cdeb8b Hide tracking button if none logged in, show for non-favorited manga (closes #3507) 2020-07-25 10:50:54 -04:00
c37377bffa Remove divider between manga info header and chapters header 2020-07-24 22:27:58 -04:00
76147a9be7 fix: Download on WiFi regardless of metered status (#3489)
* fix: Download on WiFi regardless of metered status

fixes #3395

* fix: check if not WiFi rather than checking if connection is mobile
2020-07-24 22:25:10 -04:00
bf22e69250 Pad manga info tag chips properly to avoid overlapping scrollbar 2020-07-23 18:09:47 -04:00
49693934cf Update dependencies 2020-07-22 22:21:50 -04:00
e6a63ee5b2 Fix manga overflow menu items not appearing after adding to library via categories 2020-07-21 22:07:02 -04:00
08dc57fd02 Revert ViewPager2 library 2020-07-19 21:33:19 -04:00
cede590696 Remove unused ViewStatePagerAdapter dependency 2020-07-19 12:14:08 -04:00
c401915fb5 Don't initialize mangas if viewing source in list view if on metered connection 2020-07-19 12:13:50 -04:00
2a202bd510 Migrate library to ViewPager2 2020-07-19 11:48:41 -04:00
dcd8ed08fc Rename chapters_controller to manga_controller 2020-07-19 11:32:26 -04:00
d3ebedeef2 Explicitly show "No results found" in global search instead of hiding row 2020-07-18 17:46:29 -04:00
d2e2ebbe45 Add ripple to global search source title 2020-07-18 17:39:29 -04:00
0cef05dd89 Revert "Make controllers responsible for showing/hiding FAB"
This reverts commit 8ef548032f.
2020-07-18 16:37:36 -04:00
a443dc3040 Show tracker status in button 2020-07-18 16:37:00 -04:00
4e6cc013e5 Update subsampling-scale-image-view 2020-07-18 15:01:13 -04:00
0c65d54d89 Switch to tachiyomiorg fork of subsampling-scale-image-view 2020-07-18 10:58:35 -04:00
ccd0e0cdfe Manga about section layout tweaks 2020-07-18 10:57:11 -04:00
9278ca3f5e Move chapter filter/sort/display settings into a sheet 2020-07-17 17:01:16 -04:00
d7a70b962b Toggle about section when tapping on header/empty space 2020-07-17 16:29:59 -04:00
fff0f841fa Long press favorite button to manage categories 2020-07-17 14:44:09 -04:00
8ba426350f Fix checked state for manga header buttons 2020-07-17 14:20:36 -04:00
8ef548032f Make controllers responsible for showing/hiding FAB 2020-07-17 09:50:40 -04:00
5452e29840 Remove redundant Reading Mode header 2020-07-16 23:02:14 -04:00
148f8e6d11 Tweak track search dialog list item paddings 2020-07-16 23:01:24 -04:00
13a5662a84 Update Android Gradle plugin for Android Studio 4.0.1 2020-07-16 22:45:22 -04:00
6713a7ae3c Include source ID if name not found in restore error log (closes #3018) 2020-07-16 22:36:01 -04:00
226ad13061 Include source name in update error log (closes #3482) 2020-07-16 22:30:21 -04:00
88ee86b7ef Move edit categories to overflow 2020-07-16 22:22:06 -04:00
4bc2288806 Allow category names with different casing (fixes #3465) 2020-07-14 09:04:15 -04:00
a928d9fa0b Enable more WebView settings to better mimic regular browser 2020-07-12 22:40:51 -04:00
d8f4e6b45f Don't capitalize buttons (closes #3454) 2020-07-12 22:20:37 -04:00
5ef5087406 Replace some usages of findViewById 2020-07-12 19:29:39 -04:00
135c371d88 Fix manga info actions being cut off 2020-07-12 17:16:13 -04:00
966c196f4a Remove unused CoverCache param from LibraryController 2020-07-12 17:04:52 -04:00
dc43e41896 Use view binding for sheets 2020-07-12 17:04:12 -04:00
4809d06d04 Speed up controller fade and tab expansion animations 2020-07-11 23:08:39 -04:00
9f7fda0bc5 Move edit cover to manga info 2020-07-11 22:40:05 -04:00
1f67695713 Hide migrate option for non-favorited manga 2020-07-11 19:29:40 -04:00
66ef1a8206 Hide toolbars when reader color filter sheet is opened 2020-07-11 19:21:41 -04:00
beaffc3870 Remove redundant layout for reader color filter sheet 2020-07-11 19:16:49 -04:00
8536ecb611 Remove color filter preview image 2020-07-11 19:09:13 -04:00
d7a89b0f8c Remove 32-bit color setting from reader sheet 2020-07-11 18:58:04 -04:00
943081e80d Reorder animation speed options 2020-07-11 18:36:08 -04:00
3f007a1edd Update some icons 2020-07-11 17:21:08 -04:00
e33cacf6a4 Use root FAB/CoordinatorLayout in MangaController 2020-07-11 12:32:17 -04:00
23fe848a35 Move tracking to manga info actions
Currently just opens a separate view. To be iterated upon later.
2020-07-11 12:13:05 -04:00
fa5d2276c0 Group theme settings into category 2020-07-10 22:37:36 -04:00
d353a3457d Split general reader settings into reading mode and display 2020-07-10 22:29:46 -04:00
b363b9fc1a Make page transitions setting apply to webtoon viewer as well 2020-07-10 22:24:55 -04:00
1920568057 Unify crop borders settings 2020-07-10 22:19:08 -04:00
763da19c9d Add shortcut to global search query from library (closes #2183) 2020-07-10 19:31:23 -04:00
1813dbbf59 Add ability to sort library by date added (closes #1287) 2020-07-10 15:56:06 -04:00
339169b624 Hide invert volume keys setting when volume keys isn't enabled 2020-07-10 12:47:52 -04:00
93960315d9 Prevent downloads when less than 50MB of disk space is available (closes #1018) 2020-07-10 12:47:19 -04:00
479eb1ba71 Attach some FABs and snackbars to root CoordinatorLayout
Fixes some issues around snackbars sometimes being out of view.
2020-07-10 11:15:57 -04:00
962d8e5fd2 Don't capitalize category names 2020-07-10 09:57:29 -04:00
fefd4c0b26 Update build.gradle (#3438)
Replace minify and shink resources with post processing block
2020-07-10 09:36:33 -04:00
40639c0933 Update dependencies, remove play-services-oss-licenses 2020-07-09 18:30:57 -04:00
7401673ac1 Update some icons 2020-07-07 17:29:32 -04:00
41f759dafe Make tabs not all caps 2020-07-07 17:29:32 -04:00
Jay
73dcc7bcb1 Fix download cache not working for a source name change 2020-07-07 17:29:32 -04:00
d8b1f60581 update BaseController.kt (#3423) 2020-07-07 17:29:15 -04:00
16fc58bd16 Use custom view to handle manga info cover size (fixes #3354) 2020-07-04 16:08:01 -04:00
68df2f4ce7 Point to GitHub releases for stable changelogs 2020-07-04 14:36:56 -04:00
367932de69 Move preview notes to GitHub 2020-07-04 14:06:23 -04:00
3da08cbcbf Add cv and fil locales 2020-07-04 13:56:25 -04:00
68efc0c42f Fix Chinese plurals 2020-07-04 13:56:13 -04:00
f5b01b0ca2 Translated using Weblate (Chuvash) (#3352)
Currently translated at 100.0% (555 of 555 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (555 of 555 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (555 of 555 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (555 of 555 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (555 of 555 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (555 of 555 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (555 of 555 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Swedish)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 56.6% (314 of 554 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 43.5% (241 of 554 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 42.5% (236 of 554 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 40.7% (226 of 554 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Chuvash)

Currently translated at 33.3% (185 of 554 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (554 of 554 strings)

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

Added translation using Weblate (Chuvash)

Translated using Weblate (Chinese (Traditional))

Currently translated at 97.8% (542 of 554 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 96.9% (537 of 554 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 96.2% (533 of 554 strings)

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

Translated using Weblate (Filipino)

Currently translated at 43.8% (243 of 554 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 94.4% (523 of 554 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (553 of 554 strings)

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

Translated using Weblate (Hindi)

Currently translated at 98.0% (543 of 554 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (553 of 554 strings)

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

Translated using Weblate (Japanese)

Currently translated at 99.2% (550 of 554 strings)

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

Translated using Weblate (Norwegian Bokmål)

Currently translated at 89.3% (495 of 554 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 99.6% (552 of 554 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (554 of 554 strings)

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

Deleted translation using Weblate (Portuguese (Portugal))

Added translation using Weblate (Portuguese (Portugal))

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (554 of 554 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 98.3% (538 of 547 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Polish)

Currently translated at 99.8% (546 of 547 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 99.6% (545 of 547 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 92.5% (506 of 547 strings)

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

Translated using Weblate (Filipino)

Currently translated at 44.0% (241 of 547 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Bulgarian)

Currently translated at 96.5% (527 of 546 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Filipino)

Currently translated at 44.1% (241 of 546 strings)

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

Translated using Weblate (Filipino)

Currently translated at 25.0% (137 of 546 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 92.1% (503 of 546 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 92.6% (503 of 543 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Filipino)

Currently translated at 19.7% (107 of 543 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 92.6% (503 of 543 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (543 of 543 strings)

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

Added translation using Weblate (Filipino)

Translated using Weblate (Norwegian Bokmål)

Currently translated at 83.2% (452 of 543 strings)

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

Translated using Weblate (Japanese)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 99.8% (542 of 543 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (543 of 543 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% (543 of 543 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% (543 of 543 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 90.2% (490 of 543 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 86.0% (467 of 543 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 84.3% (458 of 543 strings)

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

Translated using Weblate (Croatian)

Currently translated at 99.8% (542 of 543 strings)

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

Translated using Weblate (Polish)

Currently translated at 99.6% (541 of 543 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Russian)

Currently translated at 99.6% (541 of 543 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Croatian)

Currently translated at 99.8% (542 of 543 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 83.0% (451 of 543 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 81.7% (444 of 543 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 81.3% (442 of 543 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (543 of 543 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (541 of 541 strings)

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

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2020-07-04 13:52:36 -04:00
5733429682 Merge pull request #3406 from purinsan/dev
Fix #2767
2020-07-04 13:48:43 -04:00
c6d29fc19b Move MaterialFastScroll to widget package 2020-07-04 13:45:01 -04:00
d9a12d79b0 Add ability to download unread chapters from library 2020-07-04 10:54:01 -04:00
963cf4c996 Fallback to default UA string for all network requests 2020-07-04 10:33:31 -04:00
0ef073669a Update notifier paused state when starting downloads (fixes #3254) 2020-07-04 10:27:57 -04:00
ed1123feb0 Define SDK/build tool versions in buildSrc 2020-07-01 11:53:30 -04:00
c2114bbd4f Dependency updates 2020-07-01 10:15:46 -04:00
fedb1d2590 Fix Set categories dialog not showing up when manga is added from download snackbar 2020-06-30 22:20:35 +06:30
eef39b75a6 Dependency updates 2020-06-28 22:38:24 -04:00
eca593ac36 Hide download menu for local manga 2020-06-28 22:29:49 -04:00
a1917b8c81 Jump to chapters list earlier on to avoid jank 2020-06-27 21:08:43 -04:00
ec6dba12bd Fix a settings section label 2020-06-27 10:24:29 -04:00
da0671ad62 More descriptive "Unknown"s in manga info 2020-06-25 18:17:17 -04:00
04d83e9a6a Add option to reverse tapping (#3360)
* Add option to reverse tapping

* Fix string for preference key

* Invert tapping for Webtoon and Vertical

* Use enum instead of boolean

* Add option to reader sheet

* Hide from reader sheet if tapping disabled and remove hard coded string

* Hide option if tapping disabled
2020-06-25 18:12:12 -04:00
2cb7624953 Make jumping to manga chapters optional 2020-06-25 18:10:46 -04:00
e6ace844b6 Hide option if tapping disabled 2020-06-25 18:01:45 +02:00
f2f6628693 Separate out chapters heading into separate adapter 2020-06-24 22:48:20 -04:00
1c33032721 Scroll directly to chapters list for library manga (closes #3353) 2020-06-24 22:39:53 -04:00
b3f5f13c39 Lazily instantiate Cloudflare client 2020-06-24 18:07:29 -04:00
a5339969c9 Fix about info now showing after initial manga load (closes #3381) 2020-06-24 18:07:16 -04:00
1681437206 Hide from reader sheet if tapping disabled and remove hard coded string 2020-06-24 15:48:08 +02:00
2eaf083eee Add option to reader sheet 2020-06-24 09:50:23 +02:00
3645d19135 Add advanced option for DNS over HTTPS via Cloudflare (closes #3377) 2020-06-23 22:42:53 -04:00
3b4b1185e2 Add network header logging in debug builds 2020-06-23 22:26:37 -04:00
406c5bde11 Open error log on tapping notification body (closes #3376) 2020-06-23 22:17:19 -04:00
75d1913aaf ignore fact that loader might be null (#3374) 2020-06-23 18:11:47 -04:00
eb254d9c56 Move about summary more/less expander button 2020-06-23 08:51:51 -04:00
4e633b8936 Replace filter group icons with expand more/less 2020-06-23 08:51:38 -04:00
e99a27e382 Fix filter group icon being invisible in light theme 2020-06-23 08:45:36 -04:00
c8a6a2653f Use enum instead of boolean 2020-06-23 12:09:10 +02:00
0ea0eba4f0 Invert tapping for Webtoon and Vertical 2020-06-23 10:58:56 +02:00
7f88b56d8b Resolve merge conflicts 2020-06-23 10:23:14 +02:00
789421c7a0 Scroll up/down when tapping top/bottom thirds in vertical reading modes (closes #3363) 2020-06-22 22:36:33 -04:00
d44503cb19 Show available manga info on view load (closes #3367) 2020-06-22 22:28:53 -04:00
0258422527 Remove "No chapter" string in favour of just "0 chapters" (should close #3357) 2020-06-22 22:21:40 -04:00
47327d840d Remove redundant landscape manga info header 2020-06-22 22:20:40 -04:00
d0f1a33744 Fix string for preference key 2020-06-21 18:50:55 +02:00
c54b8e62d7 Add option to reverse tapping 2020-06-21 16:47:18 +02:00
3dc738f28c Restore separate artist field 2020-06-20 22:30:02 -04:00
ca75400467 Remove incorrectly translated app name 2020-06-20 18:37:40 -04:00
65091c05c9 Allow switching between read/unread chapters more easily 2020-06-20 16:17:03 -04:00
52e846f3b6 More manga info header tweaks 2020-06-20 11:41:20 -04:00
ce22b2c29a Add shortcut to open extension package in system settings 2020-06-20 10:43:35 -04:00
361b0284fa Add tooltips to manga info header action icons 2020-06-19 17:40:17 -04:00
b8947a1c50 Bleed cover backdrop into actions bar in manga info header 2020-06-19 17:32:44 -04:00
8d1effa0e8 Add Persian locale 2020-06-19 17:22:22 -04:00
096a9f4cbf Translated using Weblate (Turkish) (#3326)
Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (541 of 541 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Hungarian)

Currently translated at 37.1% (203 of 546 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Malay)

Currently translated at 99.8% (545 of 546 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (French)

Currently translated at 99.4% (543 of 546 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Tagalog)

Currently translated at 77.1% (421 of 546 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (546 of 546 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 81.8% (447 of 546 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (546 of 546 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 79.7% (436 of 547 strings)

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

Translated using Weblate (Tagalog)

Currently translated at 70.9% (388 of 547 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Persian)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (547 of 547 strings)

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

Added translation using Weblate (Persian)

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 77.1% (423 of 548 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (549 of 549 strings)

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

Translated using Weblate (Croatian)

Currently translated at 99.8% (547 of 548 strings)

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

Translated using Weblate (Swedish)

Currently translated at 100.0% (548 of 548 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2020-06-19 17:21:17 -04:00
18712b166f Combine chapter sort asc/desc and sorting mode menus 2020-06-19 16:27:17 -04:00
4605e14729 Merge manga info and chapters views 2020-06-19 16:11:59 -04:00
a768280d82 Use view binding in TrackAdapter 2020-06-17 22:45:40 -04:00
cbb8f25645 Allow extension details header to scroll 2020-06-17 22:39:45 -04:00
24ff7ff67c Use download badge color for downloaded only banner 2020-06-17 22:21:24 -04:00
eee0bd6cf4 Rename some code from "catalogue" to "source" 2020-06-17 22:18:33 -04:00
f176a5179a Restore individual sources in source filter view 2020-06-17 22:10:12 -04:00
381ba86e3c Adopt download/local badge colors from J2K 2020-06-17 22:00:58 -04:00
f4be8e28ca Stop IDE from complaining about missing translations 2020-06-17 09:10:12 -04:00
e17605f8d9 Localize/reword "Loader not implemented" error message 2020-06-17 09:09:01 -04:00
e06a488af8 Add categories to more 2020-06-17 09:07:20 -04:00
6dc8bfed8d Remove some unnecessary strings 2020-06-16 17:30:35 -04:00
f642f23366 chapter deletion logic fixed (#3320) 2020-06-15 23:03:56 -04:00
c041410f61 Clean up extension details layout a bit 2020-06-15 22:54:11 -04:00
ff5f13eafe Remove categories for multi language "single" sources 2020-06-15 22:45:28 -04:00
749b240897 Group extension details classes together 2020-06-15 22:25:15 -04:00
9207762ade Add some missing locales 2020-06-14 11:55:28 -04:00
0a22950ad3 Georgian translation (#3323)
* added georgian (ka) locale for strings.xml

* added georgian to locale list
2020-06-14 11:26:38 -04:00
0fcd404b4f Translated using Weblate (Portuguese (Brazil)) (#3312)
Currently translated at 100.0% (549 of 549 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Croatian)

Currently translated at 99.8% (547 of 548 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (549 of 549 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (German)

Currently translated at 100.0% (549 of 549 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Japanese)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Polish)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 76.5% (421 of 550 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 73.0% (402 of 550 strings)

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

Translated using Weblate (Chinese (Traditional))

Currently translated at 71.0% (391 of 550 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (550 of 550 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% (550 of 550 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% (550 of 550 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% (550 of 550 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% (550 of 550 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (550 of 550 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% (550 of 550 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Malay)

Currently translated at 99.8% (549 of 550 strings)

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

Translated using Weblate (Croatian)

Currently translated at 97.4% (536 of 550 strings)

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

Translated using Weblate (Norwegian Bokmål)

Currently translated at 84.0% (462 of 550 strings)

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

Translated using Weblate (Swedish)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (550 of 550 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (550 of 550 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% (550 of 550 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% (550 of 550 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Japanese)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Japanese)

Currently translated at 91.9% (504 of 548 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (548 of 548 strings)

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

Added translation using Weblate (Belarusian)

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (548 of 548 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% (548 of 548 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% (548 of 548 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% (548 of 548 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 12.7% (70 of 548 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 12.7% (70 of 548 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Polish)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Italian)

Currently translated at 99.4% (545 of 548 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (548 of 548 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Italian)

Currently translated at 98.9% (541 of 547 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Polish)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (547 of 547 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (547 of 547 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% (547 of 547 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Turkish)

Currently translated at 99.8% (545 of 546 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Polish)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (546 of 546 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 95.4% (520 of 545 strings)

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

Translated using Weblate (Japanese)

Currently translated at 73.7% (402 of 545 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 98.5% (537 of 545 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 98.3% (536 of 545 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 99.6% (543 of 545 strings)

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

Translated using Weblate (Bulgarian)

Currently translated at 95.9% (523 of 545 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (German)

Currently translated at 99.8% (544 of 545 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (545 of 545 strings)

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

Translated using Weblate (Spanish (Latin America))

Currently translated at 11.2% (61 of 542 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Polish)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Malay)

Currently translated at 99.8% (541 of 542 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (542 of 542 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% (542 of 542 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% (542 of 542 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% (542 of 542 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% (542 of 542 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% (542 of 542 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% (542 of 542 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (542 of 542 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Swedish)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (540 of 541 strings)

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

Translated using Weblate (Croatian)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 99.6% (539 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 99.0% (536 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 99.0% (536 of 541 strings)

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

Translated using Weblate (Croatian)

Currently translated at 99.8% (540 of 541 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Arabic)

Currently translated at 95.5% (517 of 541 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 99.2% (537 of 541 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Ukrainian)

Currently translated at 92.0% (498 of 541 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (541 of 541 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 97.7% (529 of 541 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (541 of 541 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2020-06-14 11:26:28 -04:00
150ea29a70 Move untrusted/obsolete/unofficial extension text 2020-06-13 17:27:03 -04:00
f9baff0e90 Show "Start" in chapters FAB if nothing is read yet 2020-06-13 17:15:22 -04:00
6ad3fcb91d Rounded library badges 2020-06-13 17:00:32 -04:00
395ca3630c Combine author and artist fields in manga info 2020-06-13 16:25:26 -04:00
e9fbdb660b Fix webview height issue
Collapsing toolbar doesn't work currently.
2020-06-13 15:56:40 -04:00
526e029ebb Update some themes 2020-06-13 15:34:55 -04:00
763c4522b7 Reduce navigation view text weight 2020-06-13 15:17:33 -04:00
32e3ac63ed Bump dependencies 2020-06-13 15:17:21 -04:00
90f31aab38 Hide download/delete chapter menu item for local manga 2020-06-11 17:41:00 -04:00
3d44feaf2c Bump AndroidX dependency versions 2020-06-10 23:02:31 -04:00
f5a44245e9 Update SwipeRefreshLayout 2020-06-10 23:01:47 -04:00
7e7eb9f39f Use AndroidX WebKit library 2020-06-10 23:01:11 -04:00
6c9b982104 Remove unused INSTALL_SHORTCUT permission 2020-06-10 21:45:35 -04:00
a106104027 Enable localStorage in WebView 2020-06-09 09:15:43 -04:00
7753161332 Switch to AboutLibraries 2020-06-08 18:26:20 -04:00
ec9d592cf1 Maybe fix license task for Windows 2020-06-07 16:10:43 -04:00
31015504f4 TLS 1.3 support for Android < 10
Should fix https://github.com/inorichi/tachiyomi-extensions/issues/3233
2020-06-07 15:47:42 -04:00
bd40ec527d Use Gradle with sources 2020-06-07 15:47:13 -04:00
3899938b25 Reading mode snackbar 2020-06-07 11:14:19 -04:00
ca7373c28b Make extension settings button inline 2020-06-06 14:36:04 -04:00
390bdfa93d Update conductor-support-preference to handle EditTextPreference bind listener 2020-06-06 12:54:58 -04:00
ac2df87954 Alternate open source licenses plugin 2020-06-06 11:40:46 -04:00
0b73f8b1ef Update source/extension filter controller titles 2020-06-06 10:17:18 -04:00
812fcec9f0 Move source preferences to separate controller 2020-06-05 09:50:30 -04:00
ec7297f8c2 Add arrow to source search headers 2020-06-05 09:33:42 -04:00
0fdb19c07d Global Search click title to search in Source (#3265)
* Global Search click to search in source

* Add requested changes
2020-06-04 22:38:42 -04:00
3bc07b4753 Update Gradle 2020-06-03 09:11:57 -04:00
0b8b13d0bb Add FastScroller to downloads 2020-06-01 08:55:37 -04:00
4c5bf9bc8e Remove unused arrays 2020-05-31 18:44:38 -04:00
9f53109414 Default reading mode to RTL (since this is a manga app...) 2020-05-31 18:44:19 -04:00
bccb1229c8 use material password toggle (#3261)
use material for track search
2020-05-31 18:29:20 -04:00
40ab3fe0a6 Tweak extension detail header design 2020-05-31 18:28:23 -04:00
4a99118cce Replace some icons 2020-05-31 18:18:21 -04:00
58ba29fa16 Remove hide option from local source 2020-05-31 18:00:28 -04:00
54cfb2acdf Manage sources from extension details (closes #3152) 2020-05-31 16:23:51 -04:00
0bf14fd31c Sort sources/extensions filter languages 2020-05-31 15:09:19 -04:00
7dd9a0211b Remove Serializable interface from Source
No longer needed since we don't serialize it for the migration controller bundle.
2020-05-31 11:42:39 -04:00
3d43473bf8 Mark isDownloading flag as volatile (maybe fixes #3219) 2020-05-31 11:42:12 -04:00
2194c4ba28 Localize "Page list is empty" exception message 2020-05-31 11:26:36 -04:00
744a4f8f47 Force proper LTR layout for reader pages/bottom toolbar (fixes #3258) 2020-05-31 11:19:09 -04:00
67d91f7b69 Reword theme settings 2020-05-31 11:16:08 -04:00
bf5065d16b Minor cleanup, remove some usages of ArrayList 2020-05-30 23:42:11 -04:00
3e837f8781 Add Browse to start screen options 2020-05-30 23:22:48 -04:00
77d378ccd1 Fix crash when opening chapter from migration 2020-05-30 23:16:17 -04:00
1a542bae71 Replace deprecated height/width constraint attributes 2020-05-30 23:10:39 -04:00
e3ed12b5d2 Set reader menu visibility on system UI visibility change (closes #3149 and #2068) 2020-05-30 19:07:31 -04:00
759795940b Use onBackpressureLatest for chapter/page download observables (maybe fixes #3229) 2020-05-30 18:20:19 -04:00
a23d5ab734 Add bottom padding to migrate sources list 2020-05-30 18:06:40 -04:00
6ee69ef430 Bump Firebase dependency 2020-05-30 10:17:47 -04:00
3edf17d322 handle default category and add favorite check (#3248)
Download new chapters bug fix
2020-05-30 09:17:27 -04:00
8c2b2f99bc Add option to hide library category tabs 2020-05-28 18:15:24 -04:00
73dc51b3f6 Update Android Gradle plugin for AS 4.0 2020-05-28 17:40:16 -04:00
9a082d4df1 Fix being unable to restore backups if there's no categories 2020-05-25 16:41:53 -04:00
f430b6f853 Use getEnum for theme preferences too 2020-05-25 15:13:30 -04:00
78a352541a Use actual enum support for display mode preferences 2020-05-25 15:03:51 -04:00
a0f5633094 Use enums for other PreferenceValues 2020-05-25 14:07:12 -04:00
0af81c7d05 Comfortable grid code cleanup 2020-05-25 14:01:17 -04:00
52e82b3548 Comfortable Grid (#3238)
* Comfortable Grid

* Add requested changes

* Add more requested changes
2020-05-25 13:39:14 -04:00
f05b99ec1f Convert source display mode preference to flow, remove RxPreferences 2020-05-25 12:35:29 -04:00
194897bf3c Convert column preferences to flows 2020-05-24 18:34:19 -04:00
7cf26363c8 Convert last used source preference to flow 2020-05-24 18:16:07 -04:00
3d1dec4c05 [CI SKIP] Update issue closer action 2020-05-24 10:40:30 -04:00
af1935d2e4 Split up MigrationController 2020-05-23 20:49:20 -04:00
3c4bc17065 Reorganize some migration classes 2020-05-23 19:33:47 -04:00
333d1c1ad9 Remove explicit "SELECT" button from migration source list 2020-05-23 18:58:05 -04:00
4e027cec71 Move migration into browse as a tab 2020-05-23 18:53:13 -04:00
39ae84301a Move migration classes under browse 2020-05-23 18:40:33 -04:00
3bf14623ad Update some icons 2020-05-23 18:28:28 -04:00
ac8f2923e5 Handle empty thumbnail_url when refreshing covers 2020-05-23 15:19:17 -04:00
e9d3b75e2b Tweak history card design 2020-05-23 11:33:23 -04:00
e6bc181e7a Avoid replacing covers with null when updating library (sort of closes #3194) 2020-05-23 10:56:44 -04:00
a2ece82197 Group advanced settings 2020-05-23 10:46:07 -04:00
259946cf0a Make metadata updating optional 2020-05-23 10:37:46 -04:00
4fdb4f14a8 Move categories up in library settings 2020-05-23 10:33:49 -04:00
914f5e569b Update drag icon 2020-05-23 10:23:44 -04:00
9b4ffd1cd5 Fix recycled icon in source migration list when source isn't installed 2020-05-23 10:15:06 -04:00
ed87dd89a1 Dependency updates 2020-05-21 09:06:24 -04:00
3b41a78e76 Add explicit refresh icon asset (maybe fixes #3218) 2020-05-21 09:00:01 -04:00
0fccbbc0ca Rename downloaded chapters (#3216) 2020-05-20 22:42:30 -04:00
067627b51a Change resume button in history to an icon 2020-05-20 22:35:25 -04:00
09816ed5b6 Refactor history_item.xml to use ConstraintLayout 2020-05-20 22:32:04 -04:00
b457cdb0c2 Scroll up/down when tapping top/bottom quarters of webtoon viewer
Includes a fix from J2K: 4e45a337da
2020-05-20 22:23:45 -04:00
5fd1dec347 Add bottom padding to history/sources/extensions (fixes #3192) 2020-05-20 17:43:34 -04:00
647391ef73 Make library update error notification optional (closes #3200) 2020-05-20 17:39:01 -04:00
ed029c52ae download new chapters changes (#3193)
* download new chapters changes

* move initialFetchChapters logic into onNextChapters

* refractor download new chapter logic to be more explicit
2020-05-17 17:33:26 -04:00
102a372df9 Sort chapters by upload date (#3180)
* Added sorting by upload date

Spanish 'strings' contains the proper translation for the new feature.

* Added missing sorting cases handling

Previous commit missed some cases resulting in errors at runtime

* Implemented review changes

Shorter UI text and >= date comparison instead of >
2020-05-17 10:18:49 -04:00
d4ffb09a8b Minor edits 2020-05-17 10:18:05 -04:00
6ba052d2af Implemented review changes
Shorter UI text and >= date comparison instead of >
2020-05-16 18:54:47 -03:00
57b63f43f5 Fix cover thumbnails not being translucent for favorited manga in sources 2020-05-16 16:47:19 -04:00
2fb0969c75 Fix unread badges not hiding in list view 2020-05-16 16:38:43 -04:00
3357e878a5 Square covers in list view (closes #3121) 2020-05-16 16:35:40 -04:00
471d5d62d5 Move cover card outline clipping code from item to holder classes 2020-05-16 16:32:14 -04:00
e810b343cf Specify charset for ZIP chapters when using Android N+ (fixes #905) 2020-05-16 15:22:02 -04:00
620be2617a Remove redundant helper function 2020-05-16 12:28:11 -04:00
035038a0b6 Add migrate option from manga info view (closes #1903) 2020-05-16 12:27:56 -04:00
b8ffb87f01 Add fastscroller to migration lists 2020-05-16 12:15:04 -04:00
39e1e11f99 Sort list of sources in migration alphabetically 2020-05-16 12:12:03 -04:00
5f9df78ab0 Localize tracker not logged in error 2020-05-16 11:33:41 -04:00
a00d11701f Warn about missing sources before restoring backup 2020-05-16 11:29:55 -04:00
1cf74a5396 Add fastscroller to updates and history 2020-05-16 10:17:09 -04:00
8cd27a199d Minor cleanup 2020-05-16 10:00:22 -04:00
772929b5c6 Return job failure if library update actually doesn't start 2020-05-16 09:57:27 -04:00
c4ca3606ad Slightly simplify AMOLED theme definition 2020-05-15 09:22:01 -04:00
9e830f1c55 Added missing sorting cases handling
Previous commit missed some cases resulting in errors at runtime
2020-05-15 01:10:02 -03:00
ee8c71c14a Added sorting by upload date
Spanish 'strings' contains the proper translation for the new feature.
2020-05-14 23:38:19 -03:00
39bd823651 Minor show more info button margin adjustment 2020-05-14 22:33:36 -04:00
a9d16fad34 Add Croatian to settings 2020-05-14 17:25:33 -04:00
1da169319d Fix Chinese plurals 2020-05-14 17:25:26 -04:00
2bb1eea2be Translations (Continuous) (#3125)
* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Polish)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Japanese)

Currently translated at 76.2% (405 of 531 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Marathi)

Currently translated at 46.8% (249 of 531 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (531 of 531 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% (531 of 531 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (531 of 531 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (531 of 531 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% (531 of 531 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% (531 of 531 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% (531 of 531 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% (531 of 531 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% (531 of 531 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% (528 of 528 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 99.4% (525 of 528 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (528 of 528 strings)

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

Added translation using Weblate (Spanish (Latin America))

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Italian)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Italian)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Italian)

Currently translated at 93.5% (494 of 528 strings)

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

Translated using Weblate (Italian)

Currently translated at 93.5% (494 of 528 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 34.0% (180 of 528 strings)

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

Translated using Weblate (Hungarian)

Currently translated at 30.1% (159 of 528 strings)

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

Translated using Weblate (Kannada)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Marathi)

Currently translated at 46.5% (246 of 528 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Bengali)

Currently translated at 67.9% (359 of 528 strings)

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

Translated using Weblate (Kannada)

Currently translated at 55.8% (295 of 528 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 99.8% (527 of 528 strings)

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

Translated using Weblate (Portuguese)

Currently translated at 97.9% (517 of 528 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Bulgarian)

Currently translated at 99.8% (527 of 528 strings)

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

Translated using Weblate (Indonesian)

Currently translated at 97.7% (516 of 528 strings)

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

Translated using Weblate (Kannada)

Currently translated at 27.6% (146 of 528 strings)

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

Translated using Weblate (Marathi)

Currently translated at 34.8% (184 of 528 strings)

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

Translated using Weblate (Swedish)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Swedish)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (528 of 528 strings)

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

Added translation using Weblate (Kannada)

Translated using Weblate (Marathi)

Currently translated at 18.9% (100 of 528 strings)

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

Translated using Weblate (Marathi)

Currently translated at 8.5% (45 of 528 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Russian)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Polish)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (French)

Currently translated at 99.8% (527 of 528 strings)

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

Translated using Weblate (Spanish)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (526 of 527 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (526 of 527 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 99.8% (526 of 527 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Romanian)

Currently translated at 100.0% (526 of 526 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (526 of 526 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (526 of 526 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (526 of 526 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (526 of 526 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Dutch)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Turkish)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Finnish)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Catalan)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Swedish)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Malay)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Hindi)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (528 of 528 strings)

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

Translated using Weblate (Chinese (Simplified))

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Sardinian)

Currently translated at 99.8% (526 of 527 strings)

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

Translated using Weblate (French)

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Greek)

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (German)

Currently translated at 100.0% (527 of 527 strings)

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

Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (527 of 527 strings)

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

Translated using Weblate (Czech)

Currently translated at 73.4% (387 of 527 strings)

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

* Update translation files

Updated by "Cleanup translation files" hook in Weblate.

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

Co-authored-by: Hosted Weblate <hosted@weblate.org>
2020-05-14 17:23:15 -04:00
9cb45b92e1 Move extension preferences to separate controller 2020-05-14 17:20:09 -04:00
4a8d5098da Add lost Croatian translation (#3176) 2020-05-14 17:19:20 -04:00
d875d5ef74 Better distinguish between obsolete and unofficial extensions 2020-05-13 22:47:44 -04:00
fc4e290c49 Show notification with error log on update failures 2020-05-13 22:28:15 -04:00
ccc198e081 Reword "Pager" to "Paged" 2020-05-12 22:48:51 -04:00
9f0ed77423 Remove unused function 2020-05-12 22:32:56 -04:00
bb3e616890 Avoid showing uninstalled source as last used 2020-05-12 21:28:20 -04:00
573f3a392a Null check for local source cover parent dir 2020-05-12 09:18:18 -04:00
Jay
830a834ea6 Using a float on the progress on app updates
Not sure if needed but I saw some funny behavior with the progress bar
2020-05-12 09:17:33 -04:00
e4ea5d0344 Don't throw exception to stop restore job 2020-05-12 09:11:52 -04:00
97aed045e6 Copy to clipboard when long pressing tracking title (closes #3163) 2020-05-12 09:11:15 -04:00
46b01c6134 fix regular font family name (#3154) 2020-05-11 08:31:32 -04:00
e208fa4020 Temporarily revert concurrent manga updates 2020-05-10 23:13:58 -04:00
6b71264482 Merge branch 'dev' of github.com:inorichi/tachiyomi into dev 2020-05-10 23:12:23 -04:00
5723c184b1 Cover Update Followup to Address #3139 (#3150)
* update cover logic when thumbnail url becomes null

* always clear cover on refresh even if custom cover is set

* remove concurrency changes
2020-05-10 23:10:31 -04:00
530daeaa3a Move notification logic out of LibraryUpdateService 2020-05-10 18:14:43 -04:00
dd1b5c7ea7 Minor edits 2020-05-10 12:58:13 -04:00
3cffc6890d Animate in/out ActionToolbar 2020-05-10 12:56:04 -04:00
04d44f19f5 Show date format examples, migrate to FlowPreferences 2020-05-10 12:26:18 -04:00
d46a742a43 Convert rotation to FlowPreference, remove some unused subscriptions code 2020-05-10 11:42:46 -04:00
a94fd24fa9 Default unreadBadge setting to true (closes #3138) 2020-05-10 11:23:10 -04:00
8a4c4c346a Minor wording edits 2020-05-10 11:15:59 -04:00
dc54299e24 Manga cover updates (#3101)
* cover caching overhaul

* add ui for removing custom cover

* skip some loading work

* minor cleanup

* allow refresh library metadata to refresh local manga

* rename metadata_date to cover_last_modified

* rearrange removeMangaFromLibrary

* change custom cover directory
add setting for updating cover when refreshing library

* remove toggle and explicit action for updating covers
2020-05-10 11:15:25 -04:00
436253dd63 Add more info in tracking settings section 2020-05-09 12:41:10 -04:00
29feee0095 Update details metadata along with chapters list 2020-05-09 10:26:02 -04:00
63f3180dff Move DB transaction blocks to only the DB portions of restore logic 2020-05-09 09:57:03 -04:00
6b3b98cf57 Revert Nucleus to 3.0.0
Fixes #3028
Fixes #3013
Fixes #3037
2020-05-09 09:56:42 -04:00
1442e2b53e Option to hide unread badges (closes #3095) 2020-05-08 23:01:16 -04:00
521ebf0678 Remove redundant DB call in library settings (closes #3128) 2020-05-08 22:32:12 -04:00
a20874f6a1 String Formatting Fixes (#3118)
String Formatting Fixes
2020-05-08 21:44:01 -04:00
40776bdc8d Concurrently refresh trackers 2020-05-08 19:04:45 -04:00
f853158e6b Do library checks from up to 5 sources concurrently 2020-05-08 18:58:49 -04:00
c9035b5df9 Download from up to 5 different sources concurrently (closes #2534) 2020-05-08 18:48:00 -04:00
150132f4dd Concurrently download up to 5 pages at a time 2020-05-08 18:41:11 -04:00
fb97ac47bc Replace restore completion string with plurals 2020-05-08 18:07:21 -04:00
5b53b90495 Translated using Weblate (Swedish) (#3124)
Translations
2020-05-08 17:40:40 -04:00
c6513d4450 Translated using Weblate (Swedish) (#3043)
Translations (Continuous)
2020-05-08 17:27:21 -04:00
ce6848b3c0 Show icon when chapter is bookmarked 2020-05-07 23:03:22 -04:00
d86d861e4b MaterialFastScroll updates 2020-05-07 22:40:23 -04:00
3b45fcdb21 Use borderless buttons everywhere for consistency 2020-05-07 22:24:30 -04:00
3d1250f2f8 Do some download deletion in coroutines instead of completable 2020-05-07 19:34:40 -04:00
eaf1ef831a Remove fdroid flavor 2020-05-07 17:24:06 -04:00
b4c7992726 Minor cleanup of wakelocks, extension ID backup 2020-05-07 09:14:52 -04:00
03baa21185 Destroy action mode after actioning in chapters list (closes #3004) 2020-05-06 23:21:17 -04:00
694de99a3f Adopt MaterialFastScroll from J2K fork
de8cb8c1b0
2020-05-06 23:16:02 -04:00
8383f4fb7b Increase webtoon setting change page refresh range (closes #3088) 2020-05-06 23:06:49 -04:00
eb3fff6c51 Alternative way of reading local manga JSON file 2020-05-06 22:59:54 -04:00
4ff3f0bcba Tweak about manga heading 2020-05-06 22:59:38 -04:00
788ea052fc Convert app updater to foreground service 2020-05-06 22:34:30 -04:00
08ba805bbd Fix updates/history section headers in RTL locales 2020-05-05 22:28:12 -04:00
dbd14c6dac Refresh page on 32-bit color setting change 2020-05-05 17:45:07 -04:00
5977fca6b9 Fix bottom nav opening if opening manga from updates while in action mode 2020-05-04 19:08:35 -04:00
fcf596d36b Update changelog link for preview builds 2020-05-04 08:46:07 -04:00
dabca5f09e Lighter weight method of rounding cover art 2020-05-03 22:29:09 -04:00
018dbce57e [CI SKIP] Add dev branch deploy script configuration 2020-05-03 20:08:44 -04:00
bd45cc2024 Only alert once for backup/restore progress notifications 2020-05-03 20:08:29 -04:00
abf47deee3 Fix anilist jsonnull issue
41c64b7058
2020-05-03 20:05:21 -04:00
ce0090f0ca Bunch of crash fixes 2020-05-03 12:17:10 -04:00
6cd34614f6 More extreme method for enforcing WebView availability 2020-05-02 19:03:48 -04:00
378 changed files with 16596 additions and 8480 deletions

View File

@ -14,9 +14,9 @@
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here * Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
# Bugs # Bugs
* Include version (Setting > About > Version) * Include version (More > About > Version)
* If not latest, try updating, it may have already been solved * If not latest, try updating, it may have already been solved
* Dev version is equal to the number of commits as seen in the main page * Preview version is equal to the number of commits as seen in the main page
* Include steps to reproduce (if not obvious from description) * Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed) * Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible) * If it could be device-dependent, try reproducing on another device (if possible)

View File

@ -2,7 +2,7 @@
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v0.9.2) - I have updated to the latest version of the app (stable is v0.10.0)
- I have updated all extensions - 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 - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions

View File

@ -1,7 +1,7 @@
--- ---
name: "🐞 Bug report" name: "🐞 Bug report"
about: Report a bug about: Report a bug
title: "[Bug] Write short description here" title: "[Bug] <Write short description here>"
labels: "bug" labels: "bug"
--- ---
@ -9,7 +9,7 @@ labels: "bug"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v0.9.2) - I have updated to the latest version of the app (stable is v0.10.0)
- I have updated all extensions - 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 - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions

View File

@ -1,7 +1,7 @@
--- ---
name: "🌟 Feature request" name: "🌟 Feature request"
about: Suggest a feature to improve Tachiyomi about: Suggest a feature to improve Tachiyomi
title: "[Feature Request] Write short description here" title: "[Feature Request] <Write short description here>"
labels: "feature" labels: "feature"
--- ---
@ -9,7 +9,7 @@ labels: "feature"
I acknowledge that: I acknowledge that:
- I have updated to the latest version of the app (stable is v0.9.2) - I have updated to the latest version of the app (stable is v0.10.0)
- I have updated all extensions - 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 - If this is an issue with an extension, that I should be opening an issue in https://github.com/inorichi/tachiyomi-extensions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.0 MiB

After

Width:  |  Height:  |  Size: 1.2 MiB

View File

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

View File

@ -62,6 +62,13 @@ deploy:
branch: master branch: master
condition: "-z $TRAVIS_TAG" condition: "-z $TRAVIS_TAG"
repo: inorichi/tachiyomi repo: inorichi/tachiyomi
- provider: script
script: ".travis/deploy.sh"
skip_cleanup: true
on:
branch: dev
condition: "-z $TRAVIS_TAG"
repo: inorichi/tachiyomi
env: env:
global: global:

28
PREVIEW_RELEASE_NOTES.md Normal file
View File

@ -0,0 +1,28 @@
### r1810
- Background jobs were migrated to a new system. You may need to toggle the settings to ensure they
run properly. This includes app updates, library updates, and automatic backups.
### r1340
- A new screen for managing extensions was added. If you previously installed extensions from FDroid,
you will have to uninstall all of them first (tap on the extension then uninstall), otherwise you won't be able
to update them due to signature mismatch. You won't lose anything in this process as the extensions themselves
don't store anything.
### r959
- The download manager has been rewritten and it's possible some of your downloads
aren't recognized anymore. You may have to check your downloads folder and manually delete those.
- You can now download to any folder in your SD card.
- The download directory setting has been reset.
### r857
- **Important!** Delete after read has been updated.
This means the value has been reset set to disabled.
This can be changed in Settings > Downloads
### r736
- **Important!** Now chapters follow the order of the sources. **It's required that you update your entire library
before reading in order for them to be synced.** Old behavior can be restored for a manga in the overflow menu of the chapters tab.
### r724
- Kissmanga covers may not load anymore. The only workaround is to update the details of the manga
from the info tab, or clearing the database (the latter won't fix covers from library manga).

View File

@ -38,7 +38,7 @@ Please make sure to read the full guidelines. Your issue may be closed without w
<details><summary>Bugs</summary> <details><summary>Bugs</summary>
* Include version (Setting > About > Version) * Include version (More > About > Version)
* If not latest, try updating, it may have already been solved * If not latest, try updating, it may have already been solved
* Preview version is equal to the number of commits as seen in the main page * Preview version is equal to the number of commits as seen in the main page
* Include steps to reproduce (if not obvious from description) * Include steps to reproduce (if not obvious from description)

View File

@ -1,7 +1,9 @@
import org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'com.google.android.gms.oss-licenses-plugin' apply plugin: 'com.mikepenz.aboutlibraries.plugin'
apply plugin: 'kotlin-android' apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions' apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt' apply plugin: 'kotlin-kapt'
@ -30,16 +32,16 @@ ext {
} }
android { android {
compileSdkVersion 29 compileSdkVersion AndroidConfig.compileSdk
buildToolsVersion '29.0.3' buildToolsVersion AndroidConfig.buildTools
defaultConfig { defaultConfig {
applicationId "eu.kanade.tachiyomi" applicationId "eu.kanade.tachiyomi"
minSdkVersion 21 minSdkVersion AndroidConfig.minSdk
targetSdkVersion 29 targetSdkVersion AndroidConfig.targetSdk
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
versionCode 45 versionCode 46
versionName "0.9.2" versionName "0.10.0"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -53,8 +55,8 @@ android {
} }
} }
viewBinding { buildFeatures {
enabled = true viewBinding = true
} }
buildTypes { buildTypes {
@ -62,11 +64,15 @@ android {
versionNameSuffix "-${getCommitCount()}" versionNameSuffix "-${getCommitCount()}"
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
} }
// release { release {
// minifyEnabled true postprocessing {
// shrinkResources true obfuscate false
// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' optimizeCode true
// } removeUnusedCode false
removeUnusedResources true
proguardFiles 'proguard-rules.pro'
}
}
} }
flavorDimensions "default" flavorDimensions "default"
@ -93,18 +99,25 @@ android {
exclude 'META-INF/NOTICE' exclude 'META-INF/NOTICE'
} }
dependenciesInfo {
includeInApk = false
}
lintOptions { lintOptions {
disable 'MissingTranslation'
disable 'ExtraTranslation'
abortOnError false abortOnError false
checkReleaseBuilds false checkReleaseBuilds false
} }
compileOptions { compileOptions {
sourceCompatibility = 1.8 sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = 1.8 targetCompatibility = JavaVersion.VERSION_1_8
} }
kotlinOptions { kotlinOptions {
jvmTarget = "1.8" jvmTarget = JavaVersion.VERSION_1_8.toString()
} }
} }
@ -114,30 +127,34 @@ androidExtensions {
dependencies { dependencies {
// Modified dependencies // AndroidX libraries
implementation 'com.github.inorichi:subsampling-scale-image-view:ac0dae7'
implementation 'com.github.inorichi:junrar-android:634c1f5'
// Android support library
implementation 'androidx.appcompat:appcompat:1.1.0'
implementation 'androidx.cardview:cardview:1.0.0'
implementation 'androidx.constraintlayout:constraintlayout:1.1.3'
implementation 'androidx.recyclerview:recyclerview:1.1.0'
implementation 'androidx.preference:preference:1.1.1'
implementation 'androidx.annotation:annotation:1.1.0' implementation 'androidx.annotation:annotation:1.1.0'
implementation 'androidx.browser:browser:1.2.0' implementation 'androidx.appcompat:appcompat:1.3.0-alpha01'
implementation 'androidx.multidex:multidex:2.0.1'
implementation 'androidx.biometric:biometric:1.0.1' 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'
final lifecycle_version = '2.2.0' final lifecycle_version = '2.3.0-alpha06'
implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-common-java8:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-extensions:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-process:$lifecycle_version"
implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version" implementation "androidx.lifecycle:lifecycle-runtime-ktx:$lifecycle_version"
// UI library // Job scheduling
implementation 'com.google.android.material:material:1.1.0' final work_version = '2.4.0'
implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
standardImplementation 'com.google.firebase:firebase-core:17.4.0' // UI library
implementation 'com.google.android.material:material:1.3.0-alpha02'
standardImplementation 'com.google.firebase:firebase-core:17.4.4'
// ReactiveX // ReactiveX
implementation 'io.reactivex:rxandroid:1.2.1' implementation 'io.reactivex:rxandroid:1.2.1'
@ -146,13 +163,17 @@ dependencies {
implementation 'com.github.pwittchen:reactivenetwork:0.13.0' implementation 'com.github.pwittchen:reactivenetwork:0.13.0'
// Network client // Network client
final okhttp_version = '4.5.0' final okhttp_version = '4.8.0'
implementation "com.squareup.okhttp3:okhttp:$okhttp_version" implementation "com.squareup.okhttp3:okhttp:$okhttp_version"
implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version" implementation "com.squareup.okhttp3:logging-interceptor:$okhttp_version"
implementation 'com.squareup.okio:okio:2.6.0' implementation "com.squareup.okhttp3:okhttp-dnsoverhttps:$okhttp_version"
implementation 'com.squareup.okio:okio:2.7.0'
// TLS 1.3 support for Android < 10
implementation 'org.conscrypt:conscrypt-android:2.4.0'
// REST // REST
final retrofit_version = '2.8.1' final retrofit_version = '2.9.0'
implementation "com.squareup.retrofit2:retrofit:$retrofit_version" implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version" implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version" implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
@ -167,30 +188,22 @@ dependencies {
// Disk // Disk
implementation 'com.jakewharton:disklrucache:2.0.2' implementation 'com.jakewharton:disklrucache:2.0.2'
implementation 'com.github.inorichi:unifile:e9ee588' implementation 'com.github.inorichi:unifile:e9ee588'
implementation 'com.github.inorichi:junrar-android:634c1f5'
// HTML parser // HTML parser
implementation 'org.jsoup:jsoup:1.13.1' implementation 'org.jsoup:jsoup:1.13.1'
// Job scheduling
final work_version = '2.3.4'
implementation "androidx.work:work-runtime:$work_version"
implementation "androidx.work:work-runtime-ktx:$work_version"
// Changelog
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
// Database // Database
implementation 'androidx.sqlite:sqlite:2.1.0' implementation 'androidx.sqlite:sqlite:2.1.0'
implementation 'com.github.inorichi.storio:storio-common:8be19de@aar' implementation 'com.github.inorichi.storio:storio-common:8be19de@aar'
implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar' implementation 'com.github.inorichi.storio:storio-sqlite:8be19de@aar'
implementation 'io.requery:sqlite-android:3.31.0' implementation 'io.requery:sqlite-android:3.32.2'
// Preferences // Preferences
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1' implementation 'com.github.tfcporciuncula:flow-preferences:1.1.1'
// Model View Presenter // Model View Presenter
final nucleus_version = '6.0.0' final nucleus_version = '3.0.0'
implementation "info.android15.nucleus:nucleus:$nucleus_version" implementation "info.android15.nucleus:nucleus:$nucleus_version"
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version" implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
@ -203,12 +216,13 @@ dependencies {
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version" implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version" kapt "com.github.bumptech.glide:compiler:$glide_version"
implementation 'com.github.tachiyomiorg:subsampling-scale-image-view:bff2806'
// Logging // Logging
implementation 'com.jakewharton.timber:timber:4.7.1' implementation 'com.jakewharton.timber:timber:4.7.1'
// Crash reports // Crash reports
final acra_version = '5.5.0' implementation 'ch.acra:acra-http:5.7.0'
implementation "ch.acra:acra-http:$acra_version"
// Sort // Sort
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1' implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
@ -235,16 +249,21 @@ dependencies {
implementation("com.bluelinelabs:conductor-support:2.1.5") { implementation("com.bluelinelabs:conductor-support:2.1.5") {
exclude group: "com.android.support" exclude group: "com.android.support"
} }
implementation 'com.github.inorichi:conductor-support-preference:a32c357' implementation 'com.github.tachiyomiorg:conductor-support-preference:1.1.1'
// FlowBinding // FlowBinding
final flowbinding_version = '0.11.1' final flowbinding_version = '0.12.0'
implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-android:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-appcompat:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-recyclerview:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version" implementation "io.github.reactivecircus.flowbinding:flowbinding-swiperefreshlayout:$flowbinding_version"
implementation "io.github.reactivecircus.flowbinding:flowbinding-viewpager:$flowbinding_version" 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"
// Tests // Tests
testImplementation 'junit:junit:4.13' testImplementation 'junit:junit:4.13'
testImplementation 'org.assertj:assertj-core:3.12.2' testImplementation 'org.assertj:assertj-core:3.12.2'
@ -257,14 +276,17 @@ dependencies {
implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version" implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk8:$kotlin_version"
final coroutines_version = '1.3.5' // 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'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version" implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
implementation 'com.google.android.gms:play-services-oss-licenses:17.0.0'
// For detecting memory leaks; see https://square.github.io/leakcanary/ // For detecting memory leaks; see https://square.github.io/leakcanary/
// debugImplementation 'com.squareup.leakcanary:leakcanary-android:2.2' // 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 { buildscript {
@ -282,7 +304,7 @@ repositories {
} }
// See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers // See https://kotlinlang.org/docs/reference/experimental.html#experimental-status-of-experimental-api-markers
tasks.withType(org.jetbrains.kotlin.gradle.tasks.AbstractKotlinCompile).all { tasks.withType(AbstractKotlinCompile).all {
kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.Experimental"] kotlinOptions.freeCompilerArgs += ["-Xopt-in=kotlin.Experimental"]
} }

View File

@ -12,7 +12,6 @@
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" /> <uses-permission android:name="android.permission.REQUEST_DELETE_PACKAGES" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" /> <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<application <application
android:name=".App" android:name=".App"
@ -119,13 +118,6 @@
android:name=".extension.util.ExtensionInstallActivity" android:name=".extension.util.ExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar" /> android:theme="@android:style/Theme.Translucent.NoTitleBar" />
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesMenuActivity"
android:theme="@style/Theme.MaterialComponents" />
<activity
android:name="com.google.android.gms.oss.licenses.OssLicensesActivity"
android:theme="@style/Theme.MaterialComponents" />
<provider <provider
android:name="androidx.core.content.FileProvider" android:name="androidx.core.content.FileProvider"
android:authorities="${applicationId}.provider" android:authorities="${applicationId}.provider"

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi
import android.app.Application import android.app.Application
import android.content.Context import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.os.Build
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleObserver import androidx.lifecycle.LifecycleObserver
import androidx.lifecycle.OnLifecycleEvent import androidx.lifecycle.OnLifecycleEvent
@ -12,10 +13,12 @@ import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import java.security.Security
import org.acra.ACRA import org.acra.ACRA
import org.acra.annotation.AcraCore import org.acra.annotation.AcraCore
import org.acra.annotation.AcraHttpSender import org.acra.annotation.AcraHttpSender
import org.acra.sender.HttpSender import org.acra.sender.HttpSender
import org.conscrypt.Conscrypt
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope import uy.kohesive.injekt.api.InjektScope
@ -36,6 +39,20 @@ open class App : Application(), LifecycleObserver {
super.onCreate() super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree()) if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
// Debug tool; see https://fbflipper.com/
// SoLoader.init(this, false)
// if (BuildConfig.DEBUG && FlipperUtils.shouldEnableFlipper(this)) {
// val client = AndroidFlipperClient.getInstance(this)
// client.addPlugin(InspectorFlipperPlugin(this, DescriptorMapping.withDefaults()))
// client.addPlugin(DatabasesFlipperPlugin(this))
// client.start()
// }
// TLS 1.3 support for Android < 10
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
Security.insertProviderAt(Conscrypt.newProvider(), 1)
}
Injekt = InjektScope(DefaultRegistrar()) Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this)) Injekt.importModule(AppModule(this))

View File

@ -7,8 +7,10 @@ import android.net.Uri
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
/** /**
@ -69,14 +71,11 @@ class BackupCreateService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notifier = BackupNotifier(this) notifier = BackupNotifier(this)
wakeLock = acquireWakeLock(javaClass.name)
startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build()) startForeground(Notifications.ID_BACKUP_PROGRESS, notifier.showBackupProgress().build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock"
)
wakeLock.acquire()
} }
override fun stopService(name: Intent?): Boolean { override fun stopService(name: Intent?): Boolean {
@ -108,7 +107,7 @@ class BackupCreateService : Service() {
val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0) val backupFlags = intent.getIntExtra(BackupConst.EXTRA_FLAGS, 0)
backupManager = BackupManager(this) backupManager = BackupManager(this)
val backupFileUri = Uri.parse(backupManager.createBackup(uri, backupFlags, false)) val backupFileUri = backupManager.createBackup(uri, backupFlags, false)?.toUri()
val unifile = UniFile.fromUri(this, backupFileUri) val unifile = UniFile.fromUri(this, backupFileUri)
notifier.showBackupComplete(unifile) notifier.showBackupComplete(unifile)
} catch (e: Exception) { } catch (e: Exception) {

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.backup package eu.kanade.tachiyomi.data.backup
import android.content.Context import android.content.Context
import android.net.Uri import androidx.core.net.toUri
import androidx.work.ExistingPeriodicWorkPolicy import androidx.work.ExistingPeriodicWorkPolicy
import androidx.work.PeriodicWorkRequestBuilder import androidx.work.PeriodicWorkRequestBuilder
import androidx.work.WorkManager import androidx.work.WorkManager
@ -18,7 +18,7 @@ class BackupCreatorJob(private val context: Context, workerParams: WorkerParamet
override fun doWork(): Result { override fun doWork(): Result {
val preferences = Injekt.get<PreferencesHelper>() val preferences = Injekt.get<PreferencesHelper>()
val backupManager = BackupManager(context) val backupManager = BackupManager(context)
val uri = Uri.parse(preferences.backupsDirectory().get()) val uri = preferences.backupsDirectory().get().toUri()
val flags = BackupCreateService.BACKUP_ALL val flags = BackupCreateService.BACKUP_ALL
return try { return try {
backupManager.createBackup(uri, flags, true) backupManager.createBackup(uri, flags, true)

View File

@ -46,6 +46,7 @@ import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
@ -131,8 +132,10 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
mangaEntries.add(backupMangaObject(manga, flags)) mangaEntries.add(backupMangaObject(manga, flags))
// Maintain set of extensions/sources used (excludes local source) // Maintain set of extensions/sources used (excludes local source)
if (manga.source != 0L && sourceManager.get(manga.source) != null) { if (manga.source != LocalSource.ID) {
extensions.add("${manga.source}:${sourceManager.get(manga.source)!!.name}") sourceManager.get(manga.source)?.let {
extensions.add("${manga.source}:${it.name}")
}
} }
} }
@ -322,7 +325,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
for (dbCategory in dbCategories) { for (dbCategory in dbCategories) {
// If the category is already in the db, assign the id to the file's category // If the category is already in the db, assign the id to the file's category
// and do nothing // and do nothing
if (category.nameLower == dbCategory.nameLower) { if (category.name == dbCategory.name) {
category.id = dbCategory.id category.id = dbCategory.id
found = true found = true
break break
@ -347,10 +350,10 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*/ */
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) { internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
val dbCategories = databaseHelper.getCategories().executeAsBlocking() val dbCategories = databaseHelper.getCategories().executeAsBlocking()
val mangaCategoriesToUpdate = ArrayList<MangaCategory>() val mangaCategoriesToUpdate = mutableListOf<MangaCategory>()
for (backupCategoryStr in categories) { for (backupCategoryStr in categories) {
for (dbCategory in dbCategories) { for (dbCategory in dbCategories) {
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) { if (backupCategoryStr == dbCategory.name) {
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory)) mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
break break
} }
@ -359,9 +362,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
// Update database // Update database
if (mangaCategoriesToUpdate.isNotEmpty()) { if (mangaCategoriesToUpdate.isNotEmpty()) {
val mangaAsList = ArrayList<Manga>() databaseHelper.deleteOldMangasCategories(listOf(manga)).executeAsBlocking()
mangaAsList.add(manga)
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking() databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
} }
} }
@ -373,7 +374,7 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*/ */
internal fun restoreHistoryForManga(history: List<DHistory>) { internal fun restoreHistoryForManga(history: List<DHistory>) {
// List containing history to be updated // List containing history to be updated
val historyToBeUpdated = ArrayList<History>() val historyToBeUpdated = mutableListOf<History>()
for ((url, lastRead) in history) { for ((url, lastRead) in history) {
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking() val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
// Check if history already in database and update // Check if history already in database and update
@ -407,9 +408,9 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
// Get tracks from database // Get tracks from database
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking() val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
val trackToUpdate = ArrayList<Track>() val trackToUpdate = mutableListOf<Track>()
for (track in tracks) { tracks.forEach { track ->
val service = trackManager.getService(track.sync_id) val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) { if (service != null && service.isLogged) {
var isInDatabase = false var isInDatabase = false

View File

@ -41,6 +41,7 @@ internal class BackupNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.creating_backup)) setContentTitle(context.getString(R.string.creating_backup))
setProgress(0, 0, true) setProgress(0, 0, true)
setOnlyAlertOnce(true)
} }
builder.show(Notifications.ID_BACKUP_PROGRESS) builder.show(Notifications.ID_BACKUP_PROGRESS)
@ -93,6 +94,7 @@ internal class BackupNotifier(private val context: Context) {
} }
setProgress(maxAmount, progress, false) setProgress(maxAmount, progress, false)
setOnlyAlertOnce(true)
// Clear old actions if they exist // Clear old actions if they exist
if (mActions.isNotEmpty()) { if (mActions.isNotEmpty()) {
@ -135,7 +137,7 @@ internal class BackupNotifier(private val context: Context) {
with(completeNotificationBuilder) { with(completeNotificationBuilder) {
setContentTitle(context.getString(R.string.restore_completed)) setContentTitle(context.getString(R.string.restore_completed))
setContentText(context.getString(R.string.restore_completed_content, timeString, errorCount)) setContentText(context.resources.getQuantityString(R.plurals.restore_completed_message, errorCount, timeString, errorCount))
// Clear old actions if they exist // Clear old actions if they exist
if (mActions.isNotEmpty()) { if (mActions.isNotEmpty()) {

View File

@ -32,6 +32,7 @@ import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning import eu.kanade.tachiyomi.util.system.isServiceRunning
import java.io.File import java.io.File
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
@ -109,6 +110,11 @@ class BackupRestoreService : Service() {
*/ */
private var restoreAmount = 0 private var restoreAmount = 0
/**
* Mapping of source ID to source name from backup data
*/
private var sourceMapping: Map<Long, String> = emptyMap()
/** /**
* List containing errors * List containing errors
*/ */
@ -123,14 +129,11 @@ class BackupRestoreService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
notifier = BackupNotifier(this) notifier = BackupNotifier(this)
wakeLock = acquireWakeLock(javaClass.name)
startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build()) startForeground(Notifications.ID_RESTORE_PROGRESS, notifier.showRestoreProgress().build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "${javaClass.name}:WakeLock"
)
wakeLock.acquire()
} }
override fun stopService(name: Intent?): Boolean { override fun stopService(name: Intent?): Boolean {
@ -177,7 +180,9 @@ class BackupRestoreService : Service() {
stopSelf(startId) stopSelf(startId)
} }
job = GlobalScope.launch(handler) { job = GlobalScope.launch(handler) {
restoreBackup(uri) if (!restoreBackup(uri)) {
notifier.showRestoreError(getString(R.string.restoring_backup_canceled))
}
} }
job?.invokeOnCompletion { job?.invokeOnCompletion {
stopSelf(startId) stopSelf(startId)
@ -191,7 +196,7 @@ class BackupRestoreService : Service() {
* *
* @param uri backup file to restore * @param uri backup file to restore
*/ */
private fun restoreBackup(uri: Uri) { private fun restoreBackup(uri: Uri): Boolean {
val startTime = System.currentTimeMillis() val startTime = System.currentTimeMillis()
val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader()) val reader = JsonReader(contentResolver.openInputStream(uri)!!.bufferedReader())
@ -210,10 +215,17 @@ class BackupRestoreService : Service() {
errors.clear() errors.clear()
// Restore categories // Restore categories
restoreCategories(json.get(CATEGORIES)) json.get(CATEGORIES)?.let { restoreCategories(it) }
// Store source mapping for error messages
sourceMapping = BackupRestoreValidator.getSourceMapping(json)
// Restore individual manga // Restore individual manga
mangasJson.forEach { mangasJson.forEach {
if (job?.isActive != true) {
return false
}
restoreManga(it.asJsonObject) restoreManga(it.asJsonObject)
} }
@ -223,56 +235,58 @@ class BackupRestoreService : Service() {
val logFile = writeErrorLog() val logFile = writeErrorLog()
notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name) notifier.showRestoreComplete(time, errors.size, logFile.parent, logFile.name)
return true
} }
private fun restoreCategories(categoriesJson: JsonElement) { private fun restoreCategories(categoriesJson: JsonElement) {
db.inTransaction { db.inTransaction {
backupManager.restoreCategories(categoriesJson.asJsonArray) backupManager.restoreCategories(categoriesJson.asJsonArray)
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
} }
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, getString(R.string.categories))
} }
private fun restoreManga(mangaJson: JsonObject) { private fun restoreManga(mangaJson: JsonObject) {
db.inTransaction { val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA))
val manga = backupManager.parser.fromJson<MangaImpl>(mangaJson.get(MANGA)) val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>( mangaJson.get(CHAPTERS)
mangaJson.get(CHAPTERS) ?: JsonArray()
?: JsonArray() )
) val categories = backupManager.parser.fromJson<List<String>>(
val categories = backupManager.parser.fromJson<List<String>>( mangaJson.get(CATEGORIES)
mangaJson.get(CATEGORIES) ?: JsonArray()
?: JsonArray() )
) val history = backupManager.parser.fromJson<List<DHistory>>(
val history = backupManager.parser.fromJson<List<DHistory>>( mangaJson.get(HISTORY)
mangaJson.get(HISTORY) ?: JsonArray()
?: JsonArray() )
) val tracks = backupManager.parser.fromJson<List<TrackImpl>>(
val tracks = backupManager.parser.fromJson<List<TrackImpl>>( mangaJson.get(TRACK)
mangaJson.get(TRACK) ?: JsonArray()
?: JsonArray() )
)
if (job?.isActive != true) { try {
throw Exception(getString(R.string.restoring_backup_canceled)) 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) {
try { errors.add(Date() to "${manga.title} - ${e.message}")
restoreMangaData(manga, chapters, categories, history, tracks)
} catch (e: Exception) {
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
}
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
} }
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, manga.title)
} }
/** /**
* Returns a manga restore observable * Returns a manga restore observable
* *
* @param manga manga data from json * @param manga manga data from json
* @param source source to get manga data from
* @param chapters chapters data from json * @param chapters chapters data from json
* @param categories categories data from json * @param categories categories data from json
* @param history history data from json * @param history history data from json
@ -280,23 +294,24 @@ class BackupRestoreService : Service() {
*/ */
private fun restoreMangaData( private fun restoreMangaData(
manga: Manga, manga: Manga,
source: Source,
chapters: List<Chapter>, chapters: List<Chapter>,
categories: List<String>, categories: List<String>,
history: List<DHistory>, history: List<DHistory>,
tracks: List<Track> tracks: List<Track>
) { ) {
// Get source
val source = backupManager.sourceManager.getOrStub(manga.source)
val dbManga = backupManager.getMangaFromDatabase(manga) val dbManga = backupManager.getMangaFromDatabase(manga)
if (dbManga == null) { db.inTransaction {
// Manga not in database if (dbManga == null) {
restoreMangaFetch(source, manga, chapters, categories, history, tracks) // Manga not in database
} else { // Manga in database restoreMangaFetch(source, manga, chapters, categories, history, tracks)
// Copy information from manga already in database } else { // Manga in database
backupManager.restoreMangaNoFetch(manga, dbManga) // Copy information from manga already in database
// Fetch rest of manga information backupManager.restoreMangaNoFetch(manga, dbManga)
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks) // Fetch rest of manga information
restoreMangaNoFetch(source, manga, chapters, categories, history, tracks)
}
} }
} }
@ -396,7 +411,7 @@ class BackupRestoreService : Service() {
*/ */
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> { private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
return Observable.from(tracks) return Observable.from(tracks)
.concatMap { track -> .flatMap { track ->
val service = trackManager.getService(track.sync_id) val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) { if (service != null && service.isLogged) {
service.refresh(track) service.refresh(track)
@ -406,7 +421,7 @@ class BackupRestoreService : Service() {
track track
} }
} else { } else {
errors.add(Date() to "${manga.title} - ${service?.name} not logged in") errors.add(Date() to "${manga.title} - ${getString(R.string.tracker_not_logged_in, service?.name)}")
Observable.empty() Observable.empty()
} }
} }

View File

@ -0,0 +1,46 @@
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

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.cache package eu.kanade.tachiyomi.data.cache
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.util.storage.DiskUtil import eu.kanade.tachiyomi.util.storage.DiskUtil
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -17,51 +18,89 @@ import java.io.InputStream
*/ */
class CoverCache(private val context: Context) { class CoverCache(private val context: Context) {
companion object {
private const val COVERS_DIR = "covers"
private const val CUSTOM_COVERS_DIR = "covers/custom"
}
/** /**
* Cache directory used for cache management. * Cache directory used for cache management.
*/ */
private val cacheDir = context.getExternalFilesDir("covers") private val cacheDir = getCacheDir(COVERS_DIR)
?: File(context.filesDir, "covers").also { it.mkdirs() }
private val customCoverCacheDir = getCacheDir(CUSTOM_COVERS_DIR)
/** /**
* Returns the cover from cache. * Returns the cover from cache.
* *
* @param thumbnailUrl the thumbnail url. * @param manga the manga.
* @return cover image. * @return cover image.
*/ */
fun getCoverFile(thumbnailUrl: String): File { fun getCoverFile(manga: Manga): File? {
return File(cacheDir, DiskUtil.hashKeyForDisk(thumbnailUrl)) return manga.thumbnail_url?.let {
File(cacheDir, DiskUtil.hashKeyForDisk(it))
}
} }
/** /**
* Copy the given stream to this cache. * Returns the custom cover from cache.
* *
* @param thumbnailUrl url of the thumbnail. * @param manga the manga.
* @return cover image.
*/
fun getCustomCoverFile(manga: Manga): File {
return File(customCoverCacheDir, DiskUtil.hashKeyForDisk(manga.id.toString()))
}
/**
* Saves the given stream as the manga's custom cover to cache.
*
* @param manga the manga.
* @param inputStream the stream to copy. * @param inputStream the stream to copy.
* @throws IOException if there's any error. * @throws IOException if there's any error.
*/ */
@Throws(IOException::class) @Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) { fun setCustomCoverToCache(manga: Manga, inputStream: InputStream) {
// Get destination file. getCustomCoverFile(manga).outputStream().use {
val destFile = getCoverFile(thumbnailUrl) inputStream.copyTo(it)
}
destFile.outputStream().use { inputStream.copyTo(it) }
} }
/** /**
* Delete the cover file from the cache. * Delete the cover files of the manga from the cache.
* *
* @param thumbnailUrl the thumbnail url. * @param manga the manga.
* @return status of deletion. * @param deleteCustomCover whether the custom cover should be deleted.
* @return number of files that were deleted.
*/ */
fun deleteFromCache(thumbnailUrl: String?): Boolean { fun deleteFromCache(manga: Manga, deleteCustomCover: Boolean = false): Int {
// Check if url is empty. var deleted = 0
if (thumbnailUrl.isNullOrEmpty()) {
return false getCoverFile(manga)?.let {
if (it.exists() && it.delete()) ++deleted
} }
// Remove file. if (deleteCustomCover) {
val file = getCoverFile(thumbnailUrl) if (deleteCustomCover(manga)) ++deleted
return file.exists() && file.delete() }
return deleted
}
/**
* Delete custom cover of the manga from the cache
*
* @param manga the manga.
* @return whether the cover was deleted.
*/
fun deleteCustomCover(manga: Manga): Boolean {
return getCustomCoverFile(manga).let {
it.exists() && it.delete()
}
}
private fun getCacheDir(dir: String): File {
return context.getExternalFilesDir(dir)
?: File(context.filesDir, dir).also { it.mkdirs() }
} }
} }

View File

@ -20,7 +20,7 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
/** /**
* Version of the database. * Version of the database.
*/ */
const val DATABASE_VERSION = 9 const val DATABASE_VERSION = 11
} }
override fun onCreate(db: SupportSQLiteDatabase) = with(db) { override fun onCreate(db: SupportSQLiteDatabase) = with(db) {
@ -75,6 +75,13 @@ class DbOpenCallback : SupportSQLiteOpenHelper.Callback(DATABASE_VERSION) {
db.execSQL(TrackTable.addStartDate) db.execSQL(TrackTable.addStartDate)
db.execSQL(TrackTable.addFinishDate) db.execSQL(TrackTable.addFinishDate)
} }
if (oldVersion < 10) {
db.execSQL(MangaTable.addCoverLastModified)
}
if (oldVersion < 11) {
db.execSQL(MangaTable.addDateAdded)
db.execSQL(MangaTable.backfillDateAdded)
}
} }
override fun onConfigure(db: SupportSQLiteDatabase) { override fun onConfigure(db: SupportSQLiteDatabase) {

View File

@ -14,6 +14,8 @@ import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_COVER_LAST_MODIFIED
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DATE_ADDED
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
@ -46,7 +48,7 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
.whereArgs(obj.id) .whereArgs(obj.id)
.build() .build()
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply { override fun mapToContentValues(obj: Manga) = ContentValues(17).apply {
put(COL_ID, obj.id) put(COL_ID, obj.id)
put(COL_SOURCE, obj.source) put(COL_SOURCE, obj.source)
put(COL_URL, obj.url) put(COL_URL, obj.url)
@ -62,6 +64,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
put(COL_INITIALIZED, obj.initialized) put(COL_INITIALIZED, obj.initialized)
put(COL_VIEWER, obj.viewer) put(COL_VIEWER, obj.viewer)
put(COL_CHAPTER_FLAGS, obj.chapter_flags) put(COL_CHAPTER_FLAGS, obj.chapter_flags)
put(COL_COVER_LAST_MODIFIED, obj.cover_last_modified)
put(COL_DATE_ADDED, obj.date_added)
} }
} }
@ -82,6 +86,8 @@ interface BaseMangaGetResolver {
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1 initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER)) viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS)) chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
cover_last_modified = cursor.getLong(cursor.getColumnIndex(COL_COVER_LAST_MODIFIED))
date_added = cursor.getLong(cursor.getColumnIndex(COL_DATE_ADDED))
} }
} }

View File

@ -12,9 +12,6 @@ interface Category : Serializable {
var flags: Int var flags: Int
val nameLower: String
get() = name.toLowerCase()
companion object { companion object {
fun create(name: String): Category = CategoryImpl().apply { fun create(name: String): Category = CategoryImpl().apply {

View File

@ -15,7 +15,6 @@ class CategoryImpl : Category {
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false
val category = other as Category val category = other as Category
return name == category.name return name == category.name
} }

View File

@ -31,10 +31,11 @@ class ChapterImpl : Chapter {
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false
val chapter = other as Chapter val chapter = other as Chapter
return url == chapter.url if (url != chapter.url) return false
return id == chapter.id
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return url.hashCode() return url.hashCode() + id.hashCode()
} }
} }

View File

@ -12,18 +12,18 @@ interface Manga : SManga {
var last_update: Long var last_update: Long
var date_added: Long
var viewer: Int var viewer: Int
var chapter_flags: Int var chapter_flags: Int
var cover_last_modified: Long
fun setChapterOrder(order: Int) { fun setChapterOrder(order: Int) {
setFlags(order, SORT_MASK) setFlags(order, SORT_MASK)
} }
private fun setFlags(flag: Int, mask: Int) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
fun sortDescending(): Boolean { fun sortDescending(): Boolean {
return chapter_flags and SORT_MASK == SORT_DESC return chapter_flags and SORT_MASK == SORT_DESC
} }
@ -32,6 +32,10 @@ interface Manga : SManga {
return genre?.split(", ")?.map { it.trim() } return genre?.split(", ")?.map { it.trim() }
} }
private fun setFlags(flag: Int, mask: Int) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
// Used to display the chapter's title one way or another // Used to display the chapter's title one way or another
var displayMode: Int var displayMode: Int
get() = chapter_flags and DISPLAY_MASK get() = chapter_flags and DISPLAY_MASK
@ -76,7 +80,8 @@ interface Manga : SManga {
const val SORTING_SOURCE = 0x00000000 const val SORTING_SOURCE = 0x00000000
const val SORTING_NUMBER = 0x00000100 const val SORTING_NUMBER = 0x00000100
const val SORTING_MASK = 0x00000100 const val SORTING_UPLOAD_DATE = 0x00000200
const val SORTING_MASK = 0x00000300
const val DISPLAY_NAME = 0x00000000 const val DISPLAY_NAME = 0x00000000
const val DISPLAY_NUMBER = 0x00100000 const val DISPLAY_NUMBER = 0x00100000

View File

@ -26,22 +26,26 @@ open class MangaImpl : Manga {
override var last_update: Long = 0 override var last_update: Long = 0
override var date_added: Long = 0
override var initialized: Boolean = false override var initialized: Boolean = false
override var viewer: Int = 0 override var viewer: Int = 0
override var chapter_flags: Int = 0 override var chapter_flags: Int = 0
override var cover_last_modified: Long = 0
override fun equals(other: Any?): Boolean { override fun equals(other: Any?): Boolean {
if (this === other) return true if (this === other) return true
if (other == null || javaClass != other.javaClass) return false if (other == null || javaClass != other.javaClass) return false
val manga = other as Manga val manga = other as Manga
if (url != manga.url) return false
return url == manga.url return id == manga.id
} }
override fun hashCode(): Int { override fun hashCode(): Int {
return url.hashCode() return url.hashCode() + id.hashCode()
} }
} }

View File

@ -7,6 +7,7 @@ import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.LibraryManga import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaCoverLastModifiedPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
@ -102,6 +103,11 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaTitlePutResolver()) .withPutResolver(MangaTitlePutResolver())
.prepare() .prepare()
fun updateMangaCoverLastModified(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaCoverLastModifiedPutResolver())
.prepare()
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare() fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare() fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()

View File

@ -38,7 +38,9 @@ fun getRecentsQuery() =
""" """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ? WHERE ${Manga.COL_FAVORITE} = 1
AND ${Chapter.COL_DATE_UPLOAD} > ?
AND ${Chapter.COL_DATE_FETCH} > ${Manga.COL_DATE_ADDED}
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
""" """

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResolver
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class MangaCoverLastModifiedPutResolver : PutResolver<Manga>() {
override fun performPut(db: StorIOSQLite, manga: Manga) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(manga)
val contentValues = mapToContentValues(manga)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(manga: Manga) = UpdateQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(manga.id)
.build()
fun mapToContentValues(manga: Manga) = ContentValues(1).apply {
put(MangaTable.COL_COVER_LAST_MODIFIED, manga.cover_last_modified)
}
}

View File

@ -28,6 +28,8 @@ object MangaTable {
const val COL_LAST_UPDATE = "last_update" const val COL_LAST_UPDATE = "last_update"
const val COL_DATE_ADDED = "date_added"
const val COL_INITIALIZED = "initialized" const val COL_INITIALIZED = "initialized"
const val COL_VIEWER = "viewer" const val COL_VIEWER = "viewer"
@ -38,6 +40,8 @@ object MangaTable {
const val COL_CATEGORY = "category" const val COL_CATEGORY = "category"
const val COL_COVER_LAST_MODIFIED = "cover_last_modified"
val createTableQuery: String val createTableQuery: String
get() = get() =
"""CREATE TABLE $TABLE( """CREATE TABLE $TABLE(
@ -55,7 +59,9 @@ object MangaTable {
$COL_LAST_UPDATE LONG, $COL_LAST_UPDATE LONG,
$COL_INITIALIZED BOOLEAN NOT NULL, $COL_INITIALIZED BOOLEAN NOT NULL,
$COL_VIEWER INTEGER NOT NULL, $COL_VIEWER INTEGER NOT NULL,
$COL_CHAPTER_FLAGS INTEGER NOT NULL $COL_CHAPTER_FLAGS INTEGER NOT NULL,
$COL_COVER_LAST_MODIFIED LONG NOT NULL,
$COL_DATE_ADDED LONG NOT NULL
)""" )"""
val createUrlIndexQuery: String val createUrlIndexQuery: String
@ -64,4 +70,20 @@ object MangaTable {
val createLibraryIndexQuery: String val createLibraryIndexQuery: String
get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " + get() = "CREATE INDEX library_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE) " +
"WHERE $COL_FAVORITE = 1" "WHERE $COL_FAVORITE = 1"
val addCoverLastModified: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_COVER_LAST_MODIFIED LONG NOT NULL DEFAULT 0"
val addDateAdded: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_DATE_ADDED LONG NOT NULL DEFAULT 0"
/**
* Used with addDateAdded to populate it with the oldest chapter fetch date.
*/
val backfillDateAdded: String
get() = "UPDATE $TABLE SET $COL_DATE_ADDED = " +
"(SELECT MIN(${ChapterTable.COL_DATE_FETCH}) " +
"FROM $TABLE INNER JOIN ${ChapterTable.TABLE} " +
"ON $TABLE.$COL_ID = ${ChapterTable.TABLE}.${ChapterTable.COL_MANGA_ID} " +
"GROUP BY $TABLE.$COL_ID)"
} }

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.net.Uri import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@ -59,7 +59,7 @@ class DownloadCache(
*/ */
private fun getDirectoryFromPreference(): UniFile { private fun getDirectoryFromPreference(): UniFile {
val dir = preferences.downloadsDirectory().get() val dir = preferences.downloadsDirectory().get()
return UniFile.fromUri(context, Uri.parse(dir)) return UniFile.fromUri(context, dir.toUri())
} }
/** /**
@ -81,7 +81,7 @@ class DownloadCache(
if (sourceDir != null) { if (sourceDir != null) {
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
if (mangaDir != null) { if (mangaDir != null) {
return provider.getChapterDirName(chapter) in mangaDir.files return provider.getValidChapterDirNames(chapter).any { it in mangaDir.files }
} }
} }
return false return false
@ -128,7 +128,7 @@ class DownloadCache(
.orEmpty() .orEmpty()
.associate { it.name to SourceDirectory(it) } .associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry -> .mapNotNullKeys { entry ->
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id onlineSources.find { provider.getSourceDirName(it).toLowerCase() == entry.key?.toLowerCase() }?.id
} }
rootDir.files = sourceDirs rootDir.files = sourceDirs
@ -191,9 +191,10 @@ class DownloadCache(
fun removeChapter(chapter: Chapter, manga: Manga) { fun removeChapter(chapter: Chapter, manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return val sourceDir = rootDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
val chapterDirName = provider.getChapterDirName(chapter) provider.getValidChapterDirNames(chapter).forEach {
if (chapterDirName in mangaDir.files) { if (it in mangaDir.files) {
mangaDir.files -= chapterDirName mangaDir.files -= it
}
} }
} }
@ -207,10 +208,11 @@ class DownloadCache(
fun removeChapters(chapters: List<Chapter>, manga: Manga) { fun removeChapters(chapters: List<Chapter>, manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return val sourceDir = rootDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
for (chapter in chapters) { chapters.forEach { chapter ->
val chapterDirName = provider.getChapterDirName(chapter) provider.getValidChapterDirNames(chapter).forEach {
if (chapterDirName in mangaDir.files) { if (it in mangaDir.files) {
mangaDir.files -= chapterDirName mangaDir.files -= it
}
} }
} }
} }

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
@ -11,6 +12,7 @@ import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Page import eu.kanade.tachiyomi.source.model.Page
import rx.Observable import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
/** /**
@ -151,7 +153,7 @@ class DownloadManager(private val context: Context) {
.filter { "image" in it.type.orEmpty() } .filter { "image" in it.type.orEmpty() }
if (files.isEmpty()) { if (files.isEmpty()) {
throw Exception("Page list is empty") throw Exception(context.getString(R.string.page_list_empty_error))
} }
files.sortedBy { it.name } files.sortedBy { it.name }
@ -239,4 +241,30 @@ class DownloadManager(private val context: Context) {
deleteChapters(chapters, manga, source) deleteChapters(chapters, manga, source)
} }
} }
/**
* Renames an already downloaded chapter
*
* @param source the source of the manga.
* @param manga the manga of the chapter.
* @param oldChapter the existing chapter with the old name.
* @param newChapter the target chapter with the new name.
*/
fun renameChapter(source: Source, manga: Manga, oldChapter: Chapter, newChapter: Chapter) {
val oldNames = provider.getValidChapterDirNames(oldChapter)
val newName = provider.getChapterDirName(newChapter)
val mangaDir = provider.getMangaDir(manga, source)
// Assume there's only 1 version of the chapter name formats present
val oldFolder = oldNames.asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull()
if (oldFolder?.renameTo(newName) == true) {
cache.removeChapter(oldChapter, manga)
cache.addChapter(newName, mangaDir, manga)
} else {
Timber.e("Could not rename downloaded chapter: %s.", oldNames.joinToString())
}
}
} }

View File

@ -13,8 +13,8 @@ import eu.kanade.tachiyomi.util.lang.chop
import eu.kanade.tachiyomi.util.system.notificationBuilder import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager import eu.kanade.tachiyomi.util.system.notificationManager
import java.util.regex.Pattern import java.util.regex.Pattern
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
/** /**
* DownloadNotifier is used to show notifications when downloading one or multiple chapters. * DownloadNotifier is used to show notifications when downloading one or multiple chapters.
@ -23,11 +23,15 @@ import uy.kohesive.injekt.api.get
*/ */
internal class DownloadNotifier(private val context: Context) { internal class DownloadNotifier(private val context: Context) {
private val notificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER) { private val preferences: PreferencesHelper by injectLazy()
private val progressNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)) setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
} }
private val preferences by lazy { Injekt.get<PreferencesHelper>() } private val completeNotificationBuilder = context.notificationBuilder(Notifications.CHANNEL_DOWNLOADER_COMPLETE) {
setAutoCancel(false)
}
/** /**
* Status of download. Used for correct notification icon. * Status of download. Used for correct notification icon.
@ -56,7 +60,7 @@ internal class DownloadNotifier(private val context: Context) {
/** /**
* Clear old actions if they exist. * Clear old actions if they exist.
*/ */
private fun clearActions() = with(notificationBuilder) { private fun NotificationCompat.Builder.clearActions() {
if (mActions.isNotEmpty()) { if (mActions.isNotEmpty()) {
mActions.clear() mActions.clear()
} }
@ -76,8 +80,7 @@ internal class DownloadNotifier(private val context: Context) {
* @param download download object containing download information. * @param download download object containing download information.
*/ */
fun onProgressChange(download: Download) { fun onProgressChange(download: Download) {
// Create notification with(progressNotificationBuilder) {
with(notificationBuilder) {
// Check if first call. // Check if first call.
if (!isDownloading) { if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download) setSmallIcon(android.R.drawable.stat_sys_download)
@ -94,8 +97,9 @@ internal class DownloadNotifier(private val context: Context) {
) )
} }
val downloadingProgressText = context.getString(R.string.chapter_downloading_progress) val downloadingProgressText = context.getString(
.format(download.downloadedImages, download.pages!!.size) R.string.chapter_downloading_progress, download.downloadedImages, download.pages!!.size
)
if (preferences.hideNotificationContent()) { if (preferences.hideNotificationContent()) {
setContentTitle(downloadingProgressText) setContentTitle(downloadingProgressText)
@ -109,16 +113,14 @@ internal class DownloadNotifier(private val context: Context) {
setProgress(download.pages!!.size, download.downloadedImages, false) setProgress(download.pages!!.size, download.downloadedImages, false)
} }
progressNotificationBuilder.show()
// Displays the progress bar on notification
notificationBuilder.show()
} }
/** /**
* Show notification when download is paused. * Show notification when download is paused.
*/ */
fun onDownloadPaused() { fun onDownloadPaused() {
with(notificationBuilder) { with(progressNotificationBuilder) {
setContentTitle(context.getString(R.string.chapter_paused)) setContentTitle(context.getString(R.string.chapter_paused))
setContentText(context.getString(R.string.download_notifier_download_paused)) setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_pause_24dp) setSmallIcon(R.drawable.ic_pause_24dp)
@ -140,21 +142,40 @@ internal class DownloadNotifier(private val context: Context) {
NotificationReceiver.clearDownloadsPendingBroadcast(context) NotificationReceiver.clearDownloadsPendingBroadcast(context)
) )
} }
progressNotificationBuilder.show()
// Show notification.
notificationBuilder.show()
// Reset initial values // Reset initial values
isDownloading = false isDownloading = false
} }
/**
* 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)
}
completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_COMPLETE)
// Reset states to default
errorThrown = false
isDownloading = false
}
/** /**
* Called when the downloader receives a warning. * Called when the downloader receives a warning.
* *
* @param reason the text to show. * @param reason the text to show.
*/ */
fun onWarning(reason: String) { fun onWarning(reason: String) {
with(notificationBuilder) { with(completeNotificationBuilder) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title)) setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(reason) setContentText(reason)
setSmallIcon(android.R.drawable.stat_sys_warning) setSmallIcon(android.R.drawable.stat_sys_warning)
@ -163,7 +184,7 @@ internal class DownloadNotifier(private val context: Context) {
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
} }
notificationBuilder.show() completeNotificationBuilder.show()
// Reset download information // Reset download information
isDownloading = false isDownloading = false
@ -178,7 +199,7 @@ internal class DownloadNotifier(private val context: Context) {
*/ */
fun onError(error: String? = null, chapter: String? = null) { fun onError(error: String? = null, chapter: String? = null) {
// Create notification // Create notification
with(notificationBuilder) { with(completeNotificationBuilder) {
setContentTitle( setContentTitle(
chapter chapter
?: context.getString(R.string.download_notifier_downloader_title) ?: context.getString(R.string.download_notifier_downloader_title)
@ -190,7 +211,7 @@ internal class DownloadNotifier(private val context: Context) {
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context)) setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false) setProgress(0, 0, false)
} }
notificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR) completeNotificationBuilder.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
// Reset download information // Reset download information
errorThrown = true errorThrown = true

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import androidx.core.content.edit
import com.github.salomonbrys.kotson.fromJson import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson import com.google.gson.Gson
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@ -22,7 +23,7 @@ class DownloadPendingDeleter(context: Context) {
/** /**
* Preferences used to store the list of chapters to delete. * Preferences used to store the list of chapters to delete.
*/ */
private val prefs = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE) private val preferences = context.getSharedPreferences("chapters_to_delete", Context.MODE_PRIVATE)
/** /**
* Last added chapter, used to avoid decoding from the preference too often. * Last added chapter, used to avoid decoding from the preference too often.
@ -49,7 +50,7 @@ class DownloadPendingDeleter(context: Context) {
// Last entry matches the manga, reuse it to avoid decoding json from preferences // Last entry matches the manga, reuse it to avoid decoding json from preferences
lastEntry.copy(chapters = newChapters) lastEntry.copy(chapters = newChapters)
} else { } else {
val existingEntry = prefs.getString(manga.id!!.toString(), null) val existingEntry = preferences.getString(manga.id!!.toString(), null)
if (existingEntry != null) { if (existingEntry != null) {
// Existing entry found on preferences, decode json and add the new chapter // Existing entry found on preferences, decode json and add the new chapter
val savedEntry = gson.fromJson<Entry>(existingEntry) val savedEntry = gson.fromJson<Entry>(existingEntry)
@ -69,7 +70,9 @@ class DownloadPendingDeleter(context: Context) {
// Save current state // Save current state
val json = gson.toJson(newEntry) val json = gson.toJson(newEntry)
prefs.edit().putString(newEntry.manga.id.toString(), json).apply() preferences.edit {
putString(newEntry.manga.id.toString(), json)
}
lastAddedEntry = newEntry lastAddedEntry = newEntry
} }
@ -82,7 +85,9 @@ class DownloadPendingDeleter(context: Context) {
@Synchronized @Synchronized
fun getPendingChapters(): Map<Manga, List<Chapter>> { fun getPendingChapters(): Map<Manga, List<Chapter>> {
val entries = decodeAll() val entries = decodeAll()
prefs.edit().clear().apply() preferences.edit {
clear()
}
lastAddedEntry = null lastAddedEntry = null
return entries.associate { entry -> return entries.associate { entry ->
@ -94,7 +99,7 @@ class DownloadPendingDeleter(context: Context) {
* Decodes all the chapters from preferences. * Decodes all the chapters from preferences.
*/ */
private fun decodeAll(): List<Entry> { private fun decodeAll(): List<Entry> {
return prefs.all.values.mapNotNull { rawEntry -> return preferences.all.values.mapNotNull { rawEntry ->
try { try {
(rawEntry as? String)?.let { gson.fromJson<Entry>(it) } (rawEntry as? String)?.let { gson.fromJson<Entry>(it) }
} catch (e: Exception) { } catch (e: Exception) {
@ -130,7 +135,8 @@ class DownloadPendingDeleter(context: Context) {
private data class ChapterEntry( private data class ChapterEntry(
val id: Long, val id: Long,
val url: String, val url: String,
val name: String val name: String,
val scanlator: String?
) )
/** /**
@ -154,7 +160,7 @@ class DownloadPendingDeleter(context: Context) {
* Returns a chapter entry from a chapter model. * Returns a chapter entry from a chapter model.
*/ */
private fun Chapter.toEntry(): ChapterEntry { private fun Chapter.toEntry(): ChapterEntry {
return ChapterEntry(id!!, url, name) return ChapterEntry(id!!, url, name, scanlator)
} }
/** /**
@ -174,6 +180,7 @@ class DownloadPendingDeleter(context: Context) {
it.id = id it.id = id
it.url = url it.url = url
it.name = name it.name = name
it.scanlator = scanlator
} }
} }
} }

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import android.net.Uri import androidx.core.net.toUri
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@ -32,14 +32,14 @@ class DownloadProvider(private val context: Context) {
* The root directory for downloads. * The root directory for downloads.
*/ */
private var downloadsDir = preferences.downloadsDirectory().get().let { private var downloadsDir = preferences.downloadsDirectory().get().let {
val dir = UniFile.fromUri(context, Uri.parse(it)) val dir = UniFile.fromUri(context, it.toUri())
DiskUtil.createNoMediaFile(dir, context) DiskUtil.createNoMediaFile(dir, context)
dir dir
} }
init { init {
preferences.downloadsDirectory().asFlow() preferences.downloadsDirectory().asFlow()
.onEach { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) } .onEach { downloadsDir = UniFile.fromUri(context, it.toUri()) }
.launchIn(scope) .launchIn(scope)
} }
@ -88,7 +88,9 @@ class DownloadProvider(private val context: Context) {
*/ */
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? { fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
val mangaDir = findMangaDir(manga, source) val mangaDir = findMangaDir(manga, source)
return mangaDir?.findFile(getChapterDirName(chapter)) return getValidChapterDirNames(chapter).asSequence()
.mapNotNull { mangaDir?.findFile(it) }
.firstOrNull()
} }
/** /**
@ -100,7 +102,11 @@ class DownloadProvider(private val context: Context) {
*/ */
fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> { fun findChapterDirs(chapters: List<Chapter>, manga: Manga, source: Source): List<UniFile> {
val mangaDir = findMangaDir(manga, source) ?: return emptyList() val mangaDir = findMangaDir(manga, source) ?: return emptyList()
return chapters.mapNotNull { mangaDir.findFile(getChapterDirName(it)) } return chapters.mapNotNull { chapter ->
getValidChapterDirNames(chapter).asSequence()
.mapNotNull { mangaDir.findFile(it) }
.firstOrNull()
}
} }
/** /**
@ -127,6 +133,25 @@ class DownloadProvider(private val context: Context) {
* @param chapter the chapter to query. * @param chapter the chapter to query.
*/ */
fun getChapterDirName(chapter: Chapter): String { fun getChapterDirName(chapter: Chapter): String {
return DiskUtil.buildValidFilename(chapter.name) return DiskUtil.buildValidFilename(
when {
chapter.scanlator != null -> "${chapter.scanlator}_${chapter.name}"
else -> chapter.name
}
)
}
/**
* Returns valid downloaded chapter directory names.
*
* @param chapter the chapter to query.
*/
fun getValidChapterDirNames(chapter: Chapter): List<String> {
return listOf(
getChapterDirName(chapter),
// Legacy chapter directory name used in v0.9.2 and before
DiskUtil.buildValidFilename(chapter.name)
)
} }
} }

View File

@ -4,6 +4,7 @@ import android.app.Notification
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.net.ConnectivityManager
import android.net.NetworkInfo.State.CONNECTED import android.net.NetworkInfo.State.CONNECTED
import android.net.NetworkInfo.State.DISCONNECTED import android.net.NetworkInfo.State.DISCONNECTED
import android.os.Build import android.os.Build
@ -16,9 +17,9 @@ import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.lang.plusAssign import eu.kanade.tachiyomi.util.lang.plusAssign
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.connectivityManager import eu.kanade.tachiyomi.util.system.connectivityManager
import eu.kanade.tachiyomi.util.system.notification import eu.kanade.tachiyomi.util.system.notification
import eu.kanade.tachiyomi.util.system.powerManager
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
@ -70,9 +71,7 @@ class DownloadService : Service() {
/** /**
* Wake lock to prevent the device to enter sleep mode. * Wake lock to prevent the device to enter sleep mode.
*/ */
private val wakeLock by lazy { private lateinit var wakeLock: PowerManager.WakeLock
powerManager.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "DownloadService:WakeLock")
}
/** /**
* Subscriptions to store while the service is running. * Subscriptions to store while the service is running.
@ -85,6 +84,7 @@ class DownloadService : Service() {
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification()) startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
wakeLock = acquireWakeLock(javaClass.name)
runningRelay.call(true) runningRelay.call(true)
subscriptions = CompositeSubscription() subscriptions = CompositeSubscription()
listenDownloaderState() listenDownloaderState()
@ -144,7 +144,7 @@ class DownloadService : Service() {
private fun onNetworkStateChanged(connectivity: Connectivity) { private fun onNetworkStateChanged(connectivity: Connectivity) {
when (connectivity.state) { when (connectivity.state) {
CONNECTED -> { CONNECTED -> {
if (preferences.downloadOnlyOverWifi() && connectivityManager.isActiveNetworkMetered) { if (preferences.downloadOnlyOverWifi() && connectivityManager.activeNetworkInfo?.type != ConnectivityManager.TYPE_WIFI) {
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi)) downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
} else { } else {
val started = downloadManager.startDownloads() val started = downloadManager.startDownloads()
@ -176,19 +176,19 @@ class DownloadService : Service() {
/** /**
* Releases the wake lock if it's held. * Releases the wake lock if it's held.
*/ */
fun PowerManager.WakeLock.releaseIfNeeded() { private fun PowerManager.WakeLock.releaseIfNeeded() {
if (isHeld) release() if (isHeld) release()
} }
/** /**
* Acquires the wake lock if it's not held. * Acquires the wake lock if it's not held.
*/ */
fun PowerManager.WakeLock.acquireIfNeeded() { private fun PowerManager.WakeLock.acquireIfNeeded() {
if (!isHeld) acquire() if (!isHeld) acquire()
} }
private fun getPlaceholderNotification(): Notification { private fun getPlaceholderNotification(): Notification {
return notification(Notifications.CHANNEL_DOWNLOADER) { return notification(Notifications.CHANNEL_DOWNLOADER_PROGRESS) {
setContentTitle(getString(R.string.download_notifier_downloader_title)) setContentTitle(getString(R.string.download_notifier_downloader_title))
} }
} }

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.download package eu.kanade.tachiyomi.data.download
import android.content.Context import android.content.Context
import androidx.core.content.edit
import com.google.gson.Gson import com.google.gson.Gson
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@ -42,9 +43,9 @@ class DownloadStore(
* @param downloads the list of downloads to add. * @param downloads the list of downloads to add.
*/ */
fun addAll(downloads: List<Download>) { fun addAll(downloads: List<Download>) {
val editor = preferences.edit() preferences.edit {
downloads.forEach { editor.putString(getKey(it), serialize(it)) } downloads.forEach { putString(getKey(it), serialize(it)) }
editor.apply() }
} }
/** /**
@ -53,14 +54,18 @@ class DownloadStore(
* @param download the download to remove. * @param download the download to remove.
*/ */
fun remove(download: Download) { fun remove(download: Download) {
preferences.edit().remove(getKey(download)).apply() preferences.edit {
remove(getKey(download))
}
} }
/** /**
* Removes all the downloads from the store. * Removes all the downloads from the store.
*/ */
fun clear() { fun clear() {
preferences.edit().clear().apply() preferences.edit {
clear()
}
} }
/** /**

View File

@ -5,6 +5,7 @@ import android.webkit.MimeTypeMap
import com.hippo.unifile.UniFile import com.hippo.unifile.UniFile
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
@ -116,6 +117,8 @@ class Downloader(
val pending = queue.filter { it.status != Download.DOWNLOADED } val pending = queue.filter { it.status != Download.DOWNLOADED }
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE } pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
notifier.paused = false
downloadsRelay.call(pending) downloadsRelay.call(pending)
return pending.isNotEmpty() return pending.isNotEmpty()
} }
@ -136,7 +139,7 @@ class Downloader(
notifier.paused = false notifier.paused = false
notifier.onDownloadPaused() notifier.onDownloadPaused()
} else { } else {
notifier.dismiss() notifier.downloadFinished()
} }
} }
} }
@ -181,8 +184,17 @@ class Downloader(
subscriptions.clear() subscriptions.clear()
subscriptions += downloadsRelay.concatMapIterable { it } subscriptions += downloadsRelay.concatMapIterable { it }
.concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) } // Concurrently download from 5 different sources
.onBackpressureBuffer() .groupBy { it.source }
.flatMap(
{ bySource ->
bySource.concatMap { download ->
downloadChapter(download).subscribeOn(Schedulers.io())
}
},
5
)
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe( .subscribe(
{ {
@ -219,13 +231,9 @@ class Downloader(
val wasEmpty = queue.isEmpty() val wasEmpty = queue.isEmpty()
// Called in background thread, the operation can be slow with SAF. // Called in background thread, the operation can be slow with SAF.
val chaptersWithoutDir = async { val chaptersWithoutDir = async {
val mangaDir = provider.findMangaDir(manga, source)
chapters chapters
// Avoid downloading chapters with the same name.
.distinctBy { it.name }
// Filter out those already downloaded. // Filter out those already downloaded.
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null } .filter { provider.findChapterDir(it, manga, source) == null }
// Add chapters to queue from the start. // Add chapters to queue from the start.
.sortedByDescending { it.source_order } .sortedByDescending { it.source_order }
} }
@ -260,6 +268,13 @@ class Downloader(
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer { private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
val chapterDirname = provider.getChapterDirName(download.chapter) val chapterDirname = provider.getChapterDirName(download.chapter)
val mangaDir = provider.getMangaDir(download.manga, download.source) val mangaDir = provider.getMangaDir(download.manga, download.source)
if (DiskUtil.getAvailableStorageSpace(mangaDir) < 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 tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX) val tmpDir = mangaDir.createDirectory(chapterDirname + TMP_DIR_SUFFIX)
val pageListObservable = if (download.pages == null) { val pageListObservable = if (download.pages == null) {
@ -267,7 +282,7 @@ class Downloader(
download.source.fetchPageList(download.chapter) download.source.fetchPageList(download.chapter)
.doOnNext { pages -> .doOnNext { pages ->
if (pages.isEmpty()) { if (pages.isEmpty()) {
throw Exception("Page list is empty") throw Exception(context.getString(R.string.page_list_empty_error))
} }
download.pages = pages download.pages = pages
} }
@ -289,7 +304,9 @@ class Downloader(
// Get all the URLs to the source images, fetch pages if necessary // Get all the URLs to the source images, fetch pages if necessary
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) } .flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
// Start downloading images, consider we can have downloaded images already // Start downloading images, consider we can have downloaded images already
.concatMap { page -> getOrDownloadImage(page, download, tmpDir) } // Concurrently do 5 pages at a time
.flatMap({ page -> getOrDownloadImage(page, download, tmpDir) }, 5)
.onBackpressureLatest()
// Do when page is downloaded. // Do when page is downloaded.
.doOnNext { notifier.onProgressChange(download) } .doOnNext { notifier.onProgressChange(download) }
.toList() .toList()
@ -475,5 +492,8 @@ class Downloader(
companion object { companion object {
const val TMP_DIR_SUFFIX = "_tmp" const val TMP_DIR_SUFFIX = "_tmp"
// Arbitrary minimum required space to start a download: 50 MB
const val MIN_DISK_SPACE = 50 * 1024 * 1024
} }
} }

View File

@ -12,7 +12,7 @@ import java.io.IOException
import java.io.InputStream import java.io.InputStream
import timber.log.Timber import timber.log.Timber
open class FileFetcher(private val file: File) : DataFetcher<InputStream> { open class FileFetcher(private val filePath: String = "") : DataFetcher<InputStream> {
private var data: InputStream? = null private var data: InputStream? = null
@ -20,7 +20,11 @@ open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
loadFromFile(callback) loadFromFile(callback)
} }
protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) { private fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
loadFromFile(File(filePath), callback)
}
protected fun loadFromFile(file: File, callback: DataFetcher.DataCallback<in InputStream>) {
try { try {
data = FileInputStream(file) data = FileInputStream(file)
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {

View File

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.InputStream
import java.lang.Exception
open class LibraryMangaCustomCoverFetcher(
private val manga: Manga,
private val coverCache: CoverCache
) : FileFetcher() {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
getCustomCoverFile()?.let {
loadFromFile(it, callback)
} ?: callback.onLoadFailed(Exception("Custom cover file not found"))
}
protected fun getCustomCoverFile(): File? {
return coverCache.getCustomCoverFile(manga).takeIf { it.exists() }
}
}

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File import java.io.File
import java.io.FileNotFoundException import java.io.FileNotFoundException
@ -19,31 +20,41 @@ import java.io.InputStream
class LibraryMangaUrlFetcher( class LibraryMangaUrlFetcher(
private val networkFetcher: DataFetcher<InputStream>, private val networkFetcher: DataFetcher<InputStream>,
private val manga: Manga, private val manga: Manga,
private val file: File private val coverCache: CoverCache
) : ) : LibraryMangaCustomCoverFetcher(manga, coverCache) {
FileFetcher(file) {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) { override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
if (!file.exists()) { getCustomCoverFile()?.let {
loadFromFile(it, callback)
return
}
val cover = coverCache.getCoverFile(manga)
if (cover == null) {
callback.onLoadFailed(Exception("Null thumbnail url"))
return
}
if (!cover.exists()) {
networkFetcher.loadData( networkFetcher.loadData(
priority, priority,
object : DataFetcher.DataCallback<InputStream> { object : DataFetcher.DataCallback<InputStream> {
override fun onDataReady(data: InputStream?) { override fun onDataReady(data: InputStream?) {
if (data != null) { if (data != null) {
val tmpFile = File(file.path + ".tmp") val tmpFile = File(cover.path + ".tmp")
try { try {
// Retrieve destination stream, create parent folders if needed. // Retrieve destination stream, create parent folders if needed.
val output = try { val output = try {
tmpFile.outputStream() tmpFile.outputStream()
} catch (e: FileNotFoundException) { } catch (e: FileNotFoundException) {
tmpFile.parentFile.mkdirs() tmpFile.parentFile!!.mkdirs()
tmpFile.outputStream() tmpFile.outputStream()
} }
// Copy the file and rename to the original. // Copy the file and rename to the original.
data.use { output.use { data.copyTo(output) } } data.use { output.use { data.copyTo(output) } }
tmpFile.renameTo(file) tmpFile.renameTo(cover)
loadFromFile(callback) loadFromFile(cover, callback)
} catch (e: Exception) { } catch (e: Exception) {
tmpFile.delete() tmpFile.delete()
callback.onLoadFailed(e) callback.onLoadFailed(e)
@ -59,7 +70,7 @@ class LibraryMangaUrlFetcher(
} }
) )
} else { } else {
loadFromFile(callback) loadFromFile(cover, callback)
} }
} }

View File

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.load.Key
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.security.MessageDigest
class MangaSignature(manga: Manga, file: File) : Key {
private val key = manga.thumbnail_url + file.lastModified()
override fun equals(other: Any?): Boolean {
return if (other is MangaSignature) {
key == other.key
} else {
false
}
}
override fun hashCode(): Int {
return key.hashCode()
}
override fun updateDiskCacheKey(md: MessageDigest) {
md.update(key.toByteArray(Key.CHARSET))
}
}

View File

@ -1,7 +1,15 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.load.Key
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import java.security.MessageDigest
data class MangaThumbnail(val manga: Manga, val url: String?) data class MangaThumbnail(val manga: Manga, val coverLastModified: Long) : Key {
val key = manga.url + coverLastModified
fun Manga.toMangaThumbnail() = MangaThumbnail(this, this.thumbnail_url) override fun updateDiskCacheKey(messageDigest: MessageDigest) {
messageDigest.update(key.toByteArray(Key.CHARSET))
}
}
fun Manga.toMangaThumbnail() = MangaThumbnail(this, cover_last_modified)

View File

@ -1,6 +1,5 @@
package eu.kanade.tachiyomi.data.glide package eu.kanade.tachiyomi.data.glide
import android.util.LruCache
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
import com.bumptech.glide.load.Options import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.GlideUrl import com.bumptech.glide.load.model.GlideUrl
@ -14,7 +13,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import java.io.File import eu.kanade.tachiyomi.util.isLocal
import java.io.InputStream import java.io.InputStream
import uy.kohesive.injekt.Injekt import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
@ -48,12 +47,6 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
*/ */
private val defaultClient = Injekt.get<NetworkHelper>().client private val defaultClient = Injekt.get<NetworkHelper>().client
/**
* LRU cache whose key is the thumbnail url of the manga, and the value contains the request url
* and the file where it should be stored in case the manga is a favorite.
*/
private val lruCache = LruCache<GlideUrl, File>(100)
/** /**
* Map where request headers are stored for a source. * Map where request headers are stored for a source.
*/ */
@ -78,7 +71,7 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
/** /**
* Returns a fetcher for the given manga or null if the url is empty. * Returns a fetcher for the given manga or null if the url is empty.
* *
* @param manga the model. * @param mangaThumbnail the model.
* @param width the width of the view where the resource will be loaded. * @param width the width of the view where the resource will be loaded.
* @param height the height of the view where the resource will be loaded. * @param height the height of the view where the resource will be loaded.
*/ */
@ -88,13 +81,16 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
height: Int, height: Int,
options: Options options: Options
): ModelLoader.LoadData<InputStream>? { ): ModelLoader.LoadData<InputStream>? {
// Check thumbnail is not null or empty
val url = mangaThumbnail.url
if (url == null || url.isEmpty()) {
return null
}
val manga = mangaThumbnail.manga val manga = mangaThumbnail.manga
val url = manga.thumbnail_url
if (url.isNullOrEmpty()) {
return if (!manga.favorite || manga.isLocal()) {
null
} else {
ModelLoader.LoadData(mangaThumbnail, LibraryMangaCustomCoverFetcher(manga, coverCache))
}
}
if (url.startsWith("http", true)) { if (url.startsWith("http", true)) {
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source) as? HttpSource
@ -107,19 +103,13 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
return ModelLoader.LoadData(glideUrl, networkFetcher) return ModelLoader.LoadData(glideUrl, networkFetcher)
} }
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache. val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, coverCache)
val file = lruCache.getOrPut(glideUrl) { coverCache.getCoverFile(url) }
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file)
// Return an instance of the fetcher providing the needed elements. // Return an instance of the fetcher providing the needed elements.
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher) return ModelLoader.LoadData(mangaThumbnail, libraryFetcher)
} else { } else {
// Get the file from the url, removing the scheme if present.
val file = File(url.substringAfter("file://"))
// Return an instance of the fetcher providing the needed elements. // Return an instance of the fetcher providing the needed elements.
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file)) return ModelLoader.LoadData(mangaThumbnail, FileFetcher(url.removePrefix("file://")))
} }
} }
@ -141,15 +131,4 @@ class MangaThumbnailModelLoader : ModelLoader<MangaThumbnail, InputStream> {
}.build() }.build()
} }
} }
private inline fun <K, V> LruCache<K, V>.getOrPut(key: K, defaultValue: () -> V): V {
val value = get(key)
return if (value == null) {
val answer = defaultValue()
put(key, answer)
answer
} else {
value
}
}
} }

View File

@ -36,12 +36,20 @@ class TachiGlideModule : AppGlideModule() {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) { override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client) val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory) registry.replace(
registry.append(MangaThumbnail::class.java, InputStream::class.java, MangaThumbnailModelLoader.Factory()) GlideUrl::class.java,
InputStream::class.java,
networkFactory
)
registry.append( registry.append(
InputStream::class.java, InputStream::class.java, MangaThumbnail::class.java,
PassthroughModelLoader InputStream::class.java,
.Factory() MangaThumbnailModelLoader.Factory()
)
registry.append(
InputStream::class.java,
InputStream::class.java,
PassthroughModelLoader.Factory()
) )
} }
} }

View File

@ -17,8 +17,11 @@ class LibraryUpdateJob(private val context: Context, workerParams: WorkerParamet
Worker(context, workerParams) { Worker(context, workerParams) {
override fun doWork(): Result { override fun doWork(): Result {
LibraryUpdateService.start(context) return if (LibraryUpdateService.start(context)) {
return Result.success() Result.success()
} else {
Result.failure()
}
} }
companion object { companion object {

View File

@ -0,0 +1,306 @@
package eu.kanade.tachiyomi.data.library
import android.app.Notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.net.Uri
import androidx.core.app.NotificationCompat
import androidx.core.app.NotificationManagerCompat
import com.bumptech.glide.Glide
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.main.MainActivity
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 java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import uy.kohesive.injekt.injectLazy
class LibraryUpdateNotifier(private val context: Context) {
private val preferences: PreferencesHelper by injectLazy()
/**
* Pending intent of action that cancels the library update
*/
private val cancelIntent by lazy {
NotificationReceiver.cancelLibraryUpdatePendingBroadcast(context)
}
/**
* Bitmap of the app for notifications.
*/
private val notificationBitmap by lazy {
BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher)
}
/**
* Cached progress notification to avoid creating a lot.
*/
val progressNotificationBuilder by lazy {
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
setContentTitle(context.getString(R.string.app_name))
setSmallIcon(R.drawable.ic_refresh_24dp)
setLargeIcon(notificationBitmap)
setOngoing(true)
setOnlyAlertOnce(true)
addAction(R.drawable.ic_close_24dp, context.getString(android.R.string.cancel), cancelIntent)
}
}
/**
* Shows the notification containing the currently updating manga and the progress.
*
* @param manga the manga that's being updated.
* @param current the current progress.
* @param total the total progress.
*/
fun showProgressNotification(manga: Manga, current: Int, total: Int) {
val title = if (preferences.hideNotificationContent()) {
context.getString(R.string.notification_check_updates)
} else {
manga.title
}
context.notificationManager.notify(
Notifications.ID_LIBRARY_PROGRESS,
progressNotificationBuilder
.setContentTitle(title)
.setProgress(total, current, false)
.build()
)
}
/**
* Shows notification containing update entries that failed with action to open full log.
*
* @param errors List of entry titles that failed to update.
* @param uri Uri for error log file containing all titles that failed.
*/
fun showUpdateErrorNotification(errors: List<String>, uri: Uri) {
if (errors.isEmpty()) {
return
}
context.notificationManager.notify(
Notifications.ID_LIBRARY_ERROR,
context.notificationBuilder(Notifications.CHANNEL_LIBRARY) {
setContentTitle(context.resources.getQuantityString(R.plurals.notification_update_error, errors.size, errors.size))
setStyle(
NotificationCompat.BigTextStyle().bigText(
errors.joinToString("\n") {
it.chop(NOTIF_TITLE_MAX_LEN)
}
)
)
setSmallIcon(R.drawable.ic_tachi)
val errorLogIntent = NotificationReceiver.openErrorLogPendingActivity(context, uri)
setContentIntent(errorLogIntent)
addAction(
R.drawable.nnf_ic_file_folder,
context.getString(R.string.action_open_log),
errorLogIntent
)
}
.build()
)
}
/**
* Shows the notification containing the result of the update done by the service.
*
* @param updates a list of manga with new updates.
*/
fun showUpdateNotifications(updates: List<Pair<Manga, Array<Chapter>>>) {
if (updates.isEmpty()) {
return
}
NotificationManagerCompat.from(context).apply {
// Parent group notification
notify(
Notifications.ID_NEW_CHAPTERS,
context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setContentTitle(context.getString(R.string.notification_new_chapters))
if (updates.size == 1 && !preferences.hideNotificationContent()) {
setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN))
} else {
setContentText(context.resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size))
if (!preferences.hideNotificationContent()) {
setStyle(
NotificationCompat.BigTextStyle().bigText(
updates.joinToString("\n") {
it.first.title.chop(NOTIF_TITLE_MAX_LEN)
}
)
)
}
}
setSmallIcon(R.drawable.ic_tachi)
setLargeIcon(notificationBitmap)
setGroup(Notifications.GROUP_NEW_CHAPTERS)
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
setGroupSummary(true)
priority = NotificationCompat.PRIORITY_HIGH
setContentIntent(getNotificationIntent())
setAutoCancel(true)
}
)
// Per-manga notification
if (!preferences.hideNotificationContent()) {
updates.forEach {
val (manga, chapters) = it
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
}
}
}
}
private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
return context.notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setContentTitle(manga.title)
val description = getNewChaptersDescription(chapters)
setContentText(description)
setStyle(NotificationCompat.BigTextStyle().bigText(description))
setSmallIcon(R.drawable.ic_tachi)
val icon = getMangaIcon(manga)
if (icon != null) {
setLargeIcon(icon)
}
setGroup(Notifications.GROUP_NEW_CHAPTERS)
setGroupAlertBehavior(NotificationCompat.GROUP_ALERT_SUMMARY)
priority = NotificationCompat.PRIORITY_HIGH
// Open first chapter on tap
setContentIntent(NotificationReceiver.openChapterPendingActivity(context, manga, chapters.first()))
setAutoCancel(true)
// Mark chapters as read action
addAction(
R.drawable.ic_glasses_black_24dp, context.getString(R.string.action_mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast(
context,
manga, chapters, Notifications.ID_NEW_CHAPTERS
)
)
// View chapters action
addAction(
R.drawable.ic_book_24dp, context.getString(R.string.action_view_chapters),
NotificationReceiver.openChapterPendingActivity(
context,
manga, Notifications.ID_NEW_CHAPTERS
)
)
}
}
/**
* Cancels the progress notification.
*/
fun cancelProgressNotification() {
context.notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
}
private fun getMangaIcon(manga: Manga): Bitmap? {
return try {
Glide.with(context)
.asBitmap()
.load(manga.toMangaThumbnail())
.dontTransform()
.centerCrop()
.circleCrop()
.override(
NOTIF_ICON_SIZE,
NOTIF_ICON_SIZE
)
.submit()
.get()
} catch (e: Exception) {
null
}
}
private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
val formatter = DecimalFormat(
"#.###",
DecimalFormatSymbols()
.apply { decimalSeparator = '.' }
)
val displayableChapterNumbers = chapters
.filter { it.isRecognizedNumber }
.sortedBy { it.chapter_number }
.map { formatter.format(it.chapter_number) }
.toSet()
return when (displayableChapterNumbers.size) {
// No sensible chapter numbers to show (i.e. no chapters have parsed chapter number)
0 -> {
// "1 new chapter" or "5 new chapters"
context.resources.getQuantityString(R.plurals.notification_chapters_generic, chapters.size, chapters.size)
}
// Only 1 chapter has a parsed chapter number
1 -> {
val remaining = chapters.size - displayableChapterNumbers.size
if (remaining == 0) {
// "Chapter 2.5"
context.resources.getString(R.string.notification_chapters_single, displayableChapterNumbers.first())
} else {
// "Chapter 2.5 and 10 more"
context.resources.getString(R.string.notification_chapters_single_and_more, displayableChapterNumbers.first(), remaining)
}
}
// Everything else (i.e. multiple parsed chapter numbers)
else -> {
val shouldTruncate = displayableChapterNumbers.size > NOTIF_MAX_CHAPTERS
if (shouldTruncate) {
// "Chapters 1, 2.5, 3, 4, 5 and 10 more"
val remaining = displayableChapterNumbers.size - NOTIF_MAX_CHAPTERS
val joinedChapterNumbers = displayableChapterNumbers.take(NOTIF_MAX_CHAPTERS).joinToString(", ")
context.resources.getQuantityString(R.plurals.notification_chapters_multiple_and_more, remaining, joinedChapterNumbers, remaining)
} else {
// "Chapters 1, 2.5, 3"
context.resources.getString(R.string.notification_chapters_multiple, displayableChapterNumbers.joinToString(", "))
}
}
}
}
/**
* Returns an intent to open the main activity.
*/
private fun getNotificationIntent(): PendingIntent {
val intent = Intent(context, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
action = MainActivity.SHORTCUT_RECENTLY_UPDATED
}
return PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
companion object {
private const val NOTIF_MAX_CHAPTERS = 5
private const val NOTIF_TITLE_MAX_LEN = 45
private const val NOTIF_ICON_SIZE = 192
}
}

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.library
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
/** /**
* This class will provide various functions to Rank mangas to efficiently schedule mangas to update. * This class will provide various functions to rank manga to efficiently schedule manga to update.
*/ */
object LibraryUpdateRanker { object LibraryUpdateRanker {
@ -13,7 +13,7 @@ object LibraryUpdateRanker {
) )
/** /**
* Provides a total ordering over all the Mangas. * Provides a total ordering over all the [Manga]s.
* *
* Assumption: An active [Manga] mActive is expected to have been last updated after an * Assumption: An active [Manga] mActive is expected to have been last updated after an
* inactive [Manga] mInactive. * inactive [Manga] mInactive.
@ -21,23 +21,19 @@ object LibraryUpdateRanker {
* Using this insight, function returns a Comparator for which mActive appears before mInactive. * Using this insight, function returns a Comparator for which mActive appears before mInactive.
* @return a Comparator that ranks manga based on relevance. * @return a Comparator that ranks manga based on relevance.
*/ */
fun latestFirstRanking(): Comparator<Manga> { private fun latestFirstRanking(): Comparator<Manga> =
return Comparator { mangaFirst: Manga, Comparator { first: Manga, second: Manga ->
mangaSecond: Manga -> compareValues(second.last_update, first.last_update)
compareValues(mangaSecond.last_update, mangaFirst.last_update)
} }
}
/** /**
* Provides a total ordering over all the Mangas. * Provides a total ordering over all the [Manga]s.
* *
* Order the manga lexicographically. * Order the manga lexicographically.
* @return a Comparator that ranks manga lexicographically based on the title. * @return a Comparator that ranks manga lexicographically based on the title.
*/ */
fun lexicographicRanking(): Comparator<Manga> { private fun lexicographicRanking(): Comparator<Manga> =
return Comparator { mangaFirst: Manga, Comparator { first: Manga, second: Manga ->
mangaSecond: Manga -> compareValues(first.title, second.title)
compareValues(mangaFirst.title, mangaSecond.title)
} }
}
} }

View File

@ -1,20 +1,12 @@
package eu.kanade.tachiyomi.data.library package eu.kanade.tachiyomi.data.library
import android.app.Notification
import android.app.PendingIntent
import android.app.Service import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
import android.os.Build import android.os.Build
import android.os.IBinder import android.os.IBinder
import android.os.PowerManager import android.os.PowerManager
import androidx.core.app.NotificationCompat import eu.kanade.tachiyomi.data.cache.CoverCache
import androidx.core.app.NotificationCompat.GROUP_ALERT_SUMMARY
import androidx.core.app.NotificationManagerCompat
import com.bumptech.glide.Glide
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
@ -22,26 +14,20 @@ import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme import eu.kanade.tachiyomi.data.library.LibraryUpdateRanker.rankingScheme
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.lang.chop 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 eu.kanade.tachiyomi.util.system.isServiceRunning
import eu.kanade.tachiyomi.util.system.notification import java.io.File
import eu.kanade.tachiyomi.util.system.notificationBuilder
import eu.kanade.tachiyomi.util.system.notificationManager
import java.text.DecimalFormat
import java.text.DecimalFormatSymbols
import java.util.ArrayList
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
@ -63,7 +49,8 @@ class LibraryUpdateService(
val sourceManager: SourceManager = Injekt.get(), val sourceManager: SourceManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(), val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get(), val downloadManager: DownloadManager = Injekt.get(),
val trackManager: TrackManager = Injekt.get() val trackManager: TrackManager = Injekt.get(),
val coverCache: CoverCache = Injekt.get()
) : Service() { ) : Service() {
/** /**
@ -71,45 +58,19 @@ class LibraryUpdateService(
*/ */
private lateinit var wakeLock: PowerManager.WakeLock private lateinit var wakeLock: PowerManager.WakeLock
private lateinit var notifier: LibraryUpdateNotifier
/** /**
* Subscription where the update is done. * Subscription where the update is done.
*/ */
private var subscription: Subscription? = null private var subscription: Subscription? = null
/**
* Pending intent of action that cancels the library update
*/
private val cancelIntent by lazy {
NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)
}
/**
* Bitmap of the app for notifications.
*/
private val notificationBitmap by lazy {
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
}
/**
* Cached progress notification to avoid creating a lot.
*/
private val progressNotificationBuilder by lazy {
notificationBuilder(Notifications.CHANNEL_LIBRARY) {
setContentTitle(getString(R.string.app_name))
setSmallIcon(R.drawable.ic_refresh_24dp)
setLargeIcon(notificationBitmap)
setOngoing(true)
setOnlyAlertOnce(true)
addAction(R.drawable.ic_close_24dp, getString(android.R.string.cancel), cancelIntent)
}
}
/** /**
* Defines what should be updated within a service execution. * Defines what should be updated within a service execution.
*/ */
enum class Target { enum class Target {
CHAPTERS, // Manga chapters CHAPTERS, // Manga chapters
DETAILS, // Manga metadata COVERS, // Manga covers
TRACKING // Tracking metadata TRACKING // Tracking metadata
} }
@ -125,10 +86,6 @@ class LibraryUpdateService(
*/ */
const val KEY_TARGET = "target" const val KEY_TARGET = "target"
private const val NOTIF_MAX_CHAPTERS = 5
private const val NOTIF_TITLE_MAX_LEN = 45
private const val NOTIF_ICON_SIZE = 192
/** /**
* Returns the status of the service. * Returns the status of the service.
* *
@ -182,11 +139,11 @@ class LibraryUpdateService(
*/ */
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotificationBuilder.build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock( notifier = LibraryUpdateNotifier(this)
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock" wakeLock = acquireWakeLock(javaClass.name)
)
wakeLock.acquire() startForeground(Notifications.ID_LIBRARY_PROGRESS, notifier.progressNotificationBuilder.build())
} }
/** /**
@ -234,7 +191,7 @@ class LibraryUpdateService(
// Update either chapter list or manga details. // Update either chapter list or manga details.
when (target) { when (target) {
Target.CHAPTERS -> updateChapterList(mangaList) Target.CHAPTERS -> updateChapterList(mangaList)
Target.DETAILS -> updateDetails(mangaList) Target.COVERS -> updateCovers(mangaList)
Target.TRACKING -> updateTrackings(mangaList) Target.TRACKING -> updateTrackings(mangaList)
} }
} }
@ -296,36 +253,28 @@ class LibraryUpdateService(
// Initialize the variables holding the progress of the updates. // Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0) val count = AtomicInteger(0)
// List containing new updates // List containing new updates
val newUpdates = ArrayList<Pair<LibraryManga, Array<Chapter>>>() val newUpdates = mutableListOf<Pair<LibraryManga, Array<Chapter>>>()
// List containing failed updates // List containing failed updates
val failedUpdates = ArrayList<Manga>() val failedUpdates = mutableListOf<Pair<Manga, String?>>()
// List containing categories that get included in downloads.
val categoriesToDownload = preferences.downloadNewCategories().get().map(String::toInt)
// Boolean to determine if user wants to automatically download new chapters.
val downloadNew = preferences.downloadNew().get()
// Boolean to determine if DownloadManager has downloads // Boolean to determine if DownloadManager has downloads
var hasDownloads = false var hasDownloads = false
// Emit each manga and update it sequentially. // Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate) return Observable.from(mangaToUpdate)
// Notify manga that will update. // Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } .doOnNext { notifier.showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the chapters of the manga. // Update the chapters of the manga
.concatMap { manga -> .concatMap { manga ->
updateManga(manga) updateManga(manga)
// If there's any error, return empty update and continue. // If there's any error, return empty update and continue.
.onErrorReturn { .onErrorReturn {
failedUpdates.add(manga) failedUpdates.add(Pair(manga, it.message))
Pair(emptyList(), emptyList()) Pair(emptyList(), emptyList())
} }
// Filter out mangas without new chapters (or failed). // Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first.isNotEmpty() } .filter { pair -> pair.first.isNotEmpty() }
.doOnNext { .doOnNext {
if (downloadNew && ( if (manga.shouldDownloadNewChapters(db, preferences)) {
categoriesToDownload.isEmpty() ||
manga.category in categoriesToDownload
)
) {
downloadChapters(manga, it.first) downloadChapters(manga, it.first)
hasDownloads = true hasDownloads = true
} }
@ -334,7 +283,10 @@ class LibraryUpdateService(
.map { .map {
Pair( Pair(
manga, manga,
(it.first.sortedByDescending { ch -> ch.source_order }.toTypedArray()) (
it.first.sortedByDescending { ch -> ch.source_order }
.toTypedArray()
)
) )
} }
} }
@ -345,31 +297,30 @@ class LibraryUpdateService(
} }
// Notify result of the overall update. // Notify result of the overall update.
.doOnCompleted { .doOnCompleted {
notifier.cancelProgressNotification()
if (newUpdates.isNotEmpty()) { if (newUpdates.isNotEmpty()) {
showUpdateNotifications(newUpdates) notifier.showUpdateNotifications(newUpdates)
if (downloadNew && hasDownloads) { if (hasDownloads) {
DownloadService.start(this) DownloadService.start(this)
} }
} }
if (failedUpdates.isNotEmpty()) { if (preferences.showLibraryUpdateErrors() && failedUpdates.isNotEmpty()) {
Timber.e("Failed updating: ${failedUpdates.map { it.title }}") val errorFile = writeErrorFile(failedUpdates)
notifier.showUpdateErrorNotification(
failedUpdates.map { it.first.title },
errorFile.getUriCompat(this)
)
} }
cancelProgressNotification()
} }
.map { manga -> manga.first } .map { manga -> manga.first }
} }
fun downloadChapters(manga: Manga, chapters: List<Chapter>) { private fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
// we need to get the chapters from the db so we have chapter ids
val mangaChapters = db.getChapters(manga).executeAsBlocking()
val dbChapters = chapters.map {
mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
}
// We don't want to start downloading while the library is updating, because websites // We don't want to start downloading while the library is updating, because websites
// may don't like it and they could ban the user. // may don't like it and they could ban the user.
downloadManager.downloadChapters(manga, dbChapters, false) downloadManager.downloadChapters(manga, chapters, false)
} }
/** /**
@ -379,41 +330,56 @@ class LibraryUpdateService(
* @return a pair of the inserted and removed chapters. * @return a pair of the inserted and removed chapters.
*/ */
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> { fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty() val source = sourceManager.getOrStub(manga.source)
// Update manga details metadata in the background
if (preferences.autoUpdateMetadata()) {
source.fetchMangaDetails(manga)
.map { updatedManga ->
// Avoid "losing" existing cover
if (!updatedManga.thumbnail_url.isNullOrEmpty()) {
manga.prepUpdateCover(coverCache, updatedManga, false)
} else {
updatedManga.thumbnail_url = manga.thumbnail_url
}
manga.copyFrom(updatedManga)
db.insertManga(manga).executeAsBlocking()
manga
}
.onErrorResumeNext { Observable.just(manga) }
.subscribeOn(Schedulers.io())
.subscribe()
}
return source.fetchChapterList(manga) return source.fetchChapterList(manga)
.map { syncChaptersWithSource(db, it, manga, source) } .map { syncChaptersWithSource(db, it, manga, source) }
} }
/** private fun updateCovers(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
* Method that updates the details of the given list of manga. It's called in a background var count = 0
* thread, so it's safe to do heavy operations or network calls here.
*
* @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update.
*/
fun updateDetails(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
// Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate) return Observable.from(mangaToUpdate)
// Notify manga that will update. .doOnNext {
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) } notifier.showProgressNotification(it, count++, mangaToUpdate.size)
// Update the details of the manga. }
.concatMap { manga -> .flatMap { manga ->
val source = sourceManager.get(manga.source) as? HttpSource val source = sourceManager.get(manga.source)
?: return@concatMap Observable.empty<LibraryManga>() ?: return@flatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga) source.fetchMangaDetails(manga)
.map { networkManga -> .map { networkManga ->
manga.copyFrom(networkManga) manga.prepUpdateCover(coverCache, networkManga, true)
db.insertManga(manga).executeAsBlocking() networkManga.thumbnail_url?.let {
manga.thumbnail_url = it
db.insertManga(manga).executeAsBlocking()
}
manga manga
} }
.onErrorReturn { manga } .onErrorReturn { manga }
} }
.doOnCompleted { .doOnCompleted {
cancelProgressNotification() notifier.cancelProgressNotification()
} }
} }
@ -430,7 +396,7 @@ class LibraryUpdateService(
// Emit each manga and update it sequentially. // Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate) return Observable.from(mangaToUpdate)
// Notify manga that will update. // Notify manga that will update.
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) } .doOnNext { notifier.showProgressNotification(it, count++, mangaToUpdate.size) }
// Update the tracking details. // Update the tracking details.
.concatMap { manga -> .concatMap { manga ->
val tracks = db.getTracks(manga).executeAsBlocking() val tracks = db.getTracks(manga).executeAsBlocking()
@ -449,207 +415,29 @@ class LibraryUpdateService(
.map { manga } .map { manga }
} }
.doOnCompleted { .doOnCompleted {
cancelProgressNotification() notifier.cancelProgressNotification()
} }
} }
/** /**
* Shows the notification containing the currently updating manga and the progress. * Writes basic file of update errors to cache dir.
*
* @param manga the manga that's being updated.
* @param current the current progress.
* @param total the total progress.
*/ */
private fun showProgressNotification(manga: Manga, current: Int, total: Int) { private fun writeErrorFile(errors: List<Pair<Manga, String?>>): File {
val title = if (preferences.hideNotificationContent()) { try {
getString(R.string.notification_check_updates) if (errors.isNotEmpty()) {
} else { val destFile = File(externalCacheDir, "tachiyomi_update_errors.txt")
manga.title
}
notificationManager.notify( destFile.bufferedWriter().use { out ->
Notifications.ID_LIBRARY_PROGRESS, errors.forEach { (manga, error) ->
progressNotificationBuilder val source = sourceManager.getOrStub(manga.source)
.setContentTitle(title) out.write("${manga.title} ($source): $error\n")
.setProgress(total, current, false)
.build()
)
}
/**
* Shows the notification containing the result of the update done by the service.
*
* @param updates a list of manga with new updates.
*/
private fun showUpdateNotifications(updates: List<Pair<Manga, Array<Chapter>>>) {
if (updates.isEmpty()) {
return
}
NotificationManagerCompat.from(this).apply {
// Parent group notification
notify(
Notifications.ID_NEW_CHAPTERS,
notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setContentTitle(getString(R.string.notification_new_chapters))
if (updates.size == 1 && !preferences.hideNotificationContent()) {
setContentText(updates.first().first.title.chop(NOTIF_TITLE_MAX_LEN))
} else {
setContentText(resources.getQuantityString(R.plurals.notification_new_chapters_summary, updates.size, updates.size))
if (!preferences.hideNotificationContent()) {
setStyle(
NotificationCompat.BigTextStyle().bigText(
updates.joinToString("\n") {
it.first.title.chop(NOTIF_TITLE_MAX_LEN)
}
)
)
}
} }
setSmallIcon(R.drawable.ic_tachi)
setLargeIcon(notificationBitmap)
setGroup(Notifications.GROUP_NEW_CHAPTERS)
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
setGroupSummary(true)
priority = NotificationCompat.PRIORITY_HIGH
setContentIntent(getNotificationIntent())
setAutoCancel(true)
}
)
// Per-manga notification
if (!preferences.hideNotificationContent()) {
updates.forEach {
val (manga, chapters) = it
notify(manga.id.hashCode(), createNewChaptersNotification(manga, chapters))
} }
return destFile
} }
}
}
private fun createNewChaptersNotification(manga: Manga, chapters: Array<Chapter>): Notification {
return notification(Notifications.CHANNEL_NEW_CHAPTERS) {
setContentTitle(manga.title)
val description = getNewChaptersDescription(chapters)
setContentText(description)
setStyle(NotificationCompat.BigTextStyle().bigText(description))
setSmallIcon(R.drawable.ic_tachi)
val icon = getMangaIcon(manga)
if (icon != null) {
setLargeIcon(icon)
}
setGroup(Notifications.GROUP_NEW_CHAPTERS)
setGroupAlertBehavior(GROUP_ALERT_SUMMARY)
priority = NotificationCompat.PRIORITY_HIGH
// Open first chapter on tap
setContentIntent(NotificationReceiver.openChapterPendingActivity(this@LibraryUpdateService, manga, chapters.first()))
setAutoCancel(true)
// Mark chapters as read action
addAction(
R.drawable.ic_glasses_black_24dp, getString(R.string.action_mark_as_read),
NotificationReceiver.markAsReadPendingBroadcast(
this@LibraryUpdateService,
manga, chapters, Notifications.ID_NEW_CHAPTERS
)
)
// View chapters action
addAction(
R.drawable.ic_book_24dp, getString(R.string.action_view_chapters),
NotificationReceiver.openChapterPendingActivity(
this@LibraryUpdateService,
manga, Notifications.ID_NEW_CHAPTERS
)
)
}
}
/**
* Cancels the progress notification.
*/
private fun cancelProgressNotification() {
notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
}
private fun getMangaIcon(manga: Manga): Bitmap? {
return try {
Glide.with(this)
.asBitmap()
.load(manga.toMangaThumbnail())
.dontTransform()
.centerCrop()
.circleCrop()
.override(NOTIF_ICON_SIZE, NOTIF_ICON_SIZE)
.submit()
.get()
} catch (e: Exception) { } catch (e: Exception) {
null // Empty
} }
} return File("")
private fun getNewChaptersDescription(chapters: Array<Chapter>): String {
val formatter = DecimalFormat(
"#.###",
DecimalFormatSymbols()
.apply { decimalSeparator = '.' }
)
val displayableChapterNumbers = chapters
.filter { it.isRecognizedNumber }
.sortedBy { it.chapter_number }
.map { formatter.format(it.chapter_number) }
.toSet()
return when (displayableChapterNumbers.size) {
// No sensible chapter numbers to show (i.e. no chapters have parsed chapter number)
0 -> {
// "1 new chapter" or "5 new chapters"
resources.getQuantityString(R.plurals.notification_chapters_generic, chapters.size, chapters.size)
}
// Only 1 chapter has a parsed chapter number
1 -> {
val remaining = chapters.size - displayableChapterNumbers.size
if (remaining == 0) {
// "Chapter 2.5"
resources.getString(R.string.notification_chapters_single, displayableChapterNumbers.first())
} else {
// "Chapter 2.5 and 10 more"
resources.getString(R.string.notification_chapters_single_and_more, displayableChapterNumbers.first(), remaining)
}
}
// Everything else (i.e. multiple parsed chapter numbers)
else -> {
val shouldTruncate = displayableChapterNumbers.size > NOTIF_MAX_CHAPTERS
if (shouldTruncate) {
// "Chapters 1, 2.5, 3, 4, 5 and 10 more"
val remaining = displayableChapterNumbers.size - NOTIF_MAX_CHAPTERS
val joinedChapterNumbers = displayableChapterNumbers.take(NOTIF_MAX_CHAPTERS).joinToString(", ")
resources.getQuantityString(R.plurals.notification_chapters_multiple_and_more, remaining, joinedChapterNumbers, remaining)
} else {
// "Chapters 1, 2.5, 3"
resources.getString(R.string.notification_chapters_multiple, displayableChapterNumbers.joinToString(", "))
}
}
}
}
/**
* Returns an intent to open the main activity.
*/
private fun getNotificationIntent(): PendingIntent {
val intent = Intent(this, MainActivity::class.java).apply {
flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
action = MainActivity.SHORTCUT_RECENTLY_UPDATED
}
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
} }
} }

View File

@ -25,13 +25,17 @@ object Notifications {
*/ */
const val CHANNEL_LIBRARY = "library_channel" const val CHANNEL_LIBRARY = "library_channel"
const val ID_LIBRARY_PROGRESS = -101 const val ID_LIBRARY_PROGRESS = -101
const val ID_LIBRARY_ERROR = -102
/** /**
* Notification channel and ids used by the downloader. * Notification channel and ids used by the downloader.
*/ */
const val CHANNEL_DOWNLOADER = "downloader_channel" 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 = -201
const val CHANNEL_DOWNLOADER_COMPLETE = "downloader_complete_channel"
const val ID_DOWNLOAD_CHAPTER_ERROR = -202 const val ID_DOWNLOAD_CHAPTER_ERROR = -202
const val ID_DOWNLOAD_CHAPTER_COMPLETE = -203
/** /**
* Notification channel and ids used by the library updater. * Notification channel and ids used by the library updater.
@ -49,7 +53,7 @@ object Notifications {
/** /**
* Notification channel and ids used by the backup/restore system. * Notification channel and ids used by the backup/restore system.
*/ */
private const val GROUP_BACK_RESTORE = "group_backup_restore" private const val GROUP_BACKUP_RESTORE = "group_backup_restore"
const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel" const val CHANNEL_BACKUP_RESTORE_PROGRESS = "backup_restore_progress_channel"
const val ID_BACKUP_PROGRESS = -501 const val ID_BACKUP_PROGRESS = -501
const val ID_RESTORE_PROGRESS = -503 const val ID_RESTORE_PROGRESS = -503
@ -58,6 +62,7 @@ object Notifications {
const val ID_RESTORE_COMPLETE = -504 const val ID_RESTORE_COMPLETE = -504
private val deprecatedChannels = listOf( private val deprecatedChannels = listOf(
"downloader_channel",
"backup_restore_complete_channel" "backup_restore_complete_channel"
) )
@ -69,10 +74,12 @@ object Notifications {
fun createChannels(context: Context) { fun createChannels(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val backupRestoreGroup = NotificationChannelGroup(GROUP_BACK_RESTORE, context.getString(R.string.channel_backup_restore)) listOf(
context.notificationManager.createNotificationChannelGroup(backupRestoreGroup) NotificationChannelGroup(GROUP_BACKUP_RESTORE, context.getString(R.string.group_backup_restore)),
NotificationChannelGroup(GROUP_DOWNLOADER, context.getString(R.string.group_downloader))
).forEach(context.notificationManager::createNotificationChannelGroup)
val channels = listOf( listOf(
NotificationChannel( NotificationChannel(
CHANNEL_COMMON, context.getString(R.string.channel_common), CHANNEL_COMMON, context.getString(R.string.channel_common),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
@ -84,9 +91,17 @@ object Notifications {
setShowBadge(false) setShowBadge(false)
}, },
NotificationChannel( NotificationChannel(
CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader), CHANNEL_DOWNLOADER_PROGRESS, context.getString(R.string.channel_progress),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
group = GROUP_DOWNLOADER
setShowBadge(false)
},
NotificationChannel(
CHANNEL_DOWNLOADER_COMPLETE, context.getString(R.string.channel_complete),
NotificationManager.IMPORTANCE_LOW
).apply {
group = GROUP_DOWNLOADER
setShowBadge(false) setShowBadge(false)
}, },
NotificationChannel( NotificationChannel(
@ -98,26 +113,23 @@ object Notifications {
NotificationManager.IMPORTANCE_DEFAULT NotificationManager.IMPORTANCE_DEFAULT
), ),
NotificationChannel( NotificationChannel(
CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_backup_restore_progress), CHANNEL_BACKUP_RESTORE_PROGRESS, context.getString(R.string.channel_progress),
NotificationManager.IMPORTANCE_LOW NotificationManager.IMPORTANCE_LOW
).apply { ).apply {
group = GROUP_BACK_RESTORE group = GROUP_BACKUP_RESTORE
setShowBadge(false) setShowBadge(false)
}, },
NotificationChannel( NotificationChannel(
CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_backup_restore_complete), CHANNEL_BACKUP_RESTORE_COMPLETE, context.getString(R.string.channel_complete),
NotificationManager.IMPORTANCE_HIGH NotificationManager.IMPORTANCE_HIGH
).apply { ).apply {
group = GROUP_BACK_RESTORE group = GROUP_BACKUP_RESTORE
setShowBadge(false) setShowBadge(false)
setSound(null, null) setSound(null, null)
} }
) ).forEach(context.notificationManager::createNotificationChannel)
context.notificationManager.createNotificationChannels(channels)
// Delete old notification channels // Delete old notification channels
deprecatedChannels.forEach { deprecatedChannels.forEach(context.notificationManager::deleteNotificationChannel)
context.notificationManager.deleteNotificationChannel(it)
}
} }
} }

View File

@ -21,6 +21,8 @@ object PreferenceKeys {
const val showPageNumber = "pref_show_page_number_key" const val showPageNumber = "pref_show_page_number_key"
const val showReadingMode = "pref_show_reading_mode"
const val trueColor = "pref_true_color_key" const val trueColor = "pref_true_color_key"
const val fullscreen = "fullscreen" const val fullscreen = "fullscreen"
@ -53,6 +55,8 @@ object PreferenceKeys {
const val readWithTapping = "reader_tap" const val readWithTapping = "reader_tap"
const val readWithTappingInverted = "reader_tapping_inverted"
const val readWithLongTap = "reader_long_tap" const val readWithLongTap = "reader_long_tap"
const val readWithVolumeKeys = "reader_volume_keys" const val readWithVolumeKeys = "reader_volume_keys"
@ -65,15 +69,17 @@ object PreferenceKeys {
const val landscapeColumns = "pref_library_columns_landscape_key" const val landscapeColumns = "pref_library_columns_landscape_key"
const val jumpToChapters = "jump_to_chapters"
const val updateOnlyNonCompleted = "pref_update_only_non_completed_key" const val updateOnlyNonCompleted = "pref_update_only_non_completed_key"
const val autoUpdateTrack = "pref_auto_update_manga_sync_key" const val autoUpdateTrack = "pref_auto_update_manga_sync_key"
const val lastUsedCatalogueSource = "last_catalogue_source" const val lastUsedSource = "last_catalogue_source"
const val lastUsedCategory = "last_used_category" const val lastUsedCategory = "last_used_category"
const val catalogueAsList = "pref_display_catalogue_as_list" const val sourceDisplayMode = "pref_display_mode_catalogue"
const val enabledLanguages = "source_languages" const val enabledLanguages = "source_languages"
@ -123,11 +129,15 @@ object PreferenceKeys {
const val hideNotificationContent = "hide_notification_content" const val hideNotificationContent = "hide_notification_content"
const val autoUpdateMetadata = "auto_update_metadata"
const val showLibraryUpdateErrors = "show_library_update_errors"
const val downloadNew = "download_new" const val downloadNew = "download_new"
const val downloadNewCategories = "download_new_categories" const val downloadNewCategories = "download_new_categories"
const val libraryAsList = "pref_display_library_as_list" const val libraryDisplayMode = "pref_display_mode_library"
const val lang = "app_language" const val lang = "app_language"
@ -141,10 +151,16 @@ object PreferenceKeys {
const val downloadBadge = "display_download_badge" const val downloadBadge = "display_download_badge"
const val unreadBadge = "display_unread_badge"
const val categoryTabs = "display_category_tabs"
const val alwaysShowChapterTransition = "always_show_chapter_transition" const val alwaysShowChapterTransition = "always_show_chapter_transition"
const val searchPinnedSourcesOnly = "search_pinned_sources_only" const val searchPinnedSourcesOnly = "search_pinned_sources_only"
const val enableDoh = "enable_doh"
fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId" fun trackUsername(syncId: Int) = "pref_mangasync_username_$syncId"
fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId" fun trackPassword(syncId: Int) = "pref_mangasync_password_$syncId"

View File

@ -5,14 +5,36 @@ package eu.kanade.tachiyomi.data.preference
*/ */
object PreferenceValues { object PreferenceValues {
const val THEME_MODE_LIGHT = "light" // Keys are lowercase to match legacy string values
const val THEME_MODE_DARK = "dark" enum class ThemeMode {
const val THEME_MODE_SYSTEM = "system" light,
dark,
system,
}
const val THEME_LIGHT_DEFAULT = "default" // Keys are lowercase to match legacy string values
const val THEME_LIGHT_BLUE = "blue" enum class LightThemeVariant {
default,
blue,
}
const val THEME_DARK_DEFAULT = "default" // Keys are lowercase to match legacy string values
const val THEME_DARK_BLUE = "blue" enum class DarkThemeVariant {
const val THEME_DARK_AMOLED = "amoled" default,
blue,
amoled,
}
enum class DisplayMode {
COMPACT_GRID,
COMFORTABLE_GRID,
LIST,
}
enum class TappingInvertMode {
NONE,
HORIZONTAL,
VERTICAL,
BOTH
}
} }

View File

@ -1,17 +1,15 @@
package eu.kanade.tachiyomi.data.preference package eu.kanade.tachiyomi.data.preference
import android.content.Context import android.content.Context
import android.content.SharedPreferences
import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.core.net.toUri
import androidx.preference.PreferenceManager import androidx.preference.PreferenceManager
import com.f2prateek.rx.preferences.Preference as RxPreference
import com.f2prateek.rx.preferences.RxSharedPreferences
import com.tfcporciuncula.flow.FlowSharedPreferences import com.tfcporciuncula.flow.FlowSharedPreferences
import com.tfcporciuncula.flow.Preference import com.tfcporciuncula.flow.Preference
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys import eu.kanade.tachiyomi.data.preference.PreferenceKeys as Keys
import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values import eu.kanade.tachiyomi.data.preference.PreferenceValues as Values
import eu.kanade.tachiyomi.data.preference.PreferenceValues.DisplayMode
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import java.io.File import java.io.File
@ -22,8 +20,6 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
fun <T> RxPreference<T>.getOrDefault(): T = get() ?: defaultValue()!!
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> { fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
block(get()) block(get())
@ -31,44 +27,31 @@ fun <T> Preference<T>.asImmediateFlow(block: (value: T) -> Unit): Flow<T> {
.onEach { block(it) } .onEach { block(it) }
} }
private class DateFormatConverter : RxPreference.Adapter<DateFormat> { operator fun <T> Preference<Set<T>>.plusAssign(item: T) {
override fun get(key: String, preferences: SharedPreferences): DateFormat { set(get() + item)
val dateFormat = preferences.getString(Keys.dateFormat, "")!! }
if (dateFormat != "") { operator fun <T> Preference<Set<T>>.minusAssign(item: T) {
return SimpleDateFormat(dateFormat, Locale.getDefault()) set(get() - item)
}
return DateFormat.getDateInstance(DateFormat.SHORT)
}
override fun set(key: String, value: DateFormat, editor: SharedPreferences.Editor) {
// No-op
}
} }
@OptIn(ExperimentalCoroutinesApi::class) @OptIn(ExperimentalCoroutinesApi::class)
class PreferencesHelper(val context: Context) { class PreferencesHelper(val context: Context) {
private val prefs = PreferenceManager.getDefaultSharedPreferences(context) private val prefs = PreferenceManager.getDefaultSharedPreferences(context)
private val rxPrefs = RxSharedPreferences.create(prefs)
private val flowPrefs = FlowSharedPreferences(prefs) private val flowPrefs = FlowSharedPreferences(prefs)
private val defaultDownloadsDir = Uri.fromFile( private val defaultDownloadsDir = File(
File( Environment.getExternalStorageDirectory().absolutePath + File.separator +
Environment.getExternalStorageDirectory().absolutePath + File.separator + context.getString(R.string.app_name),
context.getString(R.string.app_name), "downloads"
"downloads" ).toUri()
)
)
private val defaultBackupDir = Uri.fromFile( private val defaultBackupDir = File(
File( Environment.getExternalStorageDirectory().absolutePath + File.separator +
Environment.getExternalStorageDirectory().absolutePath + File.separator + context.getString(R.string.app_name),
context.getString(R.string.app_name), "backup"
"backup" ).toUri()
)
)
fun startScreen() = prefs.getInt(Keys.startScreen, 1) fun startScreen() = prefs.getInt(Keys.startScreen, 1)
@ -84,15 +67,19 @@ class PreferencesHelper(val context: Context) {
fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false) fun hideNotificationContent() = prefs.getBoolean(Keys.hideNotificationContent, false)
fun autoUpdateMetadata() = prefs.getBoolean(Keys.autoUpdateMetadata, false)
fun showLibraryUpdateErrors() = prefs.getBoolean(Keys.showLibraryUpdateErrors, false)
fun clear() = prefs.edit().clear().apply() fun clear() = prefs.edit().clear().apply()
fun themeMode() = flowPrefs.getString(Keys.themeMode, Values.THEME_MODE_SYSTEM) fun themeMode() = flowPrefs.getEnum(Keys.themeMode, Values.ThemeMode.system)
fun themeLight() = flowPrefs.getString(Keys.themeLight, Values.THEME_LIGHT_DEFAULT) fun themeLight() = flowPrefs.getEnum(Keys.themeLight, Values.LightThemeVariant.default)
fun themeDark() = flowPrefs.getString(Keys.themeDark, Values.THEME_DARK_DEFAULT) fun themeDark() = flowPrefs.getEnum(Keys.themeDark, Values.DarkThemeVariant.default)
fun rotation() = rxPrefs.getInteger(Keys.rotation, 1) fun rotation() = flowPrefs.getInt(Keys.rotation, 1)
fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true) fun pageTransitions() = flowPrefs.getBoolean(Keys.enableTransitions, true)
@ -100,6 +87,8 @@ class PreferencesHelper(val context: Context) {
fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true) fun showPageNumber() = flowPrefs.getBoolean(Keys.showPageNumber, true)
fun showReadingMode() = prefs.getBoolean(Keys.showReadingMode, true)
fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false) fun trueColor() = flowPrefs.getBoolean(Keys.trueColor, false)
fun fullscreen() = flowPrefs.getBoolean(Keys.fullscreen, true) fun fullscreen() = flowPrefs.getBoolean(Keys.fullscreen, true)
@ -118,7 +107,7 @@ class PreferencesHelper(val context: Context) {
fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0) fun colorFilterMode() = flowPrefs.getInt(Keys.colorFilterMode, 0)
fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 1) fun defaultViewer() = prefs.getInt(Keys.defaultViewer, 2)
fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1) fun imageScaleType() = flowPrefs.getInt(Keys.imageScaleType, 1)
@ -136,27 +125,31 @@ class PreferencesHelper(val context: Context) {
fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true) fun readWithTapping() = flowPrefs.getBoolean(Keys.readWithTapping, true)
fun readWithTappingInverted() = flowPrefs.getEnum(Keys.readWithTappingInverted, Values.TappingInvertMode.NONE)
fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true) fun readWithLongTap() = flowPrefs.getBoolean(Keys.readWithLongTap, true)
fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false) fun readWithVolumeKeys() = flowPrefs.getBoolean(Keys.readWithVolumeKeys, false)
fun readWithVolumeKeysInverted() = flowPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false) fun readWithVolumeKeysInverted() = flowPrefs.getBoolean(Keys.readWithVolumeKeysInverted, false)
fun portraitColumns() = rxPrefs.getInteger(Keys.portraitColumns, 0) fun portraitColumns() = flowPrefs.getInt(Keys.portraitColumns, 0)
fun landscapeColumns() = rxPrefs.getInteger(Keys.landscapeColumns, 0) fun landscapeColumns() = flowPrefs.getInt(Keys.landscapeColumns, 0)
fun jumpToChapters() = prefs.getBoolean(Keys.jumpToChapters, false)
fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false) fun updateOnlyNonCompleted() = prefs.getBoolean(Keys.updateOnlyNonCompleted, false)
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true) fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
fun lastUsedCatalogueSource() = rxPrefs.getLong(Keys.lastUsedCatalogueSource, -1) fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0) fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)
fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0) fun lastVersionCode() = flowPrefs.getInt("last_version_code", 0)
fun catalogueAsList() = rxPrefs.getBoolean(Keys.catalogueAsList, false) fun sourceDisplayMode() = flowPrefs.getEnum(Keys.sourceDisplayMode, DisplayMode.COMPACT_GRID)
fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("en", Locale.getDefault().language)) fun enabledLanguages() = flowPrefs.getStringSet(Keys.enabledLanguages, setOf("en", Locale.getDefault().language))
@ -177,7 +170,10 @@ class PreferencesHelper(val context: Context) {
fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString()) fun backupsDirectory() = flowPrefs.getString(Keys.backupDirectory, defaultBackupDir.toString())
fun dateFormat() = rxPrefs.getObject(Keys.dateFormat, DateFormat.getDateInstance(DateFormat.SHORT), DateFormatConverter()) fun dateFormat(format: String = flowPrefs.getString(Keys.dateFormat, "").get()): DateFormat = when (format) {
"" -> DateFormat.getDateInstance(DateFormat.SHORT)
else -> SimpleDateFormat(format, Locale.getDefault())
}
fun downloadsDirectory() = flowPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString()) fun downloadsDirectory() = flowPrefs.getString(Keys.downloadsDirectory, defaultDownloadsDir.toString())
@ -199,12 +195,16 @@ class PreferencesHelper(val context: Context) {
fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0) fun libraryUpdatePrioritization() = flowPrefs.getInt(Keys.libraryUpdatePrioritization, 0)
fun libraryAsList() = flowPrefs.getBoolean(Keys.libraryAsList, false) fun libraryDisplayMode() = flowPrefs.getEnum(Keys.libraryDisplayMode, DisplayMode.COMPACT_GRID)
fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false) fun downloadBadge() = flowPrefs.getBoolean(Keys.downloadBadge, false)
fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false) fun downloadedOnly() = flowPrefs.getBoolean(Keys.downloadedOnly, false)
fun unreadBadge() = flowPrefs.getBoolean(Keys.unreadBadge, true)
fun categoryTabs() = flowPrefs.getBoolean(Keys.categoryTabs, true)
fun filterDownloaded() = flowPrefs.getBoolean(Keys.filterDownloaded, false) fun filterDownloaded() = flowPrefs.getBoolean(Keys.filterDownloaded, false)
fun filterUnread() = flowPrefs.getBoolean(Keys.filterUnread, false) fun filterUnread() = flowPrefs.getBoolean(Keys.filterUnread, false)
@ -223,9 +223,9 @@ class PreferencesHelper(val context: Context) {
fun searchPinnedSourcesOnly() = prefs.getBoolean(Keys.searchPinnedSourcesOnly, false) fun searchPinnedSourcesOnly() = prefs.getBoolean(Keys.searchPinnedSourcesOnly, false)
fun hiddenCatalogues() = flowPrefs.getStringSet("hidden_catalogues", emptySet()) fun disabledSources() = flowPrefs.getStringSet("hidden_catalogues", emptySet())
fun pinnedCatalogues() = flowPrefs.getStringSet("pinned_catalogues", emptySet()) fun pinnedSources() = flowPrefs.getStringSet("pinned_catalogues", emptySet())
fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false) fun downloadNew() = flowPrefs.getBoolean(Keys.downloadNew, false)
@ -242,4 +242,6 @@ class PreferencesHelper(val context: Context) {
fun migrateFlags() = flowPrefs.getInt("migrate_flags", Int.MAX_VALUE) fun migrateFlags() = flowPrefs.getInt("migrate_flags", Int.MAX_VALUE)
fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet()) fun trustedSignatures() = flowPrefs.getStringSet("trusted_signatures", emptySet())
fun enableDoh() = prefs.getBoolean(Keys.enableDoh, false)
} }

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.track.anilist package eu.kanade.tachiyomi.data.track.anilist
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.get import com.github.salomonbrys.kotson.get
import com.github.salomonbrys.kotson.jsonObject import com.github.salomonbrys.kotson.jsonObject
@ -271,7 +272,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
return ALManga( return ALManga(
struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString, struct["id"].asInt, struct["title"]["romaji"].asString, struct["coverImage"]["large"].asString,
struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].asString, struct["description"].nullString.orEmpty(), struct["type"].asString, struct["status"].nullString.orEmpty(),
date, struct["chapters"].nullInt ?: 0 date, struct["chapters"].nullInt ?: 0
) )
} }
@ -291,7 +292,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
return baseMangaUrl + mediaId return baseMangaUrl + mediaId
} }
fun authUrl() = Uri.parse("${baseUrl}oauth/authorize").buildUpon() fun authUrl(): Uri = "${baseUrl}oauth/authorize".toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "token") .appendQueryParameter("response_type", "token")
.build() .build()

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.track.bangumi package eu.kanade.tachiyomi.data.track.bangumi
import android.net.Uri import android.net.Uri
import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.obj import com.github.salomonbrys.kotson.obj
import com.google.gson.Gson import com.google.gson.Gson
@ -72,9 +73,9 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
} }
fun search(search: String): Observable<List<TrackSearch>> { fun search(search: String): Observable<List<TrackSearch>> {
val url = Uri.parse( val url = "$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}"
"$apiUrl/search/subject/${URLEncoder.encode(search, Charsets.UTF_8.name())}" .toUri()
).buildUpon() .buildUpon()
.appendQueryParameter("max_results", "20") .appendQueryParameter("max_results", "20")
.build() .build()
val request = Request.Builder() val request = Request.Builder()
@ -196,8 +197,8 @@ class BangumiApi(private val client: OkHttpClient, interceptor: BangumiIntercept
return "$baseMangaUrl/$remoteId" return "$baseMangaUrl/$remoteId"
} }
fun authUrl() = fun authUrl(): Uri =
Uri.parse(loginUrl).buildUpon() loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")
.appendQueryParameter("redirect_uri", redirectUrl) .appendQueryParameter("redirect_uri", redirectUrl)

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.track.myanimelist package eu.kanade.tachiyomi.data.track.myanimelist
import android.net.Uri import androidx.core.net.toUri
import eu.kanade.tachiyomi.data.database.models.Track import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
@ -260,13 +260,13 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId private fun mangaUrl(remoteId: Int) = baseMangaUrl + remoteId
private fun loginUrl() = Uri.parse(baseUrl).buildUpon() private fun loginUrl() = baseUrl.toUri().buildUpon()
.appendPath("login.php") .appendPath("login.php")
.toString() .toString()
private fun searchUrl(query: String): String { private fun searchUrl(query: String): String {
val col = "c[]" val col = "c[]"
return Uri.parse(baseUrl).buildUpon() return baseUrl.toUri().buildUpon()
.appendPath("manga.php") .appendPath("manga.php")
.appendQueryParameter("q", query) .appendQueryParameter("q", query)
.appendQueryParameter(col, "a") .appendQueryParameter(col, "a")
@ -278,17 +278,17 @@ class MyAnimeListApi(private val client: OkHttpClient, interceptor: MyAnimeListI
.toString() .toString()
} }
private fun exportListUrl() = Uri.parse(baseUrl).buildUpon() private fun exportListUrl() = baseUrl.toUri().buildUpon()
.appendPath("panel.php") .appendPath("panel.php")
.appendQueryParameter("go", "export") .appendQueryParameter("go", "export")
.toString() .toString()
private fun editPageUrl(mediaId: Int) = Uri.parse(baseModifyListUrl).buildUpon() private fun editPageUrl(mediaId: Int) = baseModifyListUrl.toUri().buildUpon()
.appendPath(mediaId.toString()) .appendPath(mediaId.toString())
.appendPath("edit") .appendPath("edit")
.toString() .toString()
private fun addUrl() = Uri.parse(baseModifyListUrl).buildUpon() private fun addUrl() = baseModifyListUrl.toUri().buildUpon()
.appendPath("add.json") .appendPath("add.json")
.toString() .toString()

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.track.shikimori package eu.kanade.tachiyomi.data.track.shikimori
import android.net.Uri import androidx.core.net.toUri
import com.github.salomonbrys.kotson.array import com.github.salomonbrys.kotson.array
import com.github.salomonbrys.kotson.jsonObject import com.github.salomonbrys.kotson.jsonObject
import com.github.salomonbrys.kotson.nullString import com.github.salomonbrys.kotson.nullString
@ -54,7 +54,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id) fun updateLibManga(track: Track, user_id: String): Observable<Track> = addLibManga(track, user_id)
fun search(search: String): Observable<List<TrackSearch>> { fun search(search: String): Observable<List<TrackSearch>> {
val url = Uri.parse("$apiUrl/mangas").buildUpon() val url = "$apiUrl/mangas".toUri().buildUpon()
.appendQueryParameter("order", "popularity") .appendQueryParameter("order", "popularity")
.appendQueryParameter("search", search) .appendQueryParameter("search", search)
.appendQueryParameter("limit", "20") .appendQueryParameter("limit", "20")
@ -102,7 +102,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
fun findLibManga(track: Track, user_id: String): Observable<Track?> { fun findLibManga(track: Track, user_id: String): Observable<Track?> {
val url = Uri.parse("$apiUrl/v2/user_rates").buildUpon() val url = "$apiUrl/v2/user_rates".toUri().buildUpon()
.appendQueryParameter("user_id", user_id) .appendQueryParameter("user_id", user_id)
.appendQueryParameter("target_id", track.media_id.toString()) .appendQueryParameter("target_id", track.media_id.toString())
.appendQueryParameter("target_type", "Manga") .appendQueryParameter("target_type", "Manga")
@ -112,7 +112,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
.get() .get()
.build() .build()
val urlMangas = Uri.parse("$apiUrl/mangas").buildUpon() val urlMangas = "$apiUrl/mangas".toUri().buildUpon()
.appendPath(track.media_id.toString()) .appendPath(track.media_id.toString())
.build() .build()
val requestMangas = Request.Builder() val requestMangas = Request.Builder()
@ -187,7 +187,7 @@ class ShikimoriApi(private val client: OkHttpClient, interceptor: ShikimoriInter
} }
fun authUrl() = fun authUrl() =
Uri.parse(loginUrl).buildUpon() loginUrl.toUri().buildUpon()
.appendQueryParameter("client_id", clientId) .appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", redirectUrl) .appendQueryParameter("redirect_uri", redirectUrl)
.appendQueryParameter("response_type", "code") .appendQueryParameter("response_type", "code")

View File

@ -33,14 +33,15 @@ internal class UpdaterNotifier(private val context: Context) {
* *
* @param title tile of notification. * @param title tile of notification.
*/ */
fun onDownloadStarted(title: String) { fun onDownloadStarted(title: String? = null): NotificationCompat.Builder {
with(notificationBuilder) { with(notificationBuilder) {
setContentTitle(title) title?.let { setContentTitle(title) }
setContentText(context.getString(R.string.update_check_notification_download_in_progress)) setContentText(context.getString(R.string.update_check_notification_download_in_progress))
setSmallIcon(android.R.drawable.stat_sys_download) setSmallIcon(android.R.drawable.stat_sys_download)
setOngoing(true) setOngoing(true)
} }
notificationBuilder.show() notificationBuilder.show()
return notificationBuilder
} }
/** /**

View File

@ -1,36 +1,82 @@
package eu.kanade.tachiyomi.data.updater package eu.kanade.tachiyomi.data.updater
import android.app.IntentService
import android.app.PendingIntent import android.app.PendingIntent
import android.app.Service
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.network.GET import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.ProgressListener import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.network.await
import eu.kanade.tachiyomi.network.newCallWithProgress import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
import eu.kanade.tachiyomi.util.storage.saveTo import eu.kanade.tachiyomi.util.storage.saveTo
import eu.kanade.tachiyomi.util.system.acquireWakeLock
import eu.kanade.tachiyomi.util.system.isServiceRunning
import java.io.File import java.io.File
import timber.log.Timber import timber.log.Timber
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class UpdaterService : IntentService(UpdaterService::class.java.name) { class UpdaterService : Service() {
private val network: NetworkHelper by injectLazy() private val network: NetworkHelper by injectLazy()
/** /**
* Notifier for the updater state and progress. * Wake lock that will be held until the service is destroyed.
*/ */
private val notifier by lazy { UpdaterNotifier(this) } private lateinit var wakeLock: PowerManager.WakeLock
override fun onHandleIntent(intent: Intent?) { private lateinit var notifier: UpdaterNotifier
if (intent == null) return
override fun onCreate() {
super.onCreate()
notifier = UpdaterNotifier(this)
wakeLock = acquireWakeLock(javaClass.name)
startForeground(Notifications.ID_UPDATER, notifier.onDownloadStarted().build())
}
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? = null
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return START_NOT_STICKY
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return START_NOT_STICKY
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name) val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return
downloadApk(title, url) launchIO {
downloadApk(title, url)
}
stopSelf(startId)
return START_NOT_STICKY
}
override fun stopService(name: Intent?): Boolean {
destroyJob()
return super.stopService(name)
}
override fun onDestroy() {
destroyJob()
super.onDestroy()
}
private fun destroyJob() {
if (wakeLock.isHeld) {
wakeLock.release()
}
} }
/** /**
@ -38,12 +84,11 @@ class UpdaterService : IntentService(UpdaterService::class.java.name) {
* *
* @param url url location of file * @param url url location of file
*/ */
private fun downloadApk(title: String, url: String) { private suspend fun downloadApk(title: String, url: String) {
// Show notification download starting. // Show notification download starting.
notifier.onDownloadStarted(title) notifier.onDownloadStarted(title)
val progressListener = object : ProgressListener { val progressListener = object : ProgressListener {
// Progress of the download // Progress of the download
var savedProgress = 0 var savedProgress = 0
@ -51,7 +96,7 @@ class UpdaterService : IntentService(UpdaterService::class.java.name) {
var lastTick = 0L var lastTick = 0L
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) { override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
val progress = (100 * bytesRead / contentLength).toInt() val progress = (100 * (bytesRead.toFloat() / contentLength)).toInt()
val currentTime = System.currentTimeMillis() val currentTime = System.currentTimeMillis()
if (progress > savedProgress && currentTime - 200 > lastTick) { if (progress > savedProgress && currentTime - 200 > lastTick) {
savedProgress = progress savedProgress = progress
@ -63,7 +108,7 @@ class UpdaterService : IntentService(UpdaterService::class.java.name) {
try { try {
// Download the new update. // Download the new update.
val response = network.client.newCallWithProgress(GET(url), progressListener).execute() val response = network.client.newCallWithProgress(GET(url), progressListener).await()
// File where the apk will be saved. // File where the apk will be saved.
val apkFile = File(externalCacheDir, "update.apk") val apkFile = File(externalCacheDir, "update.apk")
@ -82,27 +127,37 @@ class UpdaterService : IntentService(UpdaterService::class.java.name) {
} }
companion object { companion object {
/**
* Download url.
*/
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
/** internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_URL"
* Download title
*/
internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE" internal const val EXTRA_DOWNLOAD_TITLE = "${BuildConfig.APPLICATION_ID}.UpdaterService.DOWNLOAD_TITLE"
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
private fun isRunning(context: Context): Boolean =
context.isServiceRunning(UpdaterService::class.java)
/** /**
* Downloads a new update and let the user install the new version from a notification. * Downloads a new update and let the user install the new version from a notification.
*
* @param context the application context. * @param context the application context.
* @param url the url to the new update. * @param url the url to the new update.
*/ */
fun downloadUpdate(context: Context, url: String, title: String = context.getString(R.string.app_name)) { fun start(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
val intent = Intent(context, UpdaterService::class.java).apply { if (!isRunning(context)) {
putExtra(EXTRA_DOWNLOAD_TITLE, title) val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_URL, url) putExtra(EXTRA_DOWNLOAD_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url)
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
} }
context.startService(intent)
} }
/** /**

View File

@ -4,6 +4,7 @@ import android.content.Context
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi import eu.kanade.tachiyomi.extension.api.ExtensionGithubApi
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
@ -257,8 +258,7 @@ class ExtensionManager(
if (signature !in untrustedSignatures) return if (signature !in untrustedSignatures) return
ExtensionLoader.trustedSignatures += signature ExtensionLoader.trustedSignatures += signature
val preference = preferences.trustedSignatures() preferences.trustedSignatures() += signature
preference.set(preference.get() + signature)
val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature } val nowTrustedExtensions = untrustedExtensions.filter { it.signatureHash == signature }
untrustedExtensions -= nowTrustedExtensions untrustedExtensions -= nowTrustedExtensions

View File

@ -18,7 +18,8 @@ sealed class Extension {
val sources: List<Source>, val sources: List<Source>,
override val lang: String, override val lang: String,
val hasUpdate: Boolean = false, val hasUpdate: Boolean = false,
val isObsolete: Boolean = false val isObsolete: Boolean = false,
val isUnofficial: Boolean = false
) : Extension() ) : Extension()
data class Available( data class Available(

View File

@ -7,6 +7,8 @@ import android.content.Intent
import android.content.IntentFilter import android.content.IntentFilter
import android.net.Uri import android.net.Uri
import android.os.Environment import android.os.Environment
import androidx.core.content.getSystemService
import androidx.core.net.toUri
import com.jakewharton.rxrelay.PublishRelay import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.extension.model.Extension import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.extension.model.InstallStep import eu.kanade.tachiyomi.extension.model.InstallStep
@ -63,7 +65,7 @@ internal class ExtensionInstaller(private val context: Context) {
// Register the receiver after removing (and unregistering) the previous download // Register the receiver after removing (and unregistering) the previous download
downloadReceiver.register() downloadReceiver.register()
val downloadUri = Uri.parse(url) val downloadUri = url.toUri()
val request = DownloadManager.Request(downloadUri) val request = DownloadManager.Request(downloadUri)
.setTitle(extension.name) .setTitle(extension.name)
.setMimeType(APK_MIME) .setMimeType(APK_MIME)
@ -138,8 +140,7 @@ internal class ExtensionInstaller(private val context: Context) {
* @param pkgName The package name of the extension to uninstall * @param pkgName The package name of the extension to uninstall
*/ */
fun uninstallApk(pkgName: String) { fun uninstallApk(pkgName: String) {
val packageUri = Uri.parse("package:$pkgName") val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, "package:$pkgName".toUri())
val intent = Intent(Intent.ACTION_UNINSTALL_PACKAGE, packageUri)
.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) .setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
context.startActivity(intent) context.startActivity(intent)

View File

@ -31,13 +31,13 @@ internal object ExtensionLoader {
private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES private const val PACKAGE_FLAGS = PackageManager.GET_CONFIGURATIONS or PackageManager.GET_SIGNATURES
// inorichi's key
val officialSignature = "7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/** /**
* List of the trusted signatures. * List of the trusted signatures.
*/ */
var trustedSignatures = mutableSetOf<String>() + var trustedSignatures = mutableSetOf<String>() +
Injekt.get<PreferencesHelper>().trustedSignatures().get() + Injekt.get<PreferencesHelper>().trustedSignatures().get() + officialSignature
// inorichi's key
"7ce04da7773d41b489f4693a366c36bcd0a11fc39b547168553c285bd7348e23"
/** /**
* Return a list of all the installed extensions initialized concurrently. * Return a list of all the installed extensions initialized concurrently.
@ -159,7 +159,10 @@ internal object ExtensionLoader {
else -> "all" else -> "all"
} }
val extension = Extension.Installed(extName, pkgName, versionName, versionCode, sources, lang) val extension = Extension.Installed(
extName, pkgName, versionName, versionCode, sources, lang,
isUnofficial = signatureHash != officialSignature
)
return LoadResult.Success(extension) return LoadResult.Success(extension)
} }

View File

@ -12,9 +12,7 @@ class AndroidCookieJar : CookieJar {
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) { override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
val urlString = url.toString() val urlString = url.toString()
for (cookie in cookies) { cookies.forEach { manager.setCookie(urlString, it.toString()) }
manager.setCookie(urlString, cookie.toString())
}
} }
override fun loadForRequest(url: HttpUrl): List<Cookie> { override fun loadForRequest(url: HttpUrl): List<Cookie> {

View File

@ -2,16 +2,21 @@ package eu.kanade.tachiyomi.network
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
import android.os.Build
import android.os.Handler import android.os.Handler
import android.os.Looper import android.os.Looper
import android.webkit.WebResourceRequest
import android.webkit.WebResourceResponse
import android.webkit.WebSettings import android.webkit.WebSettings
import android.webkit.WebView import android.webkit.WebView
import android.widget.Toast import android.widget.Toast
import androidx.webkit.WebViewClientCompat
import androidx.webkit.WebViewFeature
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.util.system.WebViewClientCompat import eu.kanade.tachiyomi.util.lang.launchUI
import eu.kanade.tachiyomi.util.system.WebViewUtil
import eu.kanade.tachiyomi.util.system.isOutdated import eu.kanade.tachiyomi.util.system.isOutdated
import eu.kanade.tachiyomi.util.system.setDefaultSettings
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
import java.io.IOException import java.io.IOException
import java.util.concurrent.CountDownLatch import java.util.concurrent.CountDownLatch
@ -40,9 +45,17 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
@Synchronized @Synchronized
override fun intercept(chain: Interceptor.Chain): Response { override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
if (!WebViewUtil.supportsWebView(context)) {
launchUI {
context.toast(R.string.information_webview_required, Toast.LENGTH_LONG)
}
return chain.proceed(originalRequest)
}
initWebView initWebView
val originalRequest = chain.request()
val response = chain.proceed(originalRequest) val response = chain.proceed(originalRequest)
// Check if Cloudflare anti-bot is on // Check if Cloudflare anti-bot is on
@ -83,9 +96,9 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
handler.post { handler.post {
val webview = WebView(context) val webview = WebView(context)
webView = webview webView = webview
webview.settings.javaScriptEnabled = true webview.setDefaultSettings()
// Avoid set empty User-Agent, Chromium WebView will reset to default if empty // Avoid sending empty User-Agent, Chromium WebView will reset to default if empty
webview.settings.userAgentString = request.header("User-Agent") webview.settings.userAgentString = request.header("User-Agent")
?: HttpSource.DEFAULT_USERAGENT ?: HttpSource.DEFAULT_USERAGENT
@ -103,7 +116,7 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
} }
// HTTP error codes are only received since M // HTTP error codes are only received since M
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M && if (WebViewFeature.isFeatureSupported(WebViewFeature.RECEIVE_WEB_RESOURCE_ERROR) &&
url == origRequestUrl && !challengeFound url == origRequestUrl && !challengeFound
) { ) {
// The first request didn't return the challenge, abort. // The first request didn't return the challenge, abort.
@ -111,16 +124,14 @@ class CloudflareInterceptor(private val context: Context) : Interceptor {
} }
} }
override fun onReceivedErrorCompat( override fun onReceivedHttpError(
view: WebView, view: WebView,
errorCode: Int, request: WebResourceRequest,
description: String?, errorResponse: WebResourceResponse
failingUrl: String,
isMainFrame: Boolean
) { ) {
if (isMainFrame) { if (request.isForMainFrame) {
if (errorCode == 503) { if (errorResponse.statusCode == 503) {
// Found the cloudflare challenge page. // Found the Cloudflare challenge page.
challengeFound = true challengeFound = true
} else { } else {
// Unlock thread, the challenge wasn't found. // Unlock thread, the challenge wasn't found.

View File

@ -1,28 +1,70 @@
package eu.kanade.tachiyomi.network package eu.kanade.tachiyomi.network
import android.content.Context import android.content.Context
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import java.io.File import java.io.File
import java.net.InetAddress
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import okhttp3.Cache import okhttp3.Cache
import okhttp3.HttpUrl.Companion.toHttpUrl
import okhttp3.OkHttpClient import okhttp3.OkHttpClient
import okhttp3.dnsoverhttps.DnsOverHttps
import okhttp3.logging.HttpLoggingInterceptor
import uy.kohesive.injekt.injectLazy
class NetworkHelper(context: Context) { class NetworkHelper(context: Context) {
private val preferences: PreferencesHelper by injectLazy()
private val cacheDir = File(context.cacheDir, "network_cache") private val cacheDir = File(context.cacheDir, "network_cache")
private val cacheSize = 5L * 1024 * 1024 // 5 MiB private val cacheSize = 5L * 1024 * 1024 // 5 MiB
val cookieManager = AndroidCookieJar() val cookieManager = AndroidCookieJar()
val client = OkHttpClient.Builder() val client by lazy {
.cookieJar(cookieManager) val builder = OkHttpClient.Builder()
.cache(Cache(cacheDir, cacheSize)) .cookieJar(cookieManager)
.connectTimeout(30, TimeUnit.SECONDS) .cache(Cache(cacheDir, cacheSize))
.readTimeout(30, TimeUnit.SECONDS) .connectTimeout(30, TimeUnit.SECONDS)
.build() .readTimeout(30, TimeUnit.SECONDS)
.addInterceptor(UserAgentInterceptor())
val cloudflareClient = client.newBuilder() if (BuildConfig.DEBUG) {
.addInterceptor(UserAgentInterceptor()) val httpLoggingInterceptor = HttpLoggingInterceptor().apply {
.addInterceptor(CloudflareInterceptor(context)) level = HttpLoggingInterceptor.Level.HEADERS
.build() }
builder.addInterceptor(httpLoggingInterceptor)
}
if (preferences.enableDoh()) {
builder.dns(
DnsOverHttps.Builder().client(builder.build())
.url("https://cloudflare-dns.com/dns-query".toHttpUrl())
.bootstrapDnsHosts(
listOf(
InetAddress.getByName("162.159.36.1"),
InetAddress.getByName("162.159.46.1"),
InetAddress.getByName("1.1.1.1"),
InetAddress.getByName("1.0.0.1"),
InetAddress.getByName("162.159.132.53"),
InetAddress.getByName("2606:4700:4700::1111"),
InetAddress.getByName("2606:4700:4700::1001"),
InetAddress.getByName("2606:4700:4700::0064"),
InetAddress.getByName("2606:4700:4700::6400")
)
)
.build()
)
}
builder.build()
}
val cloudflareClient by lazy {
client.newBuilder()
.addInterceptor(CloudflareInterceptor(context))
.build()
}
} }

View File

@ -1,8 +1,7 @@
package eu.kanade.tachiyomi.source package eu.kanade.tachiyomi.source
import android.content.Context import android.content.Context
import com.google.gson.Gson import com.google.gson.JsonParser
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.model.Filter import eu.kanade.tachiyomi.source.model.Filter
import eu.kanade.tachiyomi.source.model.FilterList import eu.kanade.tachiyomi.source.model.FilterList
@ -19,7 +18,6 @@ import java.io.File
import java.io.FileInputStream import java.io.FileInputStream
import java.io.InputStream import java.io.InputStream
import java.util.Locale import java.util.Locale
import java.util.Scanner
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
import java.util.zip.ZipEntry import java.util.zip.ZipEntry
import java.util.zip.ZipFile import java.util.zip.ZipFile
@ -30,13 +28,15 @@ import timber.log.Timber
class LocalSource(private val context: Context) : CatalogueSource { class LocalSource(private val context: Context) : CatalogueSource {
companion object { companion object {
const val ID = 0L
const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/" const val HELP_URL = "https://tachiyomi.org/help/guides/reading-local-manga/"
private const val COVER_NAME = "cover.jpg" private const val COVER_NAME = "cover.jpg"
private val SUPPORTED_ARCHIVE_TYPES = setOf("zip", "rar", "cbr", "cbz", "epub")
private val POPULAR_FILTERS = FilterList(OrderBy()) private val POPULAR_FILTERS = FilterList(OrderBy())
private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) }) private val LATEST_FILTERS = FilterList(OrderBy().apply { state = Filter.Sort.Selection(1, false) })
private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS) private val LATEST_THRESHOLD = TimeUnit.MILLISECONDS.convert(7, TimeUnit.DAYS)
const val ID = 0L
fun updateCover(context: Context, manga: SManga, input: InputStream): File? { fun updateCover(context: Context, manga: SManga, input: InputStream): File? {
val dir = getBaseDirectories(context).firstOrNull() val dir = getBaseDirectories(context).firstOrNull()
@ -47,7 +47,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME) val cover = File("${dir.absolutePath}/${manga.url}", COVER_NAME)
// It might not exist if using the external SD card // It might not exist if using the external SD card
cover.parentFile.mkdirs() cover.parentFile?.mkdirs()
input.use { input.use {
cover.outputStream().use { cover.outputStream().use {
input.copyTo(it) input.copyTo(it)
@ -75,9 +75,12 @@ class LocalSource(private val context: Context) : CatalogueSource {
val baseDirs = getBaseDirectories(context) val baseDirs = getBaseDirectories(context)
val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L val time = if (filters === LATEST_FILTERS) System.currentTimeMillis() - LATEST_THRESHOLD else 0L
var mangaDirs = baseDirs.mapNotNull { it.listFiles()?.toList() } var mangaDirs = baseDirs
.asSequence()
.mapNotNull { it.listFiles()?.toList() }
.flatten() .flatten()
.filter { it.isDirectory && if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time } .filter { it.isDirectory }
.filter { if (time == 0L) it.name.contains(query, ignoreCase = true) else it.lastModified() >= time }
.distinctBy { it.name } .distinctBy { it.name }
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
@ -134,18 +137,22 @@ class LocalSource(private val context: Context) : CatalogueSource {
} }
} }
} }
return Observable.just(MangasPage(mangas, false))
return Observable.just(MangasPage(mangas.toList(), false))
} }
override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS) override fun fetchLatestUpdates(page: Int) = fetchSearchManga(page, "", LATEST_FILTERS)
override fun fetchMangaDetails(manga: SManga): Observable<SManga> { override fun fetchMangaDetails(manga: SManga): Observable<SManga> {
getBaseDirectories(context) getBaseDirectories(context)
.asSequence()
.mapNotNull { File(it, manga.url).listFiles()?.toList() } .mapNotNull { File(it, manga.url).listFiles()?.toList() }
.flatten() .flatten()
.firstOrNull { it.extension == "json" } .firstOrNull { it.extension == "json" }
?.apply { ?.apply {
val json = Gson().fromJson(Scanner(this).useDelimiter("\\Z").next(), JsonObject::class.java) val reader = this.inputStream().bufferedReader()
val json = JsonParser.parseReader(reader).asJsonObject
manga.title = json["title"]?.asString ?: manga.title manga.title = json["title"]?.asString ?: manga.title
manga.author = json["author"]?.asString ?: manga.author manga.author = json["author"]?.asString ?: manga.author
manga.artist = json["artist"]?.asString ?: manga.artist manga.artist = json["artist"]?.asString ?: manga.artist
@ -154,6 +161,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
?: manga.genre ?: manga.genre
manga.status = json["status"]?.asInt ?: manga.status manga.status = json["status"]?.asInt ?: manga.status
} }
return Observable.just(manga) return Observable.just(manga)
} }
@ -204,8 +212,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
var chapterNameIndex = 0 var chapterNameIndex = 0
var mangaTitleIndex = 0 var mangaTitleIndex = 0
while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) { while (chapterNameIndex < chapterName.length && mangaTitleIndex < mangaTitle.length) {
val chapterChar = chapterName.get(chapterNameIndex) val chapterChar = chapterName[chapterNameIndex]
val mangaChar = mangaTitle.get(mangaTitleIndex) val mangaChar = mangaTitle[mangaTitleIndex]
if (!chapterChar.equals(mangaChar, true)) { if (!chapterChar.equals(mangaChar, true)) {
val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace() val invalidChapterChar = !chapterChar.isLetterOrDigit() && !chapterChar.isWhitespace()
val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace() val invalidMangaChar = !mangaChar.isLetterOrDigit() && !mangaChar.isWhitespace()
@ -235,7 +243,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
} }
private fun isSupportedFile(extension: String): Boolean { private fun isSupportedFile(extension: String): Boolean {
return extension.toLowerCase() in setOf("zip", "rar", "cbr", "cbz", "epub") return extension.toLowerCase() in SUPPORTED_ARCHIVE_TYPES
} }
fun getFormat(chapter: SChapter): Format { fun getFormat(chapter: SChapter): Format {
@ -269,8 +277,8 @@ class LocalSource(private val context: Context) : CatalogueSource {
return when (val format = getFormat(chapter)) { return when (val format = getFormat(chapter)) {
is Format.Directory -> { is Format.Directory -> {
val entry = format.file.listFiles() val entry = format.file.listFiles()
.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) }) ?.sortedWith(Comparator<File> { f1, f2 -> f1.name.compareToCaseInsensitiveNaturalOrder(f2.name) })
.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } } ?.find { !it.isDirectory && ImageUtil.isImage(it.name) { FileInputStream(it) } }
entry?.let { updateCover(context, manga, it.inputStream()) } entry?.let { updateCover(context, manga, it.inputStream()) }
} }

View File

@ -47,3 +47,5 @@ interface Source {
} }
fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this) fun Source.icon(): Drawable? = Injekt.get<ExtensionManager>().getAppIconForSource(this)
fun Source.getPreferenceKey(): String = "source_$id"

View File

@ -34,7 +34,7 @@ abstract class HttpSource : CatalogueSource {
// * Preferences that a source may need. // * Preferences that a source may need.
// */ // */
// val preferences: SharedPreferences by lazy { // val preferences: SharedPreferences by lazy {
// Injekt.get<Application>().getSharedPreferences("source_$id", Context.MODE_PRIVATE) // Injekt.get<Application>().getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
// } // }
/** /**

View File

@ -25,7 +25,7 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
private val lightTheme: Int by lazy { private val lightTheme: Int by lazy {
when (preferences.themeLight().get()) { when (preferences.themeLight().get()) {
Values.THEME_LIGHT_BLUE -> R.style.Theme_Tachiyomi_LightBlue Values.LightThemeVariant.blue -> R.style.Theme_Tachiyomi_LightBlue
else -> { else -> {
when { when {
// Light status + navigation bar // Light status + navigation bar
@ -47,8 +47,8 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
private val darkTheme: Int by lazy { private val darkTheme: Int by lazy {
when (preferences.themeDark().get()) { when (preferences.themeDark().get()) {
Values.THEME_DARK_BLUE -> R.style.Theme_Tachiyomi_DarkBlue Values.DarkThemeVariant.blue -> R.style.Theme_Tachiyomi_DarkBlue
Values.THEME_DARK_AMOLED -> R.style.Theme_Tachiyomi_Amoled Values.DarkThemeVariant.amoled -> R.style.Theme_Tachiyomi_Amoled
else -> R.style.Theme_Tachiyomi_Dark else -> R.style.Theme_Tachiyomi_Dark
} }
} }
@ -61,14 +61,14 @@ abstract class BaseActivity<VB : ViewBinding> : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) { override fun onCreate(savedInstanceState: Bundle?) {
setTheme( setTheme(
when (preferences.themeMode().get()) { when (preferences.themeMode().get()) {
Values.THEME_MODE_SYSTEM -> { Values.ThemeMode.system -> {
if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) { if (resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK == Configuration.UI_MODE_NIGHT_YES) {
darkTheme darkTheme
} else { } else {
lightTheme lightTheme
} }
} }
Values.THEME_MODE_DARK -> darkTheme Values.ThemeMode.dark -> darkTheme
else -> lightTheme else -> lightTheme
} }
) )

View File

@ -64,8 +64,9 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) { override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) { if (type.isEnter) {
setTitle() setTitle()
setHasOptionsMenu(true)
} }
setHasOptionsMenu(type.isEnter)
super.onChangeStarted(handler, type) super.onChangeStarted(handler, type)
} }
@ -73,7 +74,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
return null return null
} }
fun setTitle() { fun setTitle(title: String? = null) {
var parentController = parentController var parentController = parentController
while (parentController != null) { while (parentController != null) {
if (parentController is BaseController<*> && parentController.getTitle() != null) { if (parentController is BaseController<*> && parentController.getTitle() != null) {
@ -82,7 +83,7 @@ abstract class BaseController<VB : ViewBinding>(bundle: Bundle? = null) :
parentController = parentController.parentController parentController = parentController.parentController
} }
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle() (activity as? AppCompatActivity)?.supportActionBar?.title = title ?: getTitle()
} }
private fun Controller.instance(): String { private fun Controller.instance(): String {

View File

@ -28,8 +28,8 @@ fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: I
} }
} }
fun Controller.withFadeTransaction(): RouterTransaction { fun Controller.withFadeTransaction(duration: Long = 150L): RouterTransaction {
return RouterTransaction.with(this) return RouterTransaction.with(this)
.pushChangeHandler(FadeChangeHandler()) .pushChangeHandler(FadeChangeHandler(duration))
.popChangeHandler(FadeChangeHandler()) .popChangeHandler(FadeChangeHandler(duration))
} }

View File

@ -98,7 +98,7 @@ abstract class DialogController : RestoreViewOnCreateController {
/** /**
* Dismiss the dialog and pop this controller * Dismiss the dialog and pop this controller
*/ */
fun dismissDialog() { private fun dismissDialog() {
if (dismissed) { if (dismissed) {
return return
} }

View File

@ -0,0 +1,10 @@
package eu.kanade.tachiyomi.ui.base.controller
import com.google.android.material.floatingactionbutton.ExtendedFloatingActionButton
interface FabController {
fun configureFab(fab: ExtendedFloatingActionButton) {}
fun cleanupFab(fab: ExtendedFloatingActionButton) {}
}

View File

@ -10,25 +10,7 @@ import rx.subscriptions.CompositeSubscription
abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseController<VB>(bundle) { abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseController<VB>(bundle) {
var untilDetachSubscriptions = CompositeSubscription() private var untilDestroySubscriptions = CompositeSubscription()
private set
var untilDestroySubscriptions = CompositeSubscription()
private set
@CallSuper
override fun onAttach(view: View) {
super.onAttach(view)
if (untilDetachSubscriptions.isUnsubscribed) {
untilDetachSubscriptions = CompositeSubscription()
}
}
@CallSuper
override fun onDetach(view: View) {
super.onDetach(view)
untilDetachSubscriptions.unsubscribe()
}
@CallSuper @CallSuper
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
@ -43,49 +25,7 @@ abstract class RxController<VB : ViewBinding>(bundle: Bundle? = null) : BaseCont
untilDestroySubscriptions.unsubscribe() untilDestroySubscriptions.unsubscribe()
} }
fun <T> Observable<T>.subscribeUntilDetach(): Subscription {
return subscribe().also { untilDetachSubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDetach(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDetachSubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDetach(
onNext: (T) -> Unit,
onError: (Throwable) -> Unit
): Subscription {
return subscribe(onNext, onError).also { untilDetachSubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDetach(
onNext: (T) -> Unit,
onError: (Throwable) -> Unit,
onCompleted: () -> Unit
): Subscription {
return subscribe(onNext, onError, onCompleted).also { untilDetachSubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDestroy(): Subscription {
return subscribe().also { untilDestroySubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription { fun <T> Observable<T>.subscribeUntilDestroy(onNext: (T) -> Unit): Subscription {
return subscribe(onNext).also { untilDestroySubscriptions.add(it) } return subscribe(onNext).also { untilDestroySubscriptions.add(it) }
} }
fun <T> Observable<T>.subscribeUntilDestroy(
onNext: (T) -> Unit,
onError: (Throwable) -> Unit
): Subscription {
return subscribe(onNext, onError).also { untilDestroySubscriptions.add(it) }
}
fun <T> Observable<T>.subscribeUntilDestroy(
onNext: (T) -> Unit,
onError: (Throwable) -> Unit,
onCompleted: () -> Unit
): Subscription {
return subscribe(onNext, onError, onCompleted).also { untilDestroySubscriptions.add(it) }
}
} }

View File

@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.ui.base.controller.RootController
import eu.kanade.tachiyomi.ui.base.controller.RxController import eu.kanade.tachiyomi.ui.base.controller.RxController
import eu.kanade.tachiyomi.ui.base.controller.TabbedController import eu.kanade.tachiyomi.ui.base.controller.TabbedController
import eu.kanade.tachiyomi.ui.browse.extension.ExtensionController import eu.kanade.tachiyomi.ui.browse.extension.ExtensionController
import eu.kanade.tachiyomi.ui.browse.migration.sources.MigrationSourcesController
import eu.kanade.tachiyomi.ui.browse.source.SourceController import eu.kanade.tachiyomi.ui.browse.source.SourceController
import kotlinx.android.synthetic.main.main_activity.tabs import kotlinx.android.synthetic.main.main_activity.tabs
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -95,10 +96,6 @@ class BrowseController :
tabs.getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge() tabs.getTabAt(EXTENSIONS_CONTROLLER)?.removeBadge()
} }
fun pushController(transaction: RouterTransaction) {
router.pushController(transaction)
}
fun setExtensionUpdateBadge() { fun setExtensionUpdateBadge() {
activity?.tabs?.apply { activity?.tabs?.apply {
val updates = preferences.extensionUpdatesCount().get() val updates = preferences.extensionUpdatesCount().get()
@ -115,7 +112,8 @@ class BrowseController :
private val tabTitles = listOf( private val tabTitles = listOf(
R.string.label_sources, R.string.label_sources,
R.string.label_extensions R.string.label_extensions,
R.string.label_migration
) )
.map { resources!!.getString(it) } .map { resources!!.getString(it) }
@ -128,6 +126,7 @@ class BrowseController :
val controller: Controller = when (position) { val controller: Controller = when (position) {
SOURCES_CONTROLLER -> SourceController() SOURCES_CONTROLLER -> SourceController()
EXTENSIONS_CONTROLLER -> ExtensionController() EXTENSIONS_CONTROLLER -> ExtensionController()
MIGRATION_CONTROLLER -> MigrationSourcesController()
else -> error("Wrong position $position") else -> error("Wrong position $position")
} }
router.setRoot(RouterTransaction.with(controller)) router.setRoot(RouterTransaction.with(controller))
@ -144,5 +143,6 @@ class BrowseController :
const val SOURCES_CONTROLLER = 0 const val SOURCES_CONTROLLER = 0
const val EXTENSIONS_CONTROLLER = 1 const val EXTENSIONS_CONTROLLER = 1
const val MIGRATION_CONTROLLER = 2
} }
} }

View File

@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.BrowseController import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.extension.details.ExtensionDetailsController
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
@ -64,17 +65,17 @@ open class ExtensionController :
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
binding.extSwipeRefresh.isRefreshing = true binding.swipeRefresh.isRefreshing = true
binding.extSwipeRefresh.refreshes() binding.swipeRefresh.refreshes()
.onEach { presenter.findAvailableExtensions() } .onEach { presenter.findAvailableExtensions() }
.launchIn(scope) .launchIn(scope)
// Initialize adapter, scroll listener and recycler views // Initialize adapter, scroll listener and recycler views
adapter = ExtensionAdapter(this) adapter = ExtensionAdapter(this)
// Create recycler and set adapter. // Create recycler and set adapter.
binding.extRecycler.layoutManager = LinearLayoutManager(view.context) binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.extRecycler.adapter = adapter binding.recycler.adapter = adapter
binding.extRecycler.addItemDecoration(ExtensionDividerItemDecoration(view.context)) binding.recycler.addItemDecoration(ExtensionDividerItemDecoration(view.context))
adapter?.fastScroller = binding.fastScroller adapter?.fastScroller = binding.fastScroller
} }
@ -87,11 +88,10 @@ open class ExtensionController :
when (item.itemId) { when (item.itemId) {
R.id.action_search -> expandActionViewFromInteraction = true R.id.action_search -> expandActionViewFromInteraction = true
R.id.action_settings -> { R.id.action_settings -> {
(parentController as BrowseController).pushController( parentController!!.router.pushController(
ExtensionFilterController().withFadeTransaction() ExtensionFilterController().withFadeTransaction()
) )
} }
else -> return super.onOptionsItemSelected(item)
} }
return super.onOptionsItemSelected(item) return super.onOptionsItemSelected(item)
} }
@ -129,6 +129,9 @@ open class ExtensionController :
val searchView = searchItem.actionView as SearchView val searchView = searchItem.actionView as SearchView
searchView.maxWidth = Int.MAX_VALUE searchView.maxWidth = Int.MAX_VALUE
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
if (query.isNotEmpty()) { if (query.isNotEmpty()) {
searchItem.expandActionView() searchItem.expandActionView()
searchView.setQuery(query, true) searchView.setQuery(query, true)
@ -142,9 +145,6 @@ open class ExtensionController :
drawExtensions() drawExtensions()
} }
.launchIn(scope) .launchIn(scope)
// Fixes problem with the overflow icon showing up in lieu of search
searchItem.fixExpand(onExpand = { invalidateMenuOnExpand() })
} }
override fun onItemClick(view: View, position: Int): Boolean { override fun onItemClick(view: View, position: Int): Boolean {
@ -167,7 +167,7 @@ open class ExtensionController :
private fun openDetails(extension: Extension.Installed) { private fun openDetails(extension: Extension.Installed) {
val controller = ExtensionDetailsController(extension.pkgName) val controller = ExtensionDetailsController(extension.pkgName)
(parentController as BrowseController).pushController(controller.withFadeTransaction()) parentController!!.router.pushController(controller.withFadeTransaction())
} }
private fun openTrustDialog(extension: Extension.Untrusted) { private fun openTrustDialog(extension: Extension.Untrusted) {
@ -176,7 +176,7 @@ open class ExtensionController :
} }
fun setExtensions(extensions: List<ExtensionItem>) { fun setExtensions(extensions: List<ExtensionItem>) {
binding.extSwipeRefresh.isRefreshing = false binding.swipeRefresh.isRefreshing = false
this.extensions = extensions this.extensions = extensions
drawExtensions() drawExtensions()

View File

@ -5,6 +5,7 @@ import android.graphics.Canvas
import android.graphics.Rect import android.graphics.Rect
import android.graphics.drawable.Drawable import android.graphics.drawable.Drawable
import android.view.View import android.view.View
import androidx.core.view.marginBottom
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() { class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecoration() {
@ -25,8 +26,7 @@ class ExtensionDividerItemDecoration(context: Context) : RecyclerView.ItemDecora
if (holder is ExtensionHolder && if (holder is ExtensionHolder &&
parent.getChildViewHolder(parent.getChildAt(i + 1)) is ExtensionHolder parent.getChildViewHolder(parent.getChildAt(i + 1)) is ExtensionHolder
) { ) {
val params = child.layoutParams as RecyclerView.LayoutParams val top = child.bottom + child.marginBottom
val top = child.bottom + params.bottomMargin
val bottom = top + divider.intrinsicHeight val bottom = top + divider.intrinsicHeight
val left = parent.paddingStart + holder.margin val left = parent.paddingStart + holder.margin
val right = parent.width - parent.paddingEnd - holder.margin val right = parent.width - parent.paddingEnd - holder.margin

View File

@ -14,18 +14,16 @@ import uy.kohesive.injekt.api.get
class ExtensionFilterController : SettingsController() { class ExtensionFilterController : SettingsController() {
override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) { override fun setupPreferenceScreen(screen: PreferenceScreen) = with(screen) {
titleRes = R.string.action_filter titleRes = R.string.label_extensions
val activeLangs = preferences.enabledLanguages().get() val activeLangs = preferences.enabledLanguages().get()
val availableLangs = val availableLangs =
Injekt.get<ExtensionManager>().availableExtensions.groupBy { Injekt.get<ExtensionManager>().availableExtensions.groupBy {
it.lang it.lang
}.keys.minus("all").partition { }.keys
it in activeLangs .minus("all")
}.let { .sortedWith(compareBy({ it !in activeLangs }, { LocaleHelper.getSourceDisplayName(it, context) }))
it.first + it.second
}
availableLangs.forEach { availableLangs.forEach {
switchPreference { switchPreference {
@ -38,11 +36,13 @@ class ExtensionFilterController : SettingsController() {
val checked = newValue as Boolean val checked = newValue as Boolean
val currentActiveLangs = preferences.enabledLanguages().get() val currentActiveLangs = preferences.enabledLanguages().get()
if (checked) { preferences.enabledLanguages().set(
preferences.enabledLanguages().set(currentActiveLangs + it) if (checked) {
} else { currentActiveLangs + it
preferences.enabledLanguages().set(currentActiveLangs - it) } else {
} currentActiveLangs - it
}
)
true true
} }
} }

View File

@ -8,7 +8,6 @@ import eu.kanade.tachiyomi.extension.model.InstallStep
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
import eu.kanade.tachiyomi.util.system.LocaleHelper import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.system.getResourceColor
import io.github.mthli.slice.Slice import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.extension_card_item.card import kotlinx.android.synthetic.main.extension_card_item.card
import kotlinx.android.synthetic.main.extension_card_item.ext_button import kotlinx.android.synthetic.main.extension_card_item.ext_button
@ -16,6 +15,7 @@ import kotlinx.android.synthetic.main.extension_card_item.ext_title
import kotlinx.android.synthetic.main.extension_card_item.image import kotlinx.android.synthetic.main.extension_card_item.image
import kotlinx.android.synthetic.main.extension_card_item.lang import kotlinx.android.synthetic.main.extension_card_item.lang
import kotlinx.android.synthetic.main.extension_card_item.version import kotlinx.android.synthetic.main.extension_card_item.version
import kotlinx.android.synthetic.main.extension_card_item.warning
class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) : class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
BaseFlexibleViewHolder(view, adapter), BaseFlexibleViewHolder(view, adapter),
@ -38,13 +38,14 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
val extension = item.extension val extension = item.extension
setCardEdges(item) setCardEdges(item)
// Set source name
ext_title.text = extension.name ext_title.text = extension.name
version.text = extension.versionName version.text = extension.versionName
lang.text = if (extension !is Extension.Untrusted) { lang.text = LocaleHelper.getSourceDisplayName(extension.lang, itemView.context)
LocaleHelper.getSourceDisplayName(extension.lang, itemView.context) warning.text = when {
} else { extension is Extension.Untrusted -> itemView.context.getString(R.string.ext_untrusted).toUpperCase()
itemView.context.getString(R.string.ext_untrusted).toUpperCase() extension is Extension.Installed && extension.isObsolete -> itemView.context.getString(R.string.ext_obsolete).toUpperCase()
extension is Extension.Installed && extension.isUnofficial -> itemView.context.getString(R.string.ext_unofficial).toUpperCase()
else -> null
} }
GlideApp.with(itemView.context).clear(image) GlideApp.with(itemView.context).clear(image)
@ -63,8 +64,6 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
isEnabled = true isEnabled = true
isClickable = true isClickable = true
setTextColor(context.getResourceColor(R.attr.colorAccent))
val extension = item.extension val extension = item.extension
val installStep = item.installStep val installStep = item.installStep
@ -87,12 +86,8 @@ class ExtensionHolder(view: View, override val adapter: ExtensionAdapter) :
extension.hasUpdate -> { extension.hasUpdate -> {
setText(R.string.ext_update) setText(R.string.ext_update)
} }
extension.isObsolete -> {
setTextColor(context.getResourceColor(R.attr.colorError))
setText(R.string.ext_obsolete)
}
else -> { else -> {
setText(R.string.ext_details) setText(R.string.action_settings)
} }
} }
} else if (extension is Extension.Untrusted) { } else if (extension is Extension.Untrusted) {

View File

@ -0,0 +1,229 @@
package eu.kanade.tachiyomi.ui.browse.extension.details
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.Bundle
import android.provider.Settings
import android.util.TypedValue
import android.view.LayoutInflater
import android.view.Menu
import android.view.MenuInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import androidx.appcompat.view.ContextThemeWrapper
import androidx.preference.Preference
import androidx.preference.PreferenceGroupAdapter
import androidx.preference.PreferenceManager
import androidx.preference.PreferenceScreen
import androidx.preference.SwitchPreferenceCompat
import androidx.recyclerview.widget.ConcatAdapter
import androidx.recyclerview.widget.DividerItemDecoration
import androidx.recyclerview.widget.DividerItemDecoration.VERTICAL
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding
import eu.kanade.tachiyomi.extension.model.Extension
import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.ui.base.controller.NoToolbarElevationController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.util.preference.DSL
import eu.kanade.tachiyomi.util.preference.onChange
import eu.kanade.tachiyomi.util.preference.preferenceCategory
import eu.kanade.tachiyomi.util.preference.switchPreference
import eu.kanade.tachiyomi.util.preference.switchSettingsPreference
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import uy.kohesive.injekt.injectLazy
@SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) :
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle),
NoToolbarElevationController {
private val preferences: PreferencesHelper by injectLazy()
private var preferenceScreen: PreferenceScreen? = null
constructor(pkgName: String) : this(
Bundle().apply {
putString(PKGNAME_KEY, pkgName)
}
)
init {
setHasOptionsMenu(true)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = ExtensionDetailControllerBinding.inflate(themedInflater)
return binding.root
}
override fun createPresenter(): ExtensionDetailsPresenter {
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!)
}
override fun getTitle(): String? {
return resources?.getString(R.string.label_extension_info)
}
@SuppressLint("PrivateResource")
override fun onViewCreated(view: View) {
super.onViewCreated(view)
val extension = presenter.extension ?: return
val context = view.context
binding.extensionPrefsRecycler.layoutManager = LinearLayoutManager(context)
binding.extensionPrefsRecycler.adapter = ConcatAdapter(
ExtensionDetailsHeaderAdapter(presenter),
initPreferencesAdapter(context, extension)
)
binding.extensionPrefsRecycler.addItemDecoration(DividerItemDecoration(context, VERTICAL))
}
private fun initPreferencesAdapter(context: Context, extension: Extension.Installed): PreferenceGroupAdapter {
val themedContext = getPreferenceThemeContext()
val manager = PreferenceManager(themedContext)
manager.preferenceDataStore = EmptyPreferenceDataStore()
val screen = manager.createPreferenceScreen(themedContext)
preferenceScreen = screen
val isMultiSource = extension.sources.size > 1
val isMultiLangSingleSource = isMultiSource && extension.sources.map { it.name }.distinct().size == 1
with(screen) {
extension.sources
.groupBy { (it as CatalogueSource).lang }
.toSortedMap(compareBy { LocaleHelper.getSourceDisplayName(it, context) })
.forEach {
val preferenceBlock = {
it.value
.sortedWith(compareBy({ !it.isEnabled() }, { it.name }))
.forEach { source ->
val sourcePrefs = mutableListOf<Preference>()
val block: (@DSL SwitchPreferenceCompat).() -> Unit = {
key = source.getPreferenceKey()
title = when {
isMultiSource && !isMultiLangSingleSource -> source.toString()
else -> LocaleHelper.getSourceDisplayName(it.key, context)
}
isPersistent = false
isChecked = source.isEnabled()
onChange { newValue ->
val checked = newValue as Boolean
toggleSource(source, checked)
true
}
// React to enable/disable all changes
preferences.disabledSources().asFlow()
.onEach {
val enabled = source.isEnabled()
isChecked = enabled
sourcePrefs.forEach { pref -> pref.isVisible = enabled }
}
.launchIn(scope)
}
// Source enable/disable
if (source is ConfigurableSource) {
switchSettingsPreference {
block()
onSettingsClick = View.OnClickListener {
router.pushController(
SourcePreferencesController(source.id).withFadeTransaction()
)
}
}
} else {
switchPreference(block)
}
}
}
if (isMultiSource && !isMultiLangSingleSource) {
preferenceCategory {
title = LocaleHelper.getSourceDisplayName(it.key, context)
preferenceBlock()
}
} else {
preferenceBlock()
}
}
}
return PreferenceGroupAdapter(screen)
}
override fun onDestroyView(view: View) {
preferenceScreen = null
super.onDestroyView(view)
}
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
inflater.inflate(R.menu.extension_details, menu)
}
override fun onOptionsItemSelected(item: MenuItem): Boolean {
when (item.itemId) {
R.id.action_enable_all -> toggleAllSources(true)
R.id.action_disable_all -> toggleAllSources(false)
R.id.action_open_in_settings -> openInSettings()
}
return super.onOptionsItemSelected(item)
}
fun onExtensionUninstalled() {
router.popCurrentController()
}
private fun toggleAllSources(enable: Boolean) {
presenter.extension?.sources?.forEach { toggleSource(it, enable) }
}
private fun toggleSource(source: Source, enable: Boolean) {
if (enable) {
preferences.disabledSources() -= source.id.toString()
} else {
preferences.disabledSources() += source.id.toString()
}
}
private fun openInSettings() {
val intent = Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS).apply {
data = Uri.fromParts("package", presenter.pkgName, null)
}
startActivity(intent)
}
private fun Source.isEnabled(): Boolean {
return id.toString() !in preferences.disabledSources().get()
}
private fun getPreferenceThemeContext(): Context {
val tv = TypedValue()
activity!!.theme.resolveAttribute(R.attr.preferenceTheme, tv, true)
return ContextThemeWrapper(activity, tv.resourceId)
}
private companion object {
const val PKGNAME_KEY = "pkg_name"
}
}

View File

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.ui.browse.extension.details
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.databinding.ExtensionDetailHeaderBinding
import eu.kanade.tachiyomi.ui.browse.extension.getApplicationIcon
import eu.kanade.tachiyomi.util.system.LocaleHelper
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
class ExtensionDetailsHeaderAdapter(private val presenter: ExtensionDetailsPresenter) :
RecyclerView.Adapter<ExtensionDetailsHeaderAdapter.HeaderViewHolder>() {
private val scope = CoroutineScope(Job() + Dispatchers.Main)
private lateinit var binding: ExtensionDetailHeaderBinding
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): HeaderViewHolder {
binding = ExtensionDetailHeaderBinding.inflate(LayoutInflater.from(parent.context), parent, false)
return HeaderViewHolder(binding.root)
}
override fun getItemCount(): Int = 1
override fun onBindViewHolder(holder: HeaderViewHolder, position: Int) {
holder.bind()
}
inner class HeaderViewHolder(private val view: View) : RecyclerView.ViewHolder(view) {
fun bind() {
val extension = presenter.extension ?: return
val context = view.context
extension.getApplicationIcon(context)?.let { binding.extensionIcon.setImageDrawable(it) }
binding.extensionTitle.text = extension.name
binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName)
binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
binding.extensionPkg.text = extension.pkgName
binding.extensionUninstallButton.clicks()
.onEach { presenter.uninstallExtension() }
.launchIn(scope)
if (extension.isObsolete) {
binding.extensionWarningBanner.isVisible = true
binding.extensionWarningBanner.setText(R.string.obsolete_extension_message)
}
if (extension.isUnofficial) {
binding.extensionWarningBanner.isVisible = true
binding.extensionWarningBanner.setText(R.string.unofficial_extension_message)
}
}
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.browse.extension package eu.kanade.tachiyomi.ui.browse.extension.details
import android.os.Bundle import android.os.Bundle
import eu.kanade.tachiyomi.extension.ExtensionManager import eu.kanade.tachiyomi.extension.ExtensionManager

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.browse.extension package eu.kanade.tachiyomi.ui.browse.extension.details
import android.annotation.SuppressLint import android.annotation.SuppressLint
import android.content.Context import android.content.Context
@ -25,20 +25,16 @@ import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore import eu.kanade.tachiyomi.data.preference.EmptyPreferenceDataStore
import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore import eu.kanade.tachiyomi.data.preference.SharedPreferencesDataStore
import eu.kanade.tachiyomi.databinding.ExtensionDetailControllerBinding import eu.kanade.tachiyomi.databinding.SourcePreferencesControllerBinding
import eu.kanade.tachiyomi.source.ConfigurableSource import eu.kanade.tachiyomi.source.ConfigurableSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.getPreferenceKey
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.util.preference.preferenceCategory import timber.log.Timber
import eu.kanade.tachiyomi.util.system.LocaleHelper
import eu.kanade.tachiyomi.util.view.visible
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.android.view.clicks
@SuppressLint("RestrictedApi") @SuppressLint("RestrictedApi")
class ExtensionDetailsController(bundle: Bundle? = null) : class SourcePreferencesController(bundle: Bundle? = null) :
NucleusController<ExtensionDetailControllerBinding, ExtensionDetailsPresenter>(bundle), NucleusController<SourcePreferencesControllerBinding, SourcePreferencesPresenter>(bundle),
PreferenceManager.OnDisplayPreferenceDialogListener, PreferenceManager.OnDisplayPreferenceDialogListener,
DialogPreference.TargetFragment { DialogPreference.TargetFragment {
@ -46,46 +42,33 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
private var preferenceScreen: PreferenceScreen? = null private var preferenceScreen: PreferenceScreen? = null
constructor(pkgName: String) : this( constructor(sourceId: Long) : this(
Bundle().apply { Bundle().apply {
putString(PKGNAME_KEY, pkgName) putLong(SOURCE_ID, sourceId)
} }
) )
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View { override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
val themedInflater = inflater.cloneInContext(getPreferenceThemeContext()) val themedInflater = inflater.cloneInContext(getPreferenceThemeContext())
binding = ExtensionDetailControllerBinding.inflate(themedInflater) binding = SourcePreferencesControllerBinding.inflate(themedInflater)
return binding.root return binding.root
} }
override fun createPresenter(): ExtensionDetailsPresenter { override fun createPresenter(): SourcePreferencesPresenter {
return ExtensionDetailsPresenter(args.getString(PKGNAME_KEY)!!) return SourcePreferencesPresenter(args.getLong(SOURCE_ID))
} }
override fun getTitle(): String? { override fun getTitle(): String? {
return resources?.getString(R.string.label_extension_info) return presenter.source?.toString()
} }
@SuppressLint("PrivateResource") @SuppressLint("PrivateResource")
override fun onViewCreated(view: View) { override fun onViewCreated(view: View) {
super.onViewCreated(view) super.onViewCreated(view)
val extension = presenter.extension ?: return val source = presenter.source ?: return
val context = view.context val context = view.context
binding.extensionTitle.text = extension.name
binding.extensionVersion.text = context.getString(R.string.ext_version_info, extension.versionName)
binding.extensionLang.text = context.getString(R.string.ext_language_info, LocaleHelper.getSourceDisplayName(extension.lang, context))
binding.extensionPkg.text = extension.pkgName
extension.getApplicationIcon(context)?.let { binding.extensionIcon.setImageDrawable(it) }
binding.extensionUninstallButton.clicks()
.onEach { presenter.uninstallExtension() }
.launchIn(scope)
if (extension.isObsolete) {
binding.extensionObsolete.visible()
}
val themedContext by lazy { getPreferenceThemeContext() } val themedContext by lazy { getPreferenceThemeContext() }
val manager = PreferenceManager(themedContext) val manager = PreferenceManager(themedContext)
manager.preferenceDataStore = EmptyPreferenceDataStore() manager.preferenceDataStore = EmptyPreferenceDataStore()
@ -93,23 +76,17 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
val screen = manager.createPreferenceScreen(themedContext) val screen = manager.createPreferenceScreen(themedContext)
preferenceScreen = screen preferenceScreen = screen
val multiSource = extension.sources.size > 1 try {
addPreferencesForSource(screen, source)
for (source in extension.sources) { } catch (e: AbstractMethodError) {
if (source is ConfigurableSource) { Timber.e("Source did not implement [addPreferencesForSource]: ${source.name}")
addPreferencesForSource(screen, source, multiSource)
}
} }
manager.setPreferences(screen) manager.setPreferences(screen)
binding.extensionPrefsRecycler.layoutManager = LinearLayoutManager(context) binding.recycler.layoutManager = LinearLayoutManager(context)
binding.extensionPrefsRecycler.adapter = PreferenceGroupAdapter(screen) binding.recycler.adapter = PreferenceGroupAdapter(screen)
binding.extensionPrefsRecycler.addItemDecoration(DividerItemDecoration(context, VERTICAL)) binding.recycler.addItemDecoration(DividerItemDecoration(context, VERTICAL))
if (screen.preferenceCount == 0) {
binding.extensionPrefsEmptyView.show(R.string.ext_empty_preferences)
}
} }
override fun onDestroyView(view: View) { override fun onDestroyView(view: View) {
@ -117,10 +94,6 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
super.onDestroyView(view) super.onDestroyView(view)
} }
fun onExtensionUninstalled() {
router.popCurrentController()
}
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
lastOpenPreferencePosition?.let { outState.putInt(LASTOPENPREFERENCE_KEY, it) } lastOpenPreferencePosition?.let { outState.putInt(LASTOPENPREFERENCE_KEY, it) }
super.onSaveInstanceState(outState) super.onSaveInstanceState(outState)
@ -131,24 +104,14 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
lastOpenPreferencePosition = savedInstanceState.get(LASTOPENPREFERENCE_KEY) as? Int lastOpenPreferencePosition = savedInstanceState.get(LASTOPENPREFERENCE_KEY) as? Int
} }
private fun addPreferencesForSource(screen: PreferenceScreen, source: Source, multiSource: Boolean) { private fun addPreferencesForSource(screen: PreferenceScreen, source: Source) {
val context = screen.context val context = screen.context
// TODO val dataStore = SharedPreferencesDataStore(
val dataStore = SharedPreferencesDataStore(/*if (source is HttpSource) { context.getSharedPreferences(source.getPreferenceKey(), Context.MODE_PRIVATE)
source.preferences
} else {*/
context.getSharedPreferences("source_${source.id}", Context.MODE_PRIVATE)
/*}*/
) )
if (source is ConfigurableSource) { if (source is ConfigurableSource) {
if (multiSource) {
screen.preferenceCategory {
title = source.toString()
}
}
val newScreen = screen.preferenceManager.createPreferenceScreen(context) val newScreen = screen.preferenceManager.createPreferenceScreen(context)
source.setupPreferenceScreen(newScreen) source.setupPreferenceScreen(newScreen)
@ -207,7 +170,7 @@ class ExtensionDetailsController(bundle: Bundle? = null) :
} }
private companion object { private companion object {
const val PKGNAME_KEY = "pkg_name" const val SOURCE_ID = "source_id"
const val LASTOPENPREFERENCE_KEY = "last_open_preference" const val LASTOPENPREFERENCE_KEY = "last_open_preference"
} }
} }

View File

@ -0,0 +1,14 @@
package eu.kanade.tachiyomi.ui.browse.extension.details
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class SourcePreferencesPresenter(
val sourceId: Long,
sourceManager: SourceManager = Injekt.get()
) : BasePresenter<SourcePreferencesController>() {
val source = sourceManager.get(sourceId)
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.migration package eu.kanade.tachiyomi.ui.browse.migration
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R

View File

@ -1,8 +1,12 @@
package eu.kanade.tachiyomi.ui.migration package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.view.View import android.view.View
import com.bumptech.glide.load.engine.DiskCacheStrategy import com.bumptech.glide.load.engine.DiskCacheStrategy
import com.bumptech.glide.load.resource.bitmap.CenterCrop
import com.bumptech.glide.load.resource.bitmap.RoundedCorners
import com.bumptech.glide.request.RequestOptions
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.glide.GlideApp import eu.kanade.tachiyomi.data.glide.GlideApp
import eu.kanade.tachiyomi.data.glide.toMangaThumbnail import eu.kanade.tachiyomi.data.glide.toMangaThumbnail
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
@ -26,11 +30,13 @@ class MangaHolder(
// Update the cover. // Update the cover.
GlideApp.with(itemView.context).clear(thumbnail) GlideApp.with(itemView.context).clear(thumbnail)
val radius = itemView.context.resources.getDimensionPixelSize(R.dimen.card_radius)
val requestOptions = RequestOptions().transform(CenterCrop(), RoundedCorners(radius))
GlideApp.with(itemView.context) GlideApp.with(itemView.context)
.load(item.manga.toMangaThumbnail()) .load(item.manga.toMangaThumbnail())
.diskCacheStrategy(DiskCacheStrategy.RESOURCE) .diskCacheStrategy(DiskCacheStrategy.RESOURCE)
.centerCrop() .apply(requestOptions)
.circleCrop()
.dontAnimate() .dontAnimate()
.into(thumbnail) .into(thumbnail)
} }

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.migration package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.os.Parcelable
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
@ -7,15 +8,20 @@ import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import kotlinx.android.parcel.Parcelize
class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>() { @Parcelize
class MangaItem(val manga: Manga) : AbstractFlexibleItem<MangaHolder>(), Parcelable {
override fun getLayoutRes(): Int { override fun getLayoutRes(): Int {
return R.layout.source_list_item return R.layout.source_list_item
} }
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MangaHolder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): MangaHolder {
return MangaHolder(view, adapter) return MangaHolder(
view,
adapter
)
} }
override fun bindViewHolder( override fun bindViewHolder(

View File

@ -0,0 +1,81 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.databinding.MigrationMangaControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.search.SearchController
import eu.kanade.tachiyomi.ui.browse.source.SourceDividerItemDecoration
class MigrationMangaController :
NucleusController<MigrationMangaControllerBinding, MigrationMangaPresenter>,
FlexibleAdapter.OnItemClickListener {
private var adapter: FlexibleAdapter<IFlexible<*>>? = null
constructor(sourceId: Long, sourceName: String?) : super(
Bundle().apply {
putLong(SOURCE_ID_EXTRA, sourceId)
putString(SOURCE_NAME_EXTRA, sourceName)
}
)
@Suppress("unused")
constructor(bundle: Bundle) : this(
bundle.getLong(SOURCE_ID_EXTRA),
bundle.getString(SOURCE_NAME_EXTRA)
)
private val sourceId: Long = args.getLong(SOURCE_ID_EXTRA)
private val sourceName: String? = args.getString(SOURCE_NAME_EXTRA)
override fun getTitle(): String? {
return sourceName
}
override fun createPresenter(): MigrationMangaPresenter {
return MigrationMangaPresenter(sourceId)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationMangaControllerBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = FlexibleAdapter<IFlexible<*>>(null, this)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = adapter
binding.recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
adapter?.fastScroller = binding.fastScroller
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
fun setManga(manga: List<MangaItem>) {
adapter?.updateDataSet(manga)
}
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? MangaItem ?: return false
val controller = SearchController(item.manga)
router.pushController(controller.withFadeTransaction())
return false
}
companion object {
const val SOURCE_ID_EXTRA = "source_id_extra"
const val SOURCE_NAME_EXTRA = "source_name_extra"
}
}

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.ui.browse.migration.manga
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrationMangaPresenter(
private val sourceId: Long,
private val db: DatabaseHelper = Injekt.get()
) : BasePresenter<MigrationMangaController>() {
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
db.getFavoriteMangas()
.asRxObservable()
.observeOn(AndroidSchedulers.mainThread())
.map { libraryToMigrationItem(it) }
.subscribeLatestCache(MigrationMangaController::setManga)
}
private fun libraryToMigrationItem(library: List<Manga>): List<MangaItem> {
return library.filter { it.source == sourceId }
.sortedBy { it.title }
.map { MangaItem(it) }
}
}

View File

@ -1,17 +1,17 @@
package eu.kanade.tachiyomi.ui.migration package eu.kanade.tachiyomi.ui.browse.migration.search
import android.app.Dialog import android.app.Dialog
import android.os.Bundle import android.os.Bundle
import androidx.core.view.isVisible
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import com.afollestad.materialdialogs.list.listItemsMultiChoice import com.afollestad.materialdialogs.list.listItemsMultiChoice
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
import eu.kanade.tachiyomi.util.view.gone
import eu.kanade.tachiyomi.util.view.visible
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
class SearchController( class SearchController(
@ -21,7 +21,10 @@ class SearchController(
private var newManga: Manga? = null private var newManga: Manga? = null
override fun createPresenter(): GlobalSearchPresenter { override fun createPresenter(): GlobalSearchPresenter {
return SearchPresenter(initialQuery, manga!!) return SearchPresenter(
initialQuery,
manga!!
)
} }
override fun onSaveInstanceState(outState: Bundle) { override fun onSaveInstanceState(outState: Bundle) {
@ -52,7 +55,8 @@ class SearchController(
override fun onMangaClick(manga: Manga) { override fun onMangaClick(manga: Manga) {
newManga = manga newManga = manga
val dialog = MigrationDialog() val dialog =
MigrationDialog()
dialog.targetController = this dialog.targetController = this
dialog.showDialog(router) dialog.showDialog(router)
} }
@ -64,9 +68,9 @@ class SearchController(
fun renderIsReplacingManga(isReplacingManga: Boolean) { fun renderIsReplacingManga(isReplacingManga: Boolean) {
if (isReplacingManga) { if (isReplacingManga) {
binding.progress.visible() binding.progress.isVisible = true
} else { } else {
binding.progress.gone() binding.progress.isVisible = false
router.popController(this) router.popController(this)
} }
} }
@ -78,7 +82,10 @@ class SearchController(
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val prefValue = preferences.migrateFlags().get() val prefValue = preferences.migrateFlags().get()
val preselected = MigrationFlags.getEnabledFlagsPositions(prefValue) val preselected =
MigrationFlags.getEnabledFlagsPositions(
prefValue
)
return MaterialDialog(activity!!) return MaterialDialog(activity!!)
.message(R.string.migration_dialog_what_to_include) .message(R.string.migration_dialog_what_to_include)
@ -87,7 +94,10 @@ class SearchController(
initialSelection = preselected.toIntArray() initialSelection = preselected.toIntArray()
) { _, positions, _ -> ) { _, positions, _ ->
// Save current settings for the next time // Save current settings for the next time
val newValue = MigrationFlags.getFlagsFromPositions(positions.toTypedArray()) val newValue =
MigrationFlags.getFlagsFromPositions(
positions.toTypedArray()
)
preferences.migrateFlags().set(newValue) preferences.migrateFlags().set(newValue)
} }
.positiveButton(R.string.migrate) { .positiveButton(R.string.migrate) {

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.migration package eu.kanade.tachiyomi.ui.browse.migration.search
import android.os.Bundle import android.os.Bundle
import com.jakewharton.rxrelay.BehaviorRelay import com.jakewharton.rxrelay.BehaviorRelay
@ -8,10 +8,12 @@ import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.SChapter import eu.kanade.tachiyomi.source.model.SChapter
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.ui.browse.migration.MigrationFlags
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchCardItem import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchCardItem
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchItem
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchPresenter
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import java.util.Date
import rx.Observable import rx.Observable
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers import rx.schedulers.Schedulers
@ -70,9 +72,18 @@ class SearchPresenter(
replace: Boolean replace: Boolean
) { ) {
val flags = preferences.migrateFlags().get() val flags = preferences.migrateFlags().get()
val migrateChapters = MigrationFlags.hasChapters(flags) val migrateChapters =
val migrateCategories = MigrationFlags.hasCategories(flags) MigrationFlags.hasChapters(
val migrateTracks = MigrationFlags.hasTracks(flags) flags
)
val migrateCategories =
MigrationFlags.hasCategories(
flags
)
val migrateTracks =
MigrationFlags.hasTracks(
flags
)
db.inTransaction { db.inTransaction {
// Update chapters read // Update chapters read
@ -137,6 +148,14 @@ class SearchPresenter(
manga.viewer = prevManga.viewer manga.viewer = prevManga.viewer
db.updateMangaViewer(manga).executeAsBlocking() db.updateMangaViewer(manga).executeAsBlocking()
// Update date added
if (replace) {
manga.date_added = prevManga.date_added
prevManga.date_added = 0
} else {
manga.date_added = Date().time
}
// SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title // SearchPresenter#networkToLocalManga may have updated the manga title, so ensure db gets updated title
db.updateMangaTitle(manga).executeAsBlocking() db.updateMangaTitle(manga).executeAsBlocking()
} }

View File

@ -0,0 +1,54 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.recyclerview.widget.LinearLayoutManager
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.databinding.MigrationSourcesControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
import eu.kanade.tachiyomi.ui.browse.migration.manga.MigrationMangaController
import eu.kanade.tachiyomi.ui.browse.source.SourceDividerItemDecoration
class MigrationSourcesController :
NucleusController<MigrationSourcesControllerBinding, MigrationSourcesPresenter>(),
FlexibleAdapter.OnItemClickListener {
private var adapter: SourceAdapter? = null
override fun createPresenter(): MigrationSourcesPresenter {
return MigrationSourcesPresenter()
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = MigrationSourcesControllerBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
adapter = SourceAdapter(this)
binding.recycler.layoutManager = LinearLayoutManager(view.context)
binding.recycler.adapter = adapter
binding.recycler.addItemDecoration(SourceDividerItemDecoration(view.context))
adapter?.fastScroller = binding.fastScroller
}
override fun onDestroyView(view: View) {
adapter = null
super.onDestroyView(view)
}
fun setSources(sourcesWithManga: List<SourceItem>) {
adapter?.updateDataSet(sourcesWithManga)
}
override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false
val controller = MigrationMangaController(item.source.id, item.source.name)
parentController!!.router.pushController(controller.withFadeTransaction())
return false
}
}

View File

@ -0,0 +1,35 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.os.Bundle
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import rx.android.schedulers.AndroidSchedulers
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class MigrationSourcesPresenter(
private val sourceManager: SourceManager = Injekt.get(),
private val db: DatabaseHelper = Injekt.get()
) : BasePresenter<MigrationSourcesController>() {
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
db.getFavoriteMangas()
.asRxObservable()
.observeOn(AndroidSchedulers.mainThread())
.map { findSourcesWithManga(it) }
.subscribeLatestCache(MigrationSourcesController::setSources)
}
private fun findSourcesWithManga(library: List<Manga>): List<SourceItem> {
val header = SelectionHeader()
return library.map { it.source }.toSet()
.mapNotNull { if (it != LocalSource.ID) sourceManager.getOrStub(it) else null }
.sortedBy { it.name }
.map { SourceItem(it, header) }
}
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.migration package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -25,7 +25,10 @@ class SelectionHeader : AbstractHeaderItem<SelectionHeader.Holder>() {
* Creates a new view holder for this item. * Creates a new view holder for this item.
*/ */
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): Holder {
return Holder(view, adapter) return Holder(
view,
adapter
)
} }
/** /**

View File

@ -0,0 +1,22 @@
package eu.kanade.tachiyomi.ui.browse.migration.sources
import com.bluelinelabs.conductor.Controller
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.system.getResourceColor
/**
* Adapter that holds the catalogue cards.
*
* @param controller instance of [MigrationController].
*/
class SourceAdapter(val controller: Controller) :
FlexibleAdapter<IFlexible<*>>(null, controller, true) {
val cardBackground = controller.activity!!.getResourceColor(R.attr.colorSurface)
init {
setDisplayHeadersAtStartUp(true)
}
}

View File

@ -1,16 +1,12 @@
package eu.kanade.tachiyomi.ui.migration package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.view.View import android.view.View
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.source.icon import eu.kanade.tachiyomi.source.icon
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder import eu.kanade.tachiyomi.ui.base.holder.SlicedHolder
import eu.kanade.tachiyomi.util.view.gone
import io.github.mthli.slice.Slice import io.github.mthli.slice.Slice
import kotlinx.android.synthetic.main.source_main_controller_card_item.card import kotlinx.android.synthetic.main.source_main_controller_card_item.card
import kotlinx.android.synthetic.main.source_main_controller_card_item.image import kotlinx.android.synthetic.main.source_main_controller_card_item.image
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_browse
import kotlinx.android.synthetic.main.source_main_controller_card_item.source_latest
import kotlinx.android.synthetic.main.source_main_controller_card_item.title import kotlinx.android.synthetic.main.source_main_controller_card_item.title
class SourceHolder(view: View, override val adapter: SourceAdapter) : class SourceHolder(view: View, override val adapter: SourceAdapter) :
@ -24,14 +20,6 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) :
override val viewToSlice: View override val viewToSlice: View
get() = card get() = card
init {
source_latest.gone()
source_browse.setText(R.string.select)
source_browse.setOnClickListener {
adapter.selectClickListener?.onSelectClick(bindingAdapterPosition)
}
}
fun bind(item: SourceItem) { fun bind(item: SourceItem) {
val source = item.source val source = item.source
setCardEdges(item) setCardEdges(item)
@ -39,12 +27,9 @@ class SourceHolder(view: View, override val adapter: SourceAdapter) :
// Set source name // Set source name
title.text = source.name title.text = source.name
// Set circle letter image. // Set source icon
itemView.post { itemView.post {
val icon = source.icon() image.setImageDrawable(source.icon())
if (icon != null) {
image.setImageDrawable(icon)
}
} }
} }
} }

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.migration package eu.kanade.tachiyomi.ui.browse.migration.sources
import android.view.View import android.view.View
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
@ -14,7 +14,7 @@ import eu.kanade.tachiyomi.source.Source
* @param source Instance of [Source] containing source information. * @param source Instance of [Source] containing source information.
* @param header The header for this item. * @param header The header for this item.
*/ */
data class SourceItem(val source: Source, val header: SelectionHeader? = null) : data class SourceItem(val source: Source, val header: SelectionHeader) :
AbstractSectionableItem<SourceHolder, SelectionHeader>(header) { AbstractSectionableItem<SourceHolder, SelectionHeader>(header) {
/** /**
@ -28,7 +28,10 @@ data class SourceItem(val source: Source, val header: SelectionHeader? = null) :
* Creates a new view holder for this item. * Creates a new view holder for this item.
*/ */
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder { override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): SourceHolder {
return SourceHolder(view, adapter as SourceAdapter) return SourceHolder(
view,
adapter as SourceAdapter
)
} }
/** /**

View File

@ -22,26 +22,15 @@ class SourceAdapter(val controller: SourceController) :
/** /**
* Listener for browse item clicks. * Listener for browse item clicks.
*/ */
val browseClickListener: OnBrowseClickListener = controller val clickListener: OnSourceClickListener = controller
/**
* Listener for latest item clicks.
*/
val latestClickListener: OnLatestClickListener = controller
/** /**
* Listener which should be called when user clicks browse. * Listener which should be called when user clicks browse.
* Note: Should only be handled by [SourceController] * Note: Should only be handled by [SourceController]
*/ */
interface OnBrowseClickListener { interface OnSourceClickListener {
fun onBrowseClick(position: Int) fun onBrowseClick(position: Int)
}
/**
* Listener which should be called when user clicks latest.
* Note: Should only be handled by [SourceController]
*/
interface OnLatestClickListener {
fun onLatestClick(position: Int) fun onLatestClick(position: Int)
fun onPinClick(position: Int)
} }
} }

View File

@ -1,6 +1,8 @@
package eu.kanade.tachiyomi.ui.browse.source package eu.kanade.tachiyomi.ui.browse.source
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
import android.app.Dialog
import android.os.Bundle
import android.view.LayoutInflater import android.view.LayoutInflater
import android.view.Menu import android.view.Menu
import android.view.MenuInflater import android.view.MenuInflater
@ -17,9 +19,13 @@ import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible import eu.davidea.flexibleadapter.items.IFlexible
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.minusAssign
import eu.kanade.tachiyomi.data.preference.plusAssign
import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding import eu.kanade.tachiyomi.databinding.SourceMainControllerBinding
import eu.kanade.tachiyomi.source.CatalogueSource import eu.kanade.tachiyomi.source.CatalogueSource
import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.base.controller.NucleusController import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction import eu.kanade.tachiyomi.ui.base.controller.withFadeTransaction
@ -27,8 +33,7 @@ import eu.kanade.tachiyomi.ui.browse.BrowseController
import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController import eu.kanade.tachiyomi.ui.browse.source.browse.BrowseSourceController
import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController import eu.kanade.tachiyomi.ui.browse.source.globalsearch.GlobalSearchController
import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController import eu.kanade.tachiyomi.ui.browse.source.latest.LatestUpdatesController
import eu.kanade.tachiyomi.ui.setting.SettingsSourcesController import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.appcompat.QueryTextEvent import reactivecircus.flowbinding.appcompat.QueryTextEvent
@ -39,15 +44,14 @@ import uy.kohesive.injekt.api.get
/** /**
* This controller shows and manages the different catalogues enabled by the user. * This controller shows and manages the different catalogues enabled by the user.
* This controller should only handle UI actions, IO actions should be done by [SourcePresenter] * This controller should only handle UI actions, IO actions should be done by [SourcePresenter]
* [SourceAdapter.OnBrowseClickListener] call function data on browse item click. * [SourceAdapter.OnSourceClickListener] call function data on browse item click.
* [SourceAdapter.OnLatestClickListener] call function data on latest item click * [SourceAdapter.OnLatestClickListener] call function data on latest item click
*/ */
class SourceController : class SourceController :
NucleusController<SourceMainControllerBinding, SourcePresenter>(), NucleusController<SourceMainControllerBinding, SourcePresenter>(),
FlexibleAdapter.OnItemClickListener, FlexibleAdapter.OnItemClickListener,
FlexibleAdapter.OnItemLongClickListener, FlexibleAdapter.OnItemLongClickListener,
SourceAdapter.OnBrowseClickListener, SourceAdapter.OnSourceClickListener {
SourceAdapter.OnLatestClickListener {
private val preferences: PreferencesHelper = Injekt.get() private val preferences: PreferencesHelper = Injekt.get()
@ -113,49 +117,52 @@ class SourceController :
} }
override fun onItemClick(view: View, position: Int): Boolean { override fun onItemClick(view: View, position: Int): Boolean {
val item = adapter?.getItem(position) as? SourceItem ?: return false onItemClick(position)
val source = item.source
openCatalogue(source, BrowseSourceController(source))
return false return false
} }
private fun onItemClick(position: Int) {
val item = adapter?.getItem(position) as? SourceItem ?: return
val source = item.source
openSource(source, BrowseSourceController(source))
}
override fun onItemLongClick(position: Int) { override fun onItemLongClick(position: Int) {
val activity = activity ?: return val activity = activity ?: return
val item = adapter?.getItem(position) as? SourceItem ?: return val item = adapter?.getItem(position) as? SourceItem ?: return
val isPinned = item.header?.code?.equals(SourcePresenter.PINNED_KEY) ?: false val isPinned = item.header?.code?.equals(SourcePresenter.PINNED_KEY) ?: false
MaterialDialog(activity) val items = mutableListOf(
.title(text = item.source.name) Pair(
.listItems( activity.getString(if (isPinned) R.string.action_unpin else R.string.action_pin),
items = listOf( { toggleSourcePin(item.source) }
activity.getString(R.string.action_hide), )
activity.getString(if (isPinned) R.string.action_unpin else R.string.action_pin) )
), if (item.source !is LocalSource) {
waitForPositiveButton = false items.add(
) { dialog, which, _ -> Pair(
when (which) { activity.getString(R.string.action_disable),
0 -> hideCatalogue(item.source) { disableSource(item.source) }
1 -> pinCatalogue(item.source, isPinned) )
} )
dialog.dismiss() }
}
.show() SourceOptionsDialog(item, items).showDialog(router)
} }
private fun hideCatalogue(source: Source) { private fun disableSource(source: Source) {
val current = preferences.hiddenCatalogues().get() preferences.disabledSources() += source.id.toString()
preferences.hiddenCatalogues().set(current + source.id.toString())
presenter.updateSources() presenter.updateSources()
} }
private fun pinCatalogue(source: Source, isPinned: Boolean) { private fun toggleSourcePin(source: Source) {
val current = preferences.pinnedCatalogues().get() val isPinned = source.id.toString() in preferences.pinnedSources().get()
if (isPinned) { if (isPinned) {
preferences.pinnedCatalogues().set(current - source.id.toString()) preferences.pinnedSources() -= source.id.toString()
} else { } else {
preferences.pinnedCatalogues().set(current + source.id.toString()) preferences.pinnedSources() += source.id.toString()
} }
presenter.updateSources() presenter.updateSources()
@ -165,7 +172,7 @@ class SourceController :
* Called when browse is clicked in [SourceAdapter] * Called when browse is clicked in [SourceAdapter]
*/ */
override fun onBrowseClick(position: Int) { override fun onBrowseClick(position: Int) {
onItemClick(view!!, position) onItemClick(position)
} }
/** /**
@ -173,15 +180,23 @@ class SourceController :
*/ */
override fun onLatestClick(position: Int) { override fun onLatestClick(position: Int) {
val item = adapter?.getItem(position) as? SourceItem ?: return val item = adapter?.getItem(position) as? SourceItem ?: return
openCatalogue(item.source, LatestUpdatesController(item.source)) openSource(item.source, LatestUpdatesController(item.source))
}
/**
* Called when pin icon is clicked in [SourceAdapter]
*/
override fun onPinClick(position: Int) {
val item = adapter?.getItem(position) as? SourceItem ?: return
toggleSourcePin(item.source)
} }
/** /**
* Opens a catalogue with the given controller. * Opens a catalogue with the given controller.
*/ */
private fun openCatalogue(source: CatalogueSource, controller: BrowseSourceController) { private fun openSource(source: CatalogueSource, controller: BrowseSourceController) {
preferences.lastUsedCatalogueSource().set(source.id) preferences.lastUsedSource().set(source.id)
(parentController as BrowseController).pushController(controller.withFadeTransaction()) parentController!!.router.pushController(controller.withFadeTransaction())
} }
/** /**
@ -204,13 +219,13 @@ class SourceController :
// Create query listener which opens the global search view. // Create query listener which opens the global search view.
searchView.queryTextEvents() searchView.queryTextEvents()
.filter { it is QueryTextEvent.QuerySubmitted } .filterIsInstance<QueryTextEvent.QuerySubmitted>()
.onEach { performGlobalSearch(it.queryText.toString()) } .onEach { performGlobalSearch(it.queryText.toString()) }
.launchIn(scope) .launchIn(scope)
} }
private fun performGlobalSearch(query: String) { private fun performGlobalSearch(query: String) {
(parentController as BrowseController).pushController( parentController!!.router.pushController(
GlobalSearchController(query).withFadeTransaction() GlobalSearchController(query).withFadeTransaction()
) )
} }
@ -225,8 +240,9 @@ class SourceController :
when (item.itemId) { when (item.itemId) {
// Initialize option to open catalogue settings. // Initialize option to open catalogue settings.
R.id.action_settings -> { R.id.action_settings -> {
(parentController as BrowseController).pushController( parentController!!.router.pushController(
SettingsSourcesController().withFadeTransaction() SourceFilterController()
.withFadeTransaction()
) )
} }
} }
@ -250,4 +266,27 @@ class SourceController :
adapter?.addScrollableHeader(LangItem(SourcePresenter.LAST_USED_KEY)) adapter?.addScrollableHeader(LangItem(SourcePresenter.LAST_USED_KEY))
} }
} }
class SourceOptionsDialog(bundle: Bundle? = null) : DialogController(bundle) {
private lateinit var item: SourceItem
private lateinit var items: List<Pair<String, () -> Unit>>
constructor(item: SourceItem, items: List<Pair<String, () -> Unit>>) : this() {
this.item = item
this.items = items
}
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
return MaterialDialog(activity!!)
.title(text = item.source.toString())
.listItems(
items = items.map { it.first },
waitForPositiveButton = false
) { dialog, which, _ ->
items[which].second()
dialog.dismiss()
}
}
}
} }

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