Compare commits

..

133 Commits

Author SHA1 Message Date
75e828923a Release v0.6.8 2018-01-16 17:02:13 +01:00
b499b87f8c Migration now opens manga on long click 2018-01-15 20:22:07 +01:00
233dbec4b3 Add adaptive icon and a dev variant 2018-01-13 18:15:00 +01:00
d56ff9592e Use a single preference to store migration flags 2018-01-13 13:13:03 +01:00
08f6317beb Add error handling to migrations 2018-01-13 11:47:04 +01:00
a75457ad88 Add a new screen to help migrating manga from sources 2018-01-12 22:02:05 +01:00
b0482003bd Handle ActivityNotFoundException (#1170)
* [SettingsBackupController] Handle ActivityNotFoundException

When using `Intent.ACTION_CREATE_DOCUMENT` on SDK >= Lollipop there is no
guarantee that the ROM supports the built in file picker such as MIUI

* [SettingsBackupController] Add import for ActivityNotFoundException

* Add additional handlers to Android document intents

* Requested review changes

Move `try {`s to top of block
Replace version numbers with `Build.VERSION_CODES.LOLLIPOP`
Break out custom file picker intent to Context extention `Context.getFilePicker`
Rename `val i` to `val intent` to be more clear with variable names

* Add version check to custom file picker after exception
2018-01-12 10:57:21 +01:00
634356e72f Release v0.6.7 2018-01-09 20:42:44 +01:00
6d3cc16ab1 Include minor changes from extensions PR 2018-01-09 12:27:45 +01:00
6027671c09 Address #1154 (#1160)
* change add to library icon add toast

* adjusted toast messages
added toast to catalog long click

* adjusted strings
2018-01-08 14:08:48 +01:00
29d0cb4a15 fixed issue where some sources that use cloudflare use the Server: cloudflare as cloudflare-nginx is deprecated (#1152) 2018-01-08 11:03:37 +01:00
fe7001975a Fix padding in RecyclerViews (#1148) 2018-01-06 18:50:40 +01:00
ac88f1c146 Update README (#1142)
* Update README.md

* add pics to readme

* Update README.md

* change language from stable to just "app"

* Update README.md

* update with feedback

* change test to try

* add link to extentions repo
2018-01-05 22:54:41 +01:00
b5b86218c5 Mangachan advanced support (#1138)
* Mangachan catalogue. Add support for filtering

* MangaChan add support for status
2018-01-04 22:01:42 +01:00
bdcc6e52e6 Small new user improvements (#1143)
- Changed empty library string
- Added empty view for Categories
2018-01-01 14:57:20 +01:00
0eae817aa6 Update MangaChan.kt (#1128)
Remove useless ganres
2017-12-14 13:28:24 +01:00
8994b42760 Remove local broadcast receiver to prevent race conditions (#1123)
* Remove local broadcast receiver to prevent run exceptions.
Added option to set tile for extension update.
2017-12-11 20:01:28 +01:00
6a63ce992a [Mangafox] update mangafox URL for built-in source (#1119) 2017-12-09 13:29:30 +01:00
9ae6285eef Change discord invite link in settings (#1112)
* Change discord invite link in settings

* Change discord link is readme
2017-12-06 08:41:37 +01:00
8f9737f567 Update regexp for pages from Readmanga/Mintmanga (#1111) 2017-12-05 21:21:02 +01:00
f287d313c3 Release 0.6.6 2017-12-05 19:50:53 +01:00
e745836404 Restore tracking on backup (#1097) 2017-12-04 22:55:57 +01:00
08baf798aa Give view pager unique ids, avoiding subtle bugs 2017-12-04 22:22:35 +01:00
8bcb14c65d Add view caching to view holders 2017-12-03 17:00:32 +01:00
d94dc68830 Fix library not being updated 2017-12-03 12:59:51 +01:00
297fed6aef Repackage catalogue to match the UI 2017-12-03 12:58:38 +01:00
d690d6e0e3 Use synthetic view's new caching method 2017-12-03 01:03:15 +01:00
9ba8d88b07 Dependency updates 2017-12-02 20:59:35 +01:00
34a40b0131 Start downloader after a library update. It should help with some catalogue issues 2017-12-02 17:29:05 +01:00
182bf5f2bd Add install packages permission. Fixes #1104 2017-12-02 17:10:31 +01:00
04638535d8 Fix library options menu shown in chapters screen. Resolves #1096 2017-11-30 15:37:20 +01:00
d87c8428fe Release 0.6.5 2017-11-29 18:49:22 +01:00
166fb9a8e4 Resubscribe to library when a change of type enter occurs. Resolves #1093 2017-11-29 10:05:33 +01:00
28a21d0b8f Minor changes to download cache. Also keep the library view, as recreation is expensive 2017-11-28 23:58:37 +01:00
d1d1d60c30 Fix automatic backups (#1074)
* Fix automatic backups

* Small fixes

* small fixes
2017-11-28 22:55:50 +01:00
80fd49d60b FIx Batoto issues with logging in and loading lists/pages. (#1088) 2017-11-28 09:48:27 +01:00
34eb1331a3 Update build tools in travis 2017-11-28 01:12:45 +01:00
bff329a329 Implement a download cache 2017-11-28 00:32:51 +01:00
604929d002 Update support library and kotlin 2017-11-28 00:21:38 +01:00
4a9151e4aa Release 0.6.4 2017-11-23 18:38:51 +01:00
020cc89576 Translations (#970)
* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (German)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (French)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/fr/

* Translated using Weblate (German)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (German)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Added translation using Weblate (Indonesian)

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/id/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/bg/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/es/

* Translated using Weblate (Spanish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/es/

* Translated using Weblate (Arabic)

Currently translated at 0.2% (1 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 0.5% (2 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 1.1% (4 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 1.3% (5 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 1.6% (6 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 1.9% (7 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 2.2% (8 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 3.0% (11 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 3.3% (12 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 3.9% (14 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 4.4% (16 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 6.1% (22 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 6.7% (24 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 7.2% (26 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 8.3% (30 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 8.9% (32 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 10.6% (38 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 10.8% (39 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 12.0% (43 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 12.2% (44 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 12.5% (45 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 15.9% (57 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 16.4% (59 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 16.7% (60 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 17.3% (62 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 17.3% (62 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 17.8% (64 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 18.1% (65 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 19.2% (69 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 21.2% (76 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 22.0% (79 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 22.3% (80 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 23.4% (84 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 25.1% (90 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 25.4% (91 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 26.5% (95 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 26.8% (96 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 27.6% (99 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 27.9% (100 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 28.2% (101 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 28.4% (102 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 29.0% (104 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 29.3% (105 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 29.8% (107 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 30.1% (108 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 30.1% (108 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 30.4% (109 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 31.2% (112 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 31.8% (114 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 32.9% (118 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 33.2% (119 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 33.7% (121 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 34.0% (122 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 35.1% (126 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 35.4% (127 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 36.0% (129 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 36.3% (130 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Arabic)

Currently translated at 55.3% (198 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Polish)

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Arabic)

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ar/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/bg/

* Translated using Weblate (Bulgarian)

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/bg/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pt_BR/

* Translated using Weblate (German)

Currently translated at 99.4% (368 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (German)

Currently translated at 99.4% (368 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (German)

Currently translated at 100.0% (370 of 370 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (Polish)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/pl/

* Translated using Weblate (Dutch)

Currently translated at 96.5% (359 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (Dutch)

Currently translated at 96.7% (360 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (Dutch)

Currently translated at 97.3% (362 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (Dutch)

Currently translated at 97.5% (363 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (French)

Currently translated at 97.8% (364 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/fr/

* Translated using Weblate (French)

Currently translated at 97.8% (364 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/fr/

* Translated using Weblate (French)

Currently translated at 98.9% (368 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/fr/

* Translated using Weblate (Dutch)

Currently translated at 99.4% (370 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (Dutch)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/nl/

* Translated using Weblate (French)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/fr/

* Added translation using Weblate (Hungarian)

* Translated using Weblate (Hungarian)

Currently translated at 25.5% (95 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 25.8% (96 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Russian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ru/

* Translated using Weblate (Hungarian)

Currently translated at 30.3% (113 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 30.3% (113 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 33.0% (123 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 33.0% (123 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 33.8% (126 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 33.8% (126 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 37.0% (138 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 37.3% (139 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 37.9% (141 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Hungarian)

Currently translated at 38.1% (142 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/hu/

* Translated using Weblate (Indonesian)

Currently translated at 98.3% (366 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 99.1% (369 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 99.1% (369 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/pt_BR/

* Translated using Weblate (German)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/de/

* Added translation using Weblate (Malay)

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (German)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/de/

* Translated using Weblate (Indonesian)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/id/

* Translated using Weblate (Malay)

Currently translated at 62.9% (234 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ms/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/pt_BR/

* Translated using Weblate (Malay)

Currently translated at 83.8% (312 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ms/

* Translated using Weblate (Malay)

Currently translated at 84.1% (313 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ms/

* Translated using Weblate (Malay)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ms/

* Translated using Weblate (Malay)

Currently translated at 100.0% (372 of 372 strings)

Translation: Tachiyomi/Main
Translate-URL: http://*/projects/tachiyomi/main/ms/
2017-11-23 18:24:59 +01:00
25898d34ca Update page indicator colors and new Readmangatoday domain 2017-11-23 18:00:49 +01:00
6394388714 Fix #764. Update Kissmanga genres 2017-11-22 21:41:57 +01:00
d4101c7bdf Page indicator now uses an outline instead of overlapping shadows 2017-11-20 13:55:56 +01:00
c93bf89cbe Fix crash downloading of manga from Readmangatoday (#1071)
Close #1070
2017-11-20 10:06:23 +01:00
88d1f29fe2 Move page indicator to bottom center, and use a shadow instead of a background. Other category in catalogue list is now placed at the end 2017-11-18 14:09:38 +01:00
c437a33f2a A few fixes and dependency updates 2017-11-11 15:31:32 +01:00
e3259f39f1 Fallback covers' external directory 2017-11-08 22:34:56 +01:00
cb357b0a16 Fix some crashes 2017-11-08 22:25:00 +01:00
a7faf445c4 Add concurrency to global search queries 2017-11-08 21:02:19 +01:00
82a08f24c0 added Cloudflare email obfuscation bypass for kissmanga (#1054)
* added Cloudflare email obfuscation bypass for kissmanga

* added ignore case.
2017-11-06 22:20:04 +01:00
afa89ac125 added extra padding on page number to prevent cut off on rounded corner devices. (#1056) 2017-11-06 22:08:49 +01:00
2060b5cd34 Fix extra (advertisement) page being added in Mangahere. (#1052) 2017-11-05 10:42:02 +01:00
d69730a333 Undo last commit 2017-11-05 00:47:31 +01:00
9714a30148 Share image with both setData and extra stream intent 2017-11-04 18:05:02 +01:00
23c0f2c313 Update subsampling. Export /storage/ to SAF 2017-11-02 18:49:09 +01:00
5c4139be45 Update flexible adapter. Show fast scroller in chapters screen 2017-11-02 16:58:32 +01:00
6b1a3a20e5 Fix covers url on Mangachan. (#1045)
Fix some warnings.
2017-10-29 15:22:04 +01:00
4ae00c80ca Fix many compilation warnings 2017-10-28 19:12:17 +02:00
827792c4f0 Restore previous query in global search. Closes #1040 2017-10-28 18:26:31 +02:00
abbe700dac Build command and paths also changed 2017-10-28 17:01:17 +02:00
1347bfe243 This one should finally fix travis builds 2017-10-28 16:39:34 +02:00
a76ee95b6d Trying to fix broken travis 2017-10-28 16:24:18 +02:00
f3689f09cd Use gradle's new dependencies API. Update a few dependencies 2017-10-28 16:10:51 +02:00
d545cfd38c Update Android Studio to 3.0 2017-10-28 14:44:19 +02:00
3631a9fac2 Use new key format in badges preference 2017-10-24 17:27:45 +02:00
aee4ad2d3f Fix test 2017-10-22 18:58:31 +02:00
f88c86c799 Download count shouldn't be stored as a database field 2017-10-21 23:43:46 +02:00
60ac27e401 Add library manga class 2017-10-21 20:13:41 +02:00
d0567de4e6 Download badge 2017-10-21 17:08:49 +02:00
ca30fd6088 Actually use latest Glide version. Minor doc fix 2017-10-14 18:37:23 +02:00
len
1470e9d5ca Glide v4 2017-10-14 18:16:11 +02:00
len
f45efe2aa8 Library updater is now a foreground service 2017-10-14 13:05:02 +02:00
5b6c475817 fixed author/artist not showing for Mangahere. (#1032) 2017-10-13 00:18:17 +02:00
len
4abd2d709f Introduce coroutines. Fix #1027. Lower notification channels' importance 2017-10-13 00:12:29 +02:00
d97aff85b3 Support notification channels. Fixes #995 2017-10-10 14:16:37 +02:00
deec65446f Shortcut fix Oreo (#1026)
* Moved to Android O Shortcutmanager

* Re-added possibility to change icon shape pre oreo.
2017-10-10 12:05:33 +02:00
len
5c662b1ae1 Fix app freezes when queueing many chapters with SAF. Closes #817 2017-10-05 10:15:02 +02:00
f648940388 Fix wrong downloaded percentage when server doesn't send content length. Fixes #1019 2017-10-05 08:41:28 +02:00
len
5aae17754f Travis: forgot to add semicolons 2017-10-01 12:15:06 +02:00
len
0ef0f6ece1 Travis: don't decode secrets for PRs 2017-10-01 12:05:38 +02:00
bfd46f28e0 Reversed some things from AMOLED update (#1015) 2017-10-01 10:44:48 +02:00
len
eaece18afc Travis: use default tools 2017-10-01 00:14:11 +02:00
len
67d39b037c Clone NDK for Travis 2017-09-30 22:15:15 +02:00
len
dd3f5a146d Fix commit count in travis' build 2017-09-30 20:40:44 +02:00
len
9fdc5b4b9d Remove teamcity badge. Fix deploy script 2017-09-30 19:05:41 +02:00
1f32d13698 Travis deployment (#1014)
* Travis deployment

* Executable scripts
2017-09-30 18:49:43 +02:00
len
886b1019ed Update android sdk in travis 2017-09-29 20:24:21 +02:00
len
3e8ed8a171 Also set system locale with the Java API. Closes #978 2017-09-29 20:08:40 +02:00
len
8307daee63 Trust mangahere certificate for now... 2017-09-29 18:31:18 +02:00
9b40d10352 Improved AMOLED theme. Added Button style for borderless buttons. (#1009)
* Improved AMOLED theme. Added Button style for borderless buttons. Some UI improvements.

* Deleted unused drawables from app.
2017-09-29 08:34:13 +02:00
f2a06eab37 Fix downloads from mangahere (#997) 2017-09-26 22:15:27 +02:00
74fd70416f Add option to sort library by source. (#985)
* Add option to sort library by source.

* Implement change suggested by NoodleMage.
2017-09-24 20:01:07 +02:00
b85c164195 added licensed element check for MangaFox (#977) 2017-09-24 10:54:39 +02:00
len
75c41b645a Target sdk 26. Dependency updates. 2017-09-23 17:14:04 +02:00
54c8b3ef29 Global Search (#849)
* Global Search

* Cards are now independent of design by use of recycler.

* Added local

* Some attribute fixes + moved onclick to controller.

* Lots of improvements to code

* Reversed some stuff. Thanks API 16

* Code fixes

* Performance improvements

* Moved adapter creation to constructor

* Small changes

* Removed sources settings from settings menu. Added OnChangeListener in catalogue. Made setting icon visible if room.

* bug fix

* Code review part uno

* Code review part uno-2

* Single recycler approach

* Add last source used

* Fix scroll state and some layout issues

* Fix wrong item binding

* Use data class for items

* Calculate item position and count while binding

* Fix background color with slices

* Reuse slices. Fix card background. Flatten constraint layout

* Fix global_search scroll issue

* Store last state with global search

* Minor changes

* Remove catalogue toolbar spinner. Persist catalogue across process restarts

* Save view state of recycler views. Set toolbar title with current query
2017-09-23 13:11:39 +02:00
56bde40035 Fix search on readmanga/mintmanga (#986) 2017-09-19 20:25:09 +02:00
ff4a015baa Added regex to strip local manga chapter names (#983)
* added regex to strip non-alphanumeric characters in local filenames
2017-09-18 11:20:34 +02:00
len
0db4fcc27e Release 0.6.3 2017-09-10 12:29:17 +02:00
len
f3080b6277 Actually convert file uri to content uri 2017-09-10 12:29:04 +02:00
len
69cbbd5811 Disable file exposure detection to allow sharing from the local cache ¯\_(ツ)_/¯ 2017-09-10 12:03:44 +02:00
len
0b85760939 Fix #908 2017-09-10 10:51:21 +02:00
len
03f3a4805f Fix a crash when retrying a page 2017-09-10 10:14:40 +02:00
len
d95adf2631 Release 0.6.2 2017-09-09 14:00:21 +02:00
len
e971d40e06 Lock drawer with gravity parameter instead of a view 2017-09-08 18:18:27 +02:00
len
c65a01a5f0 Fix a crash when retrying pages 2017-09-08 17:59:43 +02:00
len
8586014e17 Exclude extensions dependencies from proguard. Enable new translations. 2017-09-08 17:43:46 +02:00
bdfae4ba04 Translations (#881)
* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pt_BR/

* Add translation link

* Added translation using Weblate (Polish)

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pt_BR/

* Translated using Weblate (Korean)

Currently translated at 25.1% (90 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ko/

* Translated using Weblate (Russian)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/ru/

* Added translation using Weblate (German)

* Translated using Weblate (German)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (German)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/de/

* Translated using Weblate (Polish)

Currently translated at 100.0% (358 of 358 strings)

Translation: Tachiyomi/Main
Translate-URL: http://weblate.j2ghz.com/projects/tachiyomi/main/pl/
2017-09-08 17:25:44 +02:00
75cb94b51a Fix tint on AMOLED theme (#966)
Fix tint on AMOLED theme
2017-08-30 21:50:19 +02:00
2f6d163a7a Simplify presenter delegate 2017-08-29 10:39:22 +02:00
ecfe72bcad Let GC take care of the presenter. Also fix #947 2017-08-29 09:55:42 +02:00
e6ff9e18cc Fix #956 2017-08-28 10:13:27 +02:00
3c550c1781 Kotlin 1.1.4. Add discord link in about 2017-08-28 10:00:13 +02:00
537693f5cf README, CONTRIBUTING and ISSUE_TEMPLATE, Discord (#952)
* Include discord, change issue segment

* Update CONTRIBUTING.md

* Update ISSUE_TEMPLATE.md
2017-08-28 09:42:44 +02:00
5ae0589547 License manga update and Manga Fox Title Update (#937)
* update mangafox parsing to read chapter title also if it exists.

* updated chapterImpl to force update chapters if chapter name changes.  This allows for chapter name changes from the source to update in app

* switched from : to - since other sites that already have title use - so it provides consistency across sources.

* fixed spacing for -

* fixes license status for manga fox
if manga is licensed no chapters will be shown.

* 1. changed equality in chapterImp back to just the url (removed scanlator, and name comparison)
2. Removed extra line of code assigning mangaFox title twice
3. Modified ChapterSourceSync for scanlator/title/url comparison.

* cleaned spaces, added comment, incorporated toChange code from other pull request

* throw exception instead of returning empty list when licensed

* space fix
2017-08-28 09:10:19 +02:00
len
71fc6fc257 Revert chapter equals method 2017-08-26 12:50:52 +02:00
len
c0d7b16ee6 Allow to update chapter metadata 2017-08-26 12:46:35 +02:00
len
f3f7aa9e1d Also fix Batoto popular query 2017-08-25 23:30:18 +02:00
len
43355970db Batoto fix. #953 2017-08-25 23:10:31 +02:00
bfa386acba Add Filter by Completed for library (#941)
* issue 938

added filter by completed manga status

* changed to use existing string
fixed space issue in method in presenter
2017-08-19 20:34:43 +02:00
len
e8b432485d Minor changes 2017-08-15 15:06:21 +02:00
a12a34e3bb Add Batoto Scanlator to Chapter view (#789)
* Added scanlator for Batoto on chapter list

* adjusted item_chapter layout for scanlator
adjusted so db chapters get updated if scanlator does not match source scanlator

* adjusted item_chapter layout for scanlator
adjusted chapter holder to dynamically set title max lines depending on if scanlator exists

* fixed excess blank line

* changed scanlator to be instantiated instead lateint to prevent toast message erro when viewing chapters by catalog

* changed item_chapter.xml to constraint layout

* removed accidental changes to catalog

* cleaned up code.

* fixed issue where long title was running into 3 dot menu
fixed issue where no scanlator for manga was causing date to not be bottom lined
fixed general chapter layout to be more similar to existing

* allow scanlator to be null

* fixed merge issue

* fixed merge issue

* attempt to fix whitespace carriage return issue

* attempt to fix whitespace carriage return issue

* attempt to fix whitespace carriage return issue
2017-08-15 15:05:41 +02:00
b79855c01d Remove circle image view dependency 2017-08-09 12:38:54 +02:00
17fe501a6d Ask permissions once. Fixes #892 2017-08-07 11:04:27 +02:00
len
8201b367ec Fix most crashes with extensions and the release version. Crop borders support in android O 2017-08-06 16:19:25 +02:00
len
6c242084ca Fallback chapter cache to internal storage 2017-08-03 21:44:31 +02:00
aefe7b176a Fixes case where manga name ends with s. (#919) 2017-07-31 20:04:14 +02:00
6059b85e58 Fix library category not updatable when empty. Closes #907 2017-07-27 09:21:15 +02:00
len
aa46c52eee Crop borders for webtoons. Closes #904 2017-07-26 20:35:31 +02:00
d3cbfbdb59 Add workaround for disappearing menu items 2017-07-26 11:29:03 +02:00
cc9b77b876 Simultaneous download will now show on start. (#911) 2017-07-26 10:49:13 +02:00
len
1568ac9e8a Release 0.6.1 2017-07-08 19:01:49 +02:00
len
1129dacdfa Downgrade jsoup 2017-07-08 18:58:53 +02:00
324 changed files with 9402 additions and 3799 deletions

View File

@ -1,6 +1,17 @@
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/WrBkRk4)
3. What is your type of issue?
* [Catalogue request](#catalogue-requests)
* [Bugs](#bugs)
* [Feature requests](#feature-requests)
* [Translations](https://github.com/inorichi/tachiyomi/wiki/Translation)
4. After following 1. and 3. you can [open your issue](https://github.com/inorichi/tachiyomi/issues/new)
***
# Catalogue requests
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions/issues, not here
* Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions#readme, not here
# Bugs
* Include version (Setting > About > Version)
@ -8,17 +19,9 @@
* Dev version is equal to the number of commits as seen in the main page
* Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
* If it could be device-dependent, try reproducing on another device (if possible)
* For large logs use http://pastebin.com/ (or similar)
* For multipart issues **use list** like this:
* [x] Done
* [ ] Not done
```
* [x] Done
* [ ] Not done
```
* Don't put together too many unrelated requests into one issue
* Don't group unrelated requests into one issue
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
@ -28,7 +31,3 @@ DON'T: https://github.com/inorichi/tachiyomi/issues/75
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed)
# Translations
[Wiki](https://github.com/inorichi/tachiyomi/wiki/Translation)

View File

@ -1,7 +1 @@
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
Remove line above and describe your issue here. Fill out version below. Use Preview.
Version: r000 or v0.0.0
(other relevant info like OS)

BIN
.github/readme-images/app-icon.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 KiB

BIN
.github/readme-images/screens.png vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1022 KiB

View File

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

20
.travis/build.sh Executable file
View File

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

15
.travis/deploy.sh Executable file
View File

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

BIN
.travis/secrets.tar.enc Normal file

Binary file not shown.

View File

@ -1,24 +1,38 @@
| Build | Download | F-Droid |
|-------|----------|-------------|
| [![TeamCity (simple build status)](https://img.shields.io/teamcity/https/teamcity.kanade.eu/s/tachiyomi_Build.svg)](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [![fdroid release](https://img.shields.io/badge/stable-f--droid.org-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid dev](https://img.shields.io/badge/dev-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) |
| Build | Download | F-Droid | Contribute | Contact |
|-------|----------|---------|------------|---------|
| [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/github/release/inorichi/tachiyomi.svg?maxAge=3600&label=stable)](https://github.com/inorichi/tachiyomi/releases) [![latest dev build](https://img.shields.io/badge/dev-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest) | [![fdroid release](https://img.shields.io/badge/stable-f--droid.org-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid dev](https://img.shields.io/badge/dev-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) | [![Translation status](http://weblate.j2ghz.com/widgets/tachiyomi/-/svg-badge.svg)](https://github.com/inorichi/tachiyomi/wiki/Translation) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/2dDQBv2) |
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
**Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened issues.**
# ![app icon](./.github/readme-images/app-icon.png)Tachiyomi
Tachiyomi is a free and open source manga reader for Android.
Keep in mind it's still a beta, so expect it to crash sometimes.
![screenshots of app](./.github/readme-images/screens.png)
# Features
## Features
* Online and offline reading
* Configurable reader with multiple viewers and settings
* MyAnimeList support
* Resume from the next unread chapter
* Chapter filtering
* Schedule searching for updates
Features include:
* Online reading from sources like Batoto, KissManga, MangaFox, [and more](https://github.com/inorichi/tachiyomi-extensions)
* Local reading of downloaded manga
* Configurable reader with multiple viewers, reading directions and other settings
* MyAnimeList, AniList, and Kitsu support
* Categories to organize your library
* Light and dark themes
* Schedule updating your library for new chapters
* Create backups locally or to your cloud service of choice
## Download
Get the app from our [releases page](https://github.com/inorichi/tachiyomi/releases) or from [f-droid](https://f-droid.org/packages/eu.kanade.tachiyomi/).
If you want to try new features before they get to the stable release, you can download the dev version [here](http://tachiyomi.kanade.eu/latest) (auto-updates not included), or add our [F-Droid repo](https://github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions).
## Issues, Feature Requests and Contributing
If you want to open an issue, please read [contributing guidelines](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md). Your issue may be closed otherwise.
## FAQ
[See our wiki.](https://github.com/inorichi/tachiyomi/wiki/FAQ)
You can also reach out to us on [Discord](https://discord.gg/WrBkRk4).
## License

View File

@ -3,10 +3,10 @@ import java.text.SimpleDateFormat
apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
apply plugin: 'kotlin-kapt'
apply plugin: 'com.github.zellius.shortcut-helper'
if (file("custom.gradle").exists()) {
apply from: "custom.gradle"
}
shortcutHelper.filePath = './shortcuts.xml'
ext {
// Git is needed in your system PATH for these commands to work.
@ -29,17 +29,17 @@ ext {
}
android {
compileSdkVersion 25
buildToolsVersion "25.0.2"
compileSdkVersion 26
buildToolsVersion "27.0.1"
publishNonDefault true
defaultConfig {
applicationId "eu.kanade.tachiyomi"
minSdkVersion 16
targetSdkVersion 25
targetSdkVersion 26
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 23
versionName "0.6.0"
versionCode 31
versionName "0.6.8"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -67,15 +67,20 @@ android {
}
}
flavorDimensions "default"
productFlavors {
standard {
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
dimension "default"
}
fdroid {
dimension "default"
}
dev {
minSdkVersion 21
resConfigs "en", "xxhdpi"
dimension "default"
}
}
@ -97,127 +102,137 @@ android {
dependencies {
// Modified dependencies
compile 'com.github.inorichi:subsampling-scale-image-view:01e5385'
compile 'com.github.inorichi:junrar-android:634c1f5'
implementation 'com.github.inorichi:subsampling-scale-image-view:c19b883'
implementation 'com.github.inorichi:junrar-android:634c1f5'
// Android support library
final support_library_version = '25.4.0'
compile "com.android.support:support-v4:$support_library_version"
compile "com.android.support:appcompat-v7:$support_library_version"
compile "com.android.support:cardview-v7:$support_library_version"
compile "com.android.support:design:$support_library_version"
compile "com.android.support:recyclerview-v7:$support_library_version"
compile "com.android.support:support-annotations:$support_library_version"
compile "com.android.support:customtabs:$support_library_version"
final support_library_version = '27.0.2'
implementation "com.android.support:support-v4:$support_library_version"
implementation "com.android.support:appcompat-v7:$support_library_version"
implementation "com.android.support:cardview-v7:$support_library_version"
implementation "com.android.support:design:$support_library_version"
implementation "com.android.support:recyclerview-v7:$support_library_version"
implementation "com.android.support:preference-v7:$support_library_version"
implementation "com.android.support:support-annotations:$support_library_version"
implementation "com.android.support:customtabs:$support_library_version"
compile 'com.android.support.constraint:constraint-layout:1.0.2'
implementation 'com.android.support.constraint:constraint-layout:1.0.2'
compile 'com.android.support:multidex:1.0.1'
implementation 'com.android.support:multidex:1.0.2'
// ReactiveX
compile 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.3.0'
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
compile 'com.github.pwittchen:reactivenetwork:0.7.0'
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'io.reactivex:rxjava:1.3.4'
implementation 'com.jakewharton.rxrelay:rxrelay:1.2.0'
implementation 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
implementation 'com.github.pwittchen:reactivenetwork:0.7.0'
// Network client
compile "com.squareup.okhttp3:okhttp:3.8.1"
compile 'com.squareup.okio:okio:1.13.0'
implementation "com.squareup.okhttp3:okhttp:3.9.1"
implementation 'com.squareup.okio:okio:1.13.0'
// REST
final retrofit_version = '2.3.0'
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
implementation "com.squareup.retrofit2:retrofit:$retrofit_version"
implementation "com.squareup.retrofit2:converter-gson:$retrofit_version"
implementation "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
// JSON
compile 'com.google.code.gson:gson:2.8.1'
compile 'com.github.salomonbrys.kotson:kotson:2.5.0'
implementation 'com.google.code.gson:gson:2.8.2'
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
// YAML
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
implementation 'com.github.bmoliveira:snake-yaml:v1.18-android'
// JavaScript engine
compile 'com.squareup.duktape:duktape-android:1.1.0'
implementation 'com.squareup.duktape:duktape-android:1.2.0'
// Disk
compile 'com.jakewharton:disklrucache:2.0.2'
compile 'com.github.seven332:unifile:1.0.0'
implementation 'com.jakewharton:disklrucache:2.0.2'
implementation 'com.github.inorichi:unifile:e9ee588'
// HTML parser
compile 'org.jsoup:jsoup:1.10.3'
implementation 'org.jsoup:jsoup:1.10.2'
// Job scheduling
compile 'com.evernote:android-job:1.1.11'
compile 'com.google.android.gms:play-services-gcm:11.0.1'
implementation 'com.evernote:android-job:1.2.1'
implementation 'com.google.android.gms:play-services-gcm:11.6.2'
// Changelog
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
implementation 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
// Database
compile "com.pushtorefresh.storio:sqlite:1.13.0"
implementation "com.pushtorefresh.storio:sqlite:1.13.0"
// Model View Presenter
final nucleus_version = '3.0.0'
compile "info.android15.nucleus:nucleus:$nucleus_version"
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
implementation "info.android15.nucleus:nucleus:$nucleus_version"
implementation "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
// Dependency injection
compile "uy.kohesive.injekt:injekt-core:1.16.1"
implementation "uy.kohesive.injekt:injekt-core:1.16.1"
// Image library
compile 'com.github.bumptech.glide:glide:3.8.0'
compile 'com.github.bumptech.glide:okhttp3-integration:1.5.0@aar'
final glide_version = '4.3.1'
implementation "com.github.bumptech.glide:glide:$glide_version"
implementation "com.github.bumptech.glide:okhttp3-integration:$glide_version"
kapt "com.github.bumptech.glide:compiler:$glide_version"
// Transformations
compile 'jp.wasabeef:glide-transformations:2.0.2'
implementation 'jp.wasabeef:glide-transformations:3.0.1'
// Logging
compile 'com.jakewharton.timber:timber:4.5.1'
implementation 'com.jakewharton.timber:timber:4.6.0'
// Crash reports
compile 'ch.acra:acra:4.9.2'
implementation 'ch.acra:acra:4.9.2'
// Sort
compile 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
implementation 'com.github.gpanther:java-nat-sort:natural-comparator-1.1'
// UI
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
compile 'eu.davidea:flexible-adapter:5.0.0-rc1'
compile 'com.nononsenseapps:filepicker:2.5.2'
compile 'com.github.amulyakhare:TextDrawable:558677e'
compile 'com.afollestad.material-dialogs:core:0.9.4.5'
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
compile 'de.hdodenhof:circleimageview:2.1.0'
compile 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
implementation 'com.dmitrymalkovich.android:material-design-dimens:1.4'
implementation 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
implementation 'eu.davidea:flexible-adapter:5.0.0-rc3'
implementation 'com.nononsenseapps:filepicker:2.5.2'
implementation 'com.github.amulyakhare:TextDrawable:558677e'
implementation('com.afollestad.material-dialogs:core:0.9.4.7') {
exclude group: "com.android.support", module: "support-v13"
}
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
implementation 'com.github.mthli:Slice:v1.2'
// Conductor
compile "com.bluelinelabs:conductor:2.1.4"
compile 'com.github.inorichi:conductor-support-preference:9e36460'
implementation "com.bluelinelabs:conductor:2.1.4"
implementation 'com.github.inorichi:conductor-support-preference:26.0.2'
// RxBindings
final rxbindings_version = '1.0.1'
compile "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
compile "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
compile "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
compile "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
implementation "com.jakewharton.rxbinding:rxbinding-kotlin:$rxbindings_version"
implementation "com.jakewharton.rxbinding:rxbinding-appcompat-v7-kotlin:$rxbindings_version"
implementation "com.jakewharton.rxbinding:rxbinding-support-v4-kotlin:$rxbindings_version"
implementation "com.jakewharton.rxbinding:rxbinding-recyclerview-v7-kotlin:$rxbindings_version"
// Tests
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1'
testCompile 'org.mockito:mockito-core:1.10.19'
testImplementation 'junit:junit:4.12'
testImplementation 'org.assertj:assertj-core:1.7.1'
testImplementation 'org.mockito:mockito-core:1.10.19'
final robolectric_version = '3.1.4'
testCompile "org.robolectric:robolectric:$robolectric_version"
testCompile "org.robolectric:shadows-multidex:$robolectric_version"
testCompile "org.robolectric:shadows-play-services:$robolectric_version"
testImplementation "org.robolectric:robolectric:$robolectric_version"
testImplementation "org.robolectric:shadows-multidex:$robolectric_version"
testImplementation "org.robolectric:shadows-play-services:$robolectric_version"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
implementation "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
final coroutines_version = '0.19.1'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}
buildscript {
ext.kotlin_version = '1.1.3'
ext.kotlin_version = '1.2.0'
repositories {
mavenCentral()
}
@ -230,49 +245,11 @@ repositories {
mavenCentral()
}
// Workaround to force a support lib version
configurations.all {
resolutionStrategy.eachDependency { details ->
def requested = details.requested
if (requested.group == 'com.android.support') {
if (!requested.name.startsWith("multidex")) {
details.useVersion '25.4.0'
}
}
kotlin {
experimental {
coroutines 'enable'
}
}
// add support for placeholders in resource files
//https://code.google.com/p/android/issues/detail?id=69224
def replacePlaceholdersInFile(basePath, fileName, placeholders) {
def file = new File(basePath, fileName);
if (!file.exists()) {
logger.quiet("Unable to replace placeholders in " + file.toString() + ". File cannot be found.")
return;
}
logger.debug("Replacing placeholders in " + file.toString())
logger.debug("Placeholders: " + placeholders.toString())
def content = file.getText('UTF-8')
placeholders.each { entry ->
content = content.replaceAll("\\\$\\{${entry.key}\\}", entry.value)
}
file.write(content, 'UTF-8')
androidExtensions {
experimental = true
}
afterEvaluate {
android.applicationVariants.all { variant ->
variant.outputs.each { output ->
output.processResources.doFirst {
// prepare placeholder map from manifestPlaceholders including applicationId placeholder
def placeholders = variant.mergedFlavor.manifestPlaceholders + [applicationId: variant.applicationId]
replacePlaceholdersInFile(resDir, 'xml-v25/shortcuts.xml', placeholders)
}
}
}
}

View File

@ -6,6 +6,14 @@
-keep class com.hippo.image.** { *; }
-keep interface com.hippo.image.** { *; }
-dontwarn nucleus.view.NucleusActionBarActivity
# Extensions may require methods unused in the core app
-keep class org.jsoup.** { *; }
-keep class kotlin.** { *; }
-keep class okhttp3.** { *; }
-keep class com.google.gson.** { *; }
-keep class com.github.salomonbrys.kotson.** { *; }
# OkHttp
-dontwarn okhttp3.**
@ -16,6 +24,7 @@
# Glide specific rules #
# https://github.com/bumptech/glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.AppGlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES;
public *;

View File

@ -9,8 +9,7 @@
android:shortcutShortLabel="@string/label_library">
<intent
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
android:targetPackage="${applicationId}" />
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut>
<shortcut
android:enabled="true"
@ -21,8 +20,7 @@
android:shortcutShortLabel="@string/short_recent_updates">
<intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
android:targetPackage="${applicationId}" />
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut>
<shortcut
android:enabled="true"
@ -33,8 +31,7 @@
android:shortcutShortLabel="@string/label_recent_manga">
<intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
android:targetPackage="${applicationId}" />
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut>
<shortcut
android:enabled="true"
@ -45,7 +42,6 @@
android:shortcutShortLabel="@string/label_catalogues">
<intent
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity"
android:targetPackage="${applicationId}" />
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut>
</shortcuts>

View File

@ -0,0 +1,34 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="108dp"
android:height="108dp"
android:viewportWidth="108.0"
android:viewportHeight="108.0">
<path
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
android:fillType="evenOdd"
android:fillColor="#000"/>
<path
android:pathData="M14.5,7L86.5,7A7,7 0,0 1,93.5 14L93.5,95A7,7 0,0 1,86.5 102L14.5,102A7,7 0,0 1,7.5 95L7.5,14A7,7 0,0 1,14.5 7z"
android:fillType="evenOdd"
android:fillColor="#455A64"/>
<path
android:pathData="M7.5,12.01C7.5,9.24 9.74,7 12.5,7L17.5,7L17.5,102L12.5,102C9.74,102 7.5,99.77 7.5,96.99L7.5,12.01Z"
android:fillType="evenOdd"
android:fillColor="#607D8B"/>
<path
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
android:fillType="evenOdd"
android:fillColor="#000"/>
<path
android:pathData="M54,54.5m-25.5,0a25.5,25.5 0,1 1,51 0a25.5,25.5 0,1 1,-51 0"
android:fillType="evenOdd"
android:fillColor="#CE2828"/>
<path
android:pathData="M54,54.5m-19.94,0a19.94,19.94 0,1 1,39.87 0a19.94,19.94 0,1 1,-39.87 0"
android:fillType="evenOdd"
android:fillColor="#FFF"/>
<path
android:pathData="M52.04,46.3L47.42,46.3C46.14,46.3 44.93,46.23 44.2,46.14L44.2,49.76C45,49.65 46.16,49.6 47.42,49.6L60.58,49.6C61.86,49.6 63.02,49.65 63.82,49.76L63.82,46.14C63.09,46.23 61.86,46.3 60.58,46.3L55.69,46.3L55.69,45.07C55.69,44.43 55.73,43.95 55.82,43.45L51.9,43.45C51.99,44 52.04,44.43 52.04,45.07L52.04,46.3ZM46.78,60.68C45.46,60.68 44.29,60.63 43.45,60.52L43.45,64.14C44.34,64.03 45.46,63.98 46.78,63.98L61.29,63.98C62.57,63.98 63.71,64.03 64.57,64.14L64.57,60.52C63.73,60.63 62.57,60.68 61.29,60.68L58.24,60.68C59.33,58.06 59.99,56.23 60.7,53.91C61.34,51.81 61.34,51.81 61.56,51.13L57.58,50.06C57.51,50.93 57.37,51.52 56.89,53.41C56.19,56.14 55.32,58.74 54.5,60.68L46.78,60.68ZM46.48,51.36C47.55,54.02 48.28,56.53 49.03,60.15L52.66,58.9C51.65,54.98 50.92,52.66 49.94,50.11L46.48,51.36Z"
android:fillType="evenOdd"
android:fillColor="#000"/>
</vector>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/transparent"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

View File

@ -0,0 +1,5 @@
<?xml version="1.0" encoding="utf-8"?>
<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android">
<background android:drawable="@android:color/transparent"/>
<foreground android:drawable="@drawable/ic_launcher_foreground"/>
</adaptive-icon>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

View File

@ -8,6 +8,7 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.REQUEST_INSTALL_PACKAGES" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<application
@ -21,14 +22,14 @@
android:theme="@style/Theme.Tachiyomi">
<activity
android:name=".ui.main.MainActivity"
android:launchMode="singleTask">
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data android:name="android.app.shortcuts"
android:resource="@xml/shortcuts"/>
<!--suppress AndroidDomInspection -->
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
</activity>
<activity
android:name=".ui.reader.ReaderActivity"
@ -85,7 +86,7 @@
android:exported="false" />
<service
android:name=".data.updater.UpdateDownloaderService"
android:name=".data.updater.UpdaterService"
android:exported="false" />
<service
@ -96,10 +97,6 @@
android:name=".data.backup.BackupRestoreService"
android:exported="false"/>
<meta-data
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
android:value="GlideModule" />
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -7,7 +7,8 @@ import android.support.multidex.MultiDex
import com.evernote.android.job.JobManager
import eu.kanade.tachiyomi.data.backup.BackupCreatorJob
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import eu.kanade.tachiyomi.util.LocaleHelper
import org.acra.ACRA
import org.acra.annotation.ReportsCrashes
@ -27,13 +28,14 @@ open class App : Application() {
override fun onCreate() {
super.onCreate()
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this))
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
setupAcra()
setupJobManager()
setupNotificationChannels()
LocaleHelper.updateConfiguration(this, resources.configuration)
}
@ -58,11 +60,15 @@ open class App : Application() {
JobManager.create(this).addJobCreator { tag ->
when (tag) {
LibraryUpdateJob.TAG -> LibraryUpdateJob()
UpdateCheckerJob.TAG -> UpdateCheckerJob()
UpdaterJob.TAG -> UpdaterJob()
BackupCreatorJob.TAG -> BackupCreatorJob()
else -> null
}
}
}
protected open fun setupNotificationChannels() {
Notifications.createChannels(this)
}
}

View File

@ -1,10 +0,0 @@
package eu.kanade.tachiyomi
object Constants {
const val NOTIFICATION_LIBRARY_PROGRESS_ID = 1
const val NOTIFICATION_LIBRARY_RESULT_ID = 2
const val NOTIFICATION_UPDATER_ID = 3
const val NOTIFICATION_DOWNLOAD_CHAPTER_ID = 4
const val NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID = 5
const val NOTIFICATION_DOWNLOAD_IMAGE_ID = 6
}

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi
import eu.kanade.tachiyomi.data.library.LibraryUpdateJob
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.updater.UpdateCheckerJob
import eu.kanade.tachiyomi.data.updater.UpdaterJob
import java.io.File
object Migrations {
@ -25,7 +25,7 @@ object Migrations {
if (oldVersion < 14) {
// Restore jobs after upgrading to evernote's job scheduler.
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
UpdateCheckerJob.setupTask()
UpdaterJob.setupTask()
}
LibraryUpdateJob.setupTask()
}
@ -45,6 +45,16 @@ object Migrations {
}
}
}
if (oldVersion < 26) {
// Delete external chapter cache dir.
val extCache = context.externalCacheDir
if (extCache != null) {
val chapterCache = File(extCache, "chapter_disk_cache")
if (chapterCache.exists()) {
chapterCache.deleteRecursively()
}
}
}
return true
}
return false

View File

@ -46,7 +46,7 @@ class BackupCreateService : IntentService(NAME) {
* Make a backup from library
*
* @param context context of application
* @param path path of Uri
* @param uri path of Uri
* @param flags determines what to backup
* @param isJob backup called from job
*/
@ -80,13 +80,10 @@ class BackupCreateService : IntentService(NAME) {
* @param uri path of Uri
* @param isJob backup called from job
*/
fun createBackupFromApp(uri: Uri, flags: Int, isJob: Boolean) {
private fun createBackupFromApp(uri: Uri, flags: Int, isJob: Boolean) {
// Create root object
val root = JsonObject()
// Create information object
val information = JsonObject()
// Create manga array
val mangaEntries = JsonArray()
@ -116,8 +113,9 @@ class BackupCreateService : IntentService(NAME) {
try {
// When BackupCreatorJob
if (isJob) {
// Get dir of file
val dir = UniFile.fromUri(this, uri)
// Get dir of file and create
var dir = UniFile.fromUri(this, uri)
dir = dir.createDirectory("automatic")
// Delete older backups
val numberOfBackups = backupManager.numberOfBackups()

View File

@ -8,13 +8,12 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
class BackupCreatorJob : Job() {
override fun onRunJob(params: Params): Result {
val preferences = Injekt.get<PreferencesHelper>()
val uri = Uri.fromFile(File(preferences.backupsDirectory().getOrDefault()))
val uri = Uri.parse(preferences.backupsDirectory().getOrDefault())
val flags = BackupCreateService.BACKUP_ALL
BackupCreateService.makeBackup(context, uri, flags, true)
return Result.SUCCESS
@ -29,7 +28,6 @@ class BackupCreatorJob : Job() {
if (interval > 0) {
JobRequest.Builder(TAG)
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
.setPersisted(true)
.setUpdateCurrent(true)
.build()
.schedule()

View File

@ -23,6 +23,7 @@ import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.util.syncChaptersWithSource
@ -41,6 +42,11 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*/
internal val sourceManager: SourceManager by injectLazy()
/**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
/**
* Version of parser
*/
@ -67,18 +73,16 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
parser = initParser()
}
private fun initParser(): Gson {
return when (version) {
1 -> GsonBuilder().create()
2 -> GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
.create()
else -> throw Exception("Json version unknown")
}
private fun initParser(): Gson = when (version) {
1 -> GsonBuilder().create()
2 -> GsonBuilder()
.registerTypeAdapter<MangaImpl>(MangaTypeAdapter.build())
.registerTypeHierarchyAdapter<ChapterImpl>(ChapterTypeAdapter.build())
.registerTypeAdapter<CategoryImpl>(CategoryTypeAdapter.build())
.registerTypeAdapter<DHistory>(HistoryTypeAdapter.build())
.registerTypeHierarchyAdapter<TrackImpl>(TrackTypeAdapter.build())
.create()
else -> throw Exception("Json version unknown")
}
/**
@ -300,23 +304,26 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
val trackToUpdate = ArrayList<Track>()
for (track in tracks) {
var isInDatabase = false
for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) {
// The sync is already in the db, only update its fields
if (track.remote_id != dbTrack.remote_id) {
dbTrack.remote_id = track.remote_id
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
var isInDatabase = false
for (dbTrack in dbTracks) {
if (track.sync_id == dbTrack.sync_id) {
// The sync is already in the db, only update its fields
if (track.remote_id != dbTrack.remote_id) {
dbTrack.remote_id = track.remote_id
}
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
}
dbTrack.last_chapter_read = Math.max(dbTrack.last_chapter_read, track.last_chapter_read)
isInDatabase = true
trackToUpdate.add(dbTrack)
break
}
}
if (!isInDatabase) {
// Insert new sync. Let the db assign the id
track.id = null
trackToUpdate.add(track)
if (!isInDatabase) {
// Insert new sync. Let the db assign the id
track.id = null
trackToUpdate.add(track)
}
}
}
// Update database
@ -361,32 +368,29 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*
* @return [Manga], null if not found
*/
internal fun getMangaFromDatabase(manga: Manga): Manga? {
return databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
}
internal fun getMangaFromDatabase(manga: Manga): Manga? =
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
/**
* Returns list containing manga from library
*
* @return [Manga] from library
*/
internal fun getFavoriteManga(): List<Manga> {
return databaseHelper.getFavoriteMangas().executeAsBlocking()
}
internal fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking()
/**
* Inserts manga and returns id
*
* @return id of [Manga], null if not found
*/
internal fun insertManga(manga: Manga): Long? {
return databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
}
internal fun insertManga(manga: Manga): Long? =
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
/**
* Inserts list of chapters
*/
internal fun insertChapters(chapters: List<Chapter>) {
private fun insertChapters(chapters: List<Chapter>) {
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
}
@ -395,7 +399,5 @@ class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
*
* @return number of backups selected by user
*/
fun numberOfBackups(): Int {
return preferences.numberOfBackups().getOrDefault()
}
fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault()
}

View File

@ -21,6 +21,7 @@ import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.chop
import eu.kanade.tachiyomi.util.isServiceRunning
@ -49,9 +50,8 @@ class BackupRestoreService : Service() {
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean {
return context.isServiceRunning(BackupRestoreService::class.java)
}
private fun isRunning(context: Context): Boolean =
context.isServiceRunning(BackupRestoreService::class.java)
/**
* Starts a service to restore a backup from Json
@ -113,7 +113,13 @@ class BackupRestoreService : Service() {
*/
private val db: DatabaseHelper by injectLazy()
lateinit var executor: ExecutorService
/**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
private lateinit var executor: ExecutorService
/**
* Method called when the service is created. It injects dependencies and acquire the wake lock.
@ -142,9 +148,7 @@ class BackupRestoreService : Service() {
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onBind(intent: Intent): IBinder? = null
/**
* Method called when the service receives an intent.
@ -164,7 +168,7 @@ class BackupRestoreService : Service() {
subscription = Observable.using(
{ db.lowLevel().beginTransaction() },
{ getRestoreObservable(uri).doOnNext{ db.lowLevel().setTransactionSuccessful() } },
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
{ executor.execute { db.lowLevel().endTransaction() } })
.doAfterTerminate { stopSelf(startId) }
.subscribeOn(Schedulers.from(executor))
@ -182,29 +186,33 @@ class BackupRestoreService : Service() {
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
val startTime = System.currentTimeMillis()
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
val json = JsonParser().parse(reader).asJsonObject
return Observable.just(Unit)
.map {
val reader = JsonReader(contentResolver.openInputStream(uri).bufferedReader())
val json = JsonParser().parse(reader).asJsonObject
// Get parser version
val version = json.get(VERSION)?.asInt ?: 1
// Get parser version
val version = json.get(VERSION)?.asInt ?: 1
// Initialize manager
backupManager = BackupManager(this, version)
// Initialize manager
backupManager = BackupManager(this, version)
val mangasJson = json.get(MANGAS).asJsonArray
val mangasJson = json.get(MANGAS).asJsonArray
restoreAmount = mangasJson.size() + 1 // +1 for categories
restoreProgress = 0
errors.clear()
restoreAmount = mangasJson.size() + 1 // +1 for categories
restoreProgress = 0
errors.clear()
// Restore categories
json.get(CATEGORIES)?.let {
backupManager.restoreCategories(it.asJsonArray)
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
}
// Restore categories
json.get(CATEGORIES)?.let {
backupManager.restoreCategories(it.asJsonArray)
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
}
return Observable.from(mangasJson)
mangasJson
}
.flatMap { Observable.from(it) }
.concatMap {
val obj = it.asJsonObject
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
@ -290,14 +298,14 @@ class BackupRestoreService : Service() {
val source = backupManager.sourceManager.get(manga.source) ?: return null
val dbManga = backupManager.getMangaFromDatabase(manga)
if (dbManga == null) {
return if (dbManga == null) {
// Manga not in database
return mangaFetchObservable(source, manga, chapters, categories, history, tracks)
mangaFetchObservable(source, manga, chapters, categories, history, tracks)
} else { // Manga in database
// Copy information from manga already in database
backupManager.restoreMangaNoFetch(manga, dbManga)
// Fetch rest of manga information
return mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
}
}
@ -317,20 +325,18 @@ class BackupRestoreService : Service() {
manga
}
.filter { it.id != null }
.flatMap { manga ->
chapterFetchObservable(source, manga, chapters)
.flatMap {
chapterFetchObservable(source, it, chapters)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnNext {
// Restore categories
backupManager.restoreCategoriesForManga(it, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(it, tracks)
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap {
trackingFetchObservable(it, tracks)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnCompleted {
restoreProgress += 1
@ -352,14 +358,12 @@ class BackupRestoreService : Service() {
}
}
.doOnNext {
// Restore categories
backupManager.restoreCategoriesForManga(it, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(it, tracks)
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap { manga ->
trackingFetchObservable(manga, tracks)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnCompleted {
restoreProgress += 1
@ -367,6 +371,17 @@ class BackupRestoreService : Service() {
}
}
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
// Restore categories
backupManager.restoreCategoriesForManga(manga, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(manga, tracks)
}
/**
* [Observable] that fetches chapter information
*
@ -379,10 +394,33 @@ class BackupRestoreService : Service() {
// If there's any error, return empty update and continue.
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
Pair(emptyList<Chapter>(), emptyList<Chapter>())
Pair(emptyList(), emptyList())
}
}
/**
* [Observable] that refreshes tracking information
* @param manga manga that needs updating.
* @param tracks list containing tracks from restore file.
* @return [Observable] that contains updated track item
*/
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
return Observable.from(tracks)
.concatMap { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service.isLogged) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
track
}
} else {
errors.add(Date() to "${manga.title} - ${service?.name} not logged in")
Observable.empty()
}
}
}
/**
* Called to update dialog in [BackupConst]

View File

@ -45,8 +45,7 @@ class ChapterCache(private val context: Context) {
private val gson: Gson by injectLazy()
/** Cache class used for cache management. */
private val diskCache = DiskLruCache.open(
File(context.externalCacheDir, PARAMETER_CACHE_DIRECTORY),
private val diskCache = DiskLruCache.open(File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE)
@ -82,10 +81,10 @@ class ChapterCache(private val context: Context) {
try {
// Remove the extension from the file to get the key of the cache
val key = file.substring(0, file.lastIndexOf("."))
val key = file.substringBeforeLast(".")
// Remove file from cache.
return diskCache.remove(key)
} catch (e: IOException) {
} catch (e: Exception) {
return false
}
}

View File

@ -20,7 +20,8 @@ class CoverCache(private val context: Context) {
/**
* Cache directory used for cache management.
*/
private val cacheDir = context.getExternalFilesDir("covers")
private val cacheDir = context.getExternalFilesDir("covers") ?:
File(context.filesDir, "covers").also { it.mkdirs() }
/**
* Returns the cover from cache.

View File

@ -17,7 +17,7 @@ class DbOpenHelper(context: Context)
/**
* Version of the database.
*/
const val DATABASE_VERSION = 4
const val DATABASE_VERSION = 5
}
override fun onCreate(db: SQLiteDatabase) = with(db) {
@ -51,6 +51,9 @@ class DbOpenHelper(context: Context)
if (oldVersion < 4) {
db.execSQL(ChapterTable.bookmarkUpdateQuery)
}
if (oldVersion < 5) {
db.execSQL(ChapterTable.addScanlator)
}
}
override fun onConfigure(db: SQLiteDatabase) {

View File

@ -20,6 +20,7 @@ import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_LAST_PAGE_READ
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_NAME
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_READ
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SCANLATOR
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_SOURCE_ORDER
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
@ -48,6 +49,7 @@ class ChapterPutResolver : DefaultPutResolver<Chapter>() {
put(COL_URL, obj.url)
put(COL_NAME, obj.name)
put(COL_READ, obj.read)
put(COL_SCANLATOR, obj.scanlator)
put(COL_BOOKMARK, obj.bookmark)
put(COL_DATE_FETCH, obj.date_fetch)
put(COL_DATE_UPLOAD, obj.date_upload)
@ -64,6 +66,7 @@ class ChapterGetResolver : DefaultGetResolver<Chapter>() {
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
scanlator = cursor.getString(cursor.getColumnIndex(COL_SCANLATOR))
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
bookmark = cursor.getInt(cursor.getColumnIndex(COL_BOOKMARK)) == 1
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))

View File

@ -65,9 +65,8 @@ class MangaPutResolver : DefaultPutResolver<Manga>() {
}
}
open class MangaGetResolver : DefaultGetResolver<Manga>() {
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
interface BaseMangaGetResolver {
fun mapBaseFromCursor(manga: Manga, cursor: Cursor) = manga.apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
@ -86,6 +85,13 @@ open class MangaGetResolver : DefaultGetResolver<Manga>() {
}
}
open class MangaGetResolver : DefaultGetResolver<Manga>(), BaseMangaGetResolver {
override fun mapFromCursor(cursor: Cursor): Manga {
return mapBaseFromCursor(MangaImpl(), cursor)
}
}
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()

View File

@ -10,6 +10,8 @@ class ChapterImpl : Chapter {
override lateinit var name: String
override var scanlator: String? = null
override var read: Boolean = false
override var bookmark: Boolean = false
@ -29,9 +31,7 @@ class ChapterImpl : Chapter {
if (other == null || javaClass != other.javaClass) return false
val chapter = other as Chapter
return url == chapter.url
}
override fun hashCode(): Int {

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.data.database.models
class LibraryManga : MangaImpl() {
var unread: Int = 0
var category: Int = 0
}

View File

@ -16,10 +16,6 @@ interface Manga : SManga {
var chapter_flags: Int
var unread: Int
var category: Int
fun setChapterOrder(order: Int) {
setFlags(order, SORT_MASK)
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.models
class MangaImpl : Manga {
open class MangaImpl : Manga {
override var id: Long? = null
@ -32,10 +32,6 @@ class MangaImpl : Manga {
override var chapter_flags: Int = 0
@Transient override var unread: Int = 0
@Transient override var category: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false

View File

@ -4,8 +4,10 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFavoritePutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaLastUpdatedPutResolver
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
@ -23,7 +25,7 @@ interface MangaQueries : DbProvider {
.prepare()
fun getLibraryMangas() = db.get()
.listOfObjects(Manga::class.java)
.listOfObjects(LibraryManga::class.java)
.withQuery(RawQuery.builder()
.query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
@ -73,6 +75,11 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaLastUpdatedPutResolver())
.prepare()
fun updateMangaFavorite(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaFavoritePutResolver())
.prepare()
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()

View File

@ -1,24 +1,23 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.database.Cursor
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
import eu.kanade.tachiyomi.data.database.models.Manga
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import eu.kanade.tachiyomi.data.database.mappers.BaseMangaGetResolver
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class LibraryMangaGetResolver : MangaGetResolver() {
class LibraryMangaGetResolver : DefaultGetResolver<LibraryManga>(), BaseMangaGetResolver {
companion object {
val INSTANCE = LibraryMangaGetResolver()
}
override fun mapFromCursor(cursor: Cursor): Manga {
val manga = super.mapFromCursor(cursor)
override fun mapFromCursor(cursor: Cursor): LibraryManga {
val manga = LibraryManga()
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD)
manga.unread = cursor.getInt(unreadColumn)
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
manga.category = cursor.getInt(categoryColumn)
mapBaseFromCursor(manga, cursor)
manga.unread = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_UNREAD))
manga.category = cursor.getInt(cursor.getColumnIndex(MangaTable.COL_CATEGORY))
return manga
}

View File

@ -0,0 +1,33 @@
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 MangaFavoritePutResolver : 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_FAVORITE, manga.favorite)
}
}

View File

@ -14,6 +14,8 @@ object ChapterTable {
const val COL_READ = "read"
const val COL_SCANLATOR = "scanlator"
const val COL_BOOKMARK = "bookmark"
const val COL_DATE_FETCH = "date_fetch"
@ -32,6 +34,7 @@ object ChapterTable {
$COL_MANGA_ID INTEGER NOT NULL,
$COL_URL TEXT NOT NULL,
$COL_NAME TEXT NOT NULL,
$COL_SCANLATOR TEXT,
$COL_READ BOOLEAN NOT NULL,
$COL_BOOKMARK BOOLEAN NOT NULL,
$COL_LAST_PAGE_READ INT NOT NULL,
@ -52,4 +55,7 @@ object ChapterTable {
val bookmarkUpdateQuery: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_BOOKMARK BOOLEAN DEFAULT FALSE"
val addScanlator: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SCANLATOR TEXT DEFAULT NULL"
}

View File

@ -0,0 +1,252 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.net.Uri
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.SourceManager
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.concurrent.TimeUnit
/**
* Cache where we dump the downloads directory from the filesystem. This class is needed because
* directory checking is expensive and it slowdowns the app. The cache is invalidated by the time
* defined in [renewInterval] as we don't have any control over the filesystem and the user can
* delete the folders at any time without the app noticing.
*
* @param context the application context.
* @param provider the downloads directories provider.
* @param sourceManager the source manager.
* @param preferences the preferences of the app.
*/
class DownloadCache(private val context: Context,
private val provider: DownloadProvider,
private val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()) {
/**
* The interval after which this cache should be invalidated. 1 hour shouldn't cause major
* issues, as the cache is only used for UI feedback.
*/
private val renewInterval = TimeUnit.HOURS.toMillis(1)
/**
* The last time the cache was refreshed.
*/
private var lastRenew = 0L
/**
* The root directory for downloads.
*/
private var rootDir = RootDirectory(getDirectoryFromPreference())
init {
preferences.downloadsDirectory().asObservable()
.skip(1)
.subscribe {
lastRenew = 0L // invalidate cache
rootDir = RootDirectory(getDirectoryFromPreference())
}
}
/**
* Returns the downloads directory from the user's preferences.
*/
private fun getDirectoryFromPreference(): UniFile {
val dir = preferences.downloadsDirectory().getOrDefault()
return UniFile.fromUri(context, Uri.parse(dir))
}
/**
* Returns true if the chapter is downloaded.
*
* @param chapter the chapter to check.
* @param manga the manga of the chapter.
* @param skipCache whether to skip the directory cache and check in the filesystem.
*/
fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean): Boolean {
if (skipCache) {
val source = sourceManager.get(manga.source) ?: return false
return provider.findChapterDir(chapter, manga, source) != null
}
checkRenew()
val sourceDir = rootDir.files[manga.source]
if (sourceDir != null) {
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
if (mangaDir != null) {
return provider.getChapterDirName(chapter) in mangaDir.files
}
}
return false
}
/**
* Returns the amount of downloaded chapters for a manga.
*
* @param manga the manga to check.
*/
fun getDownloadCount(manga: Manga): Int {
checkRenew()
val sourceDir = rootDir.files[manga.source]
if (sourceDir != null) {
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)]
if (mangaDir != null) {
return mangaDir.files.size
}
}
return 0
}
/**
* Checks if the cache needs a renewal and performs it if needed.
*/
@Synchronized
private fun checkRenew() {
if (lastRenew + renewInterval < System.currentTimeMillis()) {
renew()
lastRenew = System.currentTimeMillis()
}
}
/**
* Renews the downloads cache.
*/
private fun renew() {
val onlineSources = sourceManager.getOnlineSources()
val sourceDirs = rootDir.dir.listFiles()
.orEmpty()
.associate { it.name to SourceDirectory(it) }
.mapNotNullKeys { entry ->
onlineSources.find { provider.getSourceDirName(it) == entry.key }?.id
}
rootDir.files = sourceDirs
sourceDirs.values.forEach { sourceDir ->
val mangaDirs = sourceDir.dir.listFiles()
.orEmpty()
.associateNotNullKeys { it.name to MangaDirectory(it) }
sourceDir.files = mangaDirs
mangaDirs.values.forEach { mangaDir ->
val chapterDirs = mangaDir.dir.listFiles()
.orEmpty()
.mapNotNull { it.name }
.toHashSet()
mangaDir.files = chapterDirs
}
}
}
/**
* Adds a chapter that has just been download to this cache.
*
* @param chapterDirName the downloaded chapter's directory name.
* @param mangaUniFile the directory of the manga.
* @param manga the manga of the chapter.
*/
@Synchronized
fun addChapter(chapterDirName: String, mangaUniFile: UniFile, manga: Manga) {
// Retrieve the cached source directory or cache a new one
var sourceDir = rootDir.files[manga.source]
if (sourceDir == null) {
val source = sourceManager.get(manga.source) ?: return
val sourceUniFile = provider.findSourceDir(source) ?: return
sourceDir = SourceDirectory(sourceUniFile)
rootDir.files += manga.source to sourceDir
}
// Retrieve the cached manga directory or cache a new one
val mangaDirName = provider.getMangaDirName(manga)
var mangaDir = sourceDir.files[mangaDirName]
if (mangaDir == null) {
mangaDir = MangaDirectory(mangaUniFile)
sourceDir.files += mangaDirName to mangaDir
}
// Save the chapter directory
mangaDir.files += chapterDirName
}
/**
* Removes a chapter that has been deleted from this cache.
*
* @param chapter the chapter to remove.
* @param manga the manga of the chapter.
*/
@Synchronized
fun removeChapter(chapter: Chapter, manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return
val mangaDir = sourceDir.files[provider.getMangaDirName(manga)] ?: return
val chapterDirName = provider.getChapterDirName(chapter)
if (chapterDirName in mangaDir.files) {
mangaDir.files -= chapterDirName
}
}
/**
* Removes a manga that has been deleted from this cache.
*
* @param manga the manga to remove.
*/
@Synchronized
fun removeManga(manga: Manga) {
val sourceDir = rootDir.files[manga.source] ?: return
val mangaDirName = provider.getMangaDirName(manga)
if (mangaDirName in sourceDir.files) {
sourceDir.files -= mangaDirName
}
}
/**
* Class to store the files under the root downloads directory.
*/
private class RootDirectory(val dir: UniFile,
var files: Map<Long, SourceDirectory> = hashMapOf())
/**
* Class to store the files under a source directory.
*/
private class SourceDirectory(val dir: UniFile,
var files: Map<String, MangaDirectory> = hashMapOf())
/**
* Class to store the files under a manga directory.
*/
private class MangaDirectory(val dir: UniFile,
var files: Set<String> = hashSetOf())
/**
* Returns a new map containing only the key entries of [transform] that are not null.
*/
private inline fun <K, V, R> Map<out K, V>.mapNotNullKeys(transform: (Map.Entry<K?, V>) -> R?): Map<R, V> {
val destination = LinkedHashMap<R, V>()
forEach { element -> transform(element)?.let { destination.put(it, element.value) } }
return destination
}
/**
* Returns a map from a list containing only the key entries of [transform] that are not null.
*/
private inline fun <T, K, V> Array<T>.associateNotNullKeys(transform: (T) -> Pair<K?, V>): Map<K, V> {
val destination = LinkedHashMap<K, V>()
for (element in this) {
val (key, value) = transform(element)
if (key != null) {
destination.put(key, value)
}
}
return destination
}
}

View File

@ -24,10 +24,15 @@ class DownloadManager(context: Context) {
*/
private val provider = DownloadProvider(context)
/**
* Cache of downloaded chapters.
*/
private val cache = DownloadCache(context, provider)
/**
* Downloader whose only task is to download chapters.
*/
private val downloader = Downloader(context, provider)
private val downloader = Downloader(context, provider, cache)
/**
* Downloads queue, where the pending chapters are stored.
@ -80,9 +85,10 @@ class DownloadManager(context: Context) {
*
* @param manga the manga of the chapters.
* @param chapters the list of chapters to enqueue.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
downloader.queueChapters(manga, chapters)
fun downloadChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean = true) {
downloader.queueChapters(manga, chapters, autoStart)
}
/**
@ -94,7 +100,7 @@ class DownloadManager(context: Context) {
* @return an observable containing the list of pages from the chapter.
*/
fun buildPageList(source: Source, manga: Manga, chapter: Chapter): Observable<List<Page>> {
return buildPageList(provider.findChapterDir(source, manga, chapter))
return buildPageList(provider.findChapterDir(chapter, manga, source))
}
/**
@ -120,61 +126,45 @@ class DownloadManager(context: Context) {
}
/**
* Returns the directory name for a manga.
* Returns true if the chapter is downloaded.
*
* @param manga the manga to query.
*/
fun getMangaDirName(manga: Manga): String {
return provider.getMangaDirName(manga)
}
/**
* Returns the directory name for the given chapter.
*
* @param chapter the chapter to query.
*/
fun getChapterDirName(chapter: Chapter): String {
return provider.getChapterDirName(chapter)
}
/**
* Returns the download directory for a source if it exists.
*
* @param source the source to query.
*/
fun findSourceDir(source: Source): UniFile? {
return provider.findSourceDir(source)
}
/**
* Returns the directory for the given manga, if it exists.
*
* @param source the source of the manga.
* @param manga the manga to query.
*/
fun findMangaDir(source: Source, manga: Manga): UniFile? {
return provider.findMangaDir(source, manga)
}
/**
* Returns the directory for the given chapter, if it exists.
*
* @param source the source of the chapter.
* @param chapter the chapter to check.
* @param manga the manga of the chapter.
* @param chapter the chapter to query.
* @param skipCache whether to skip the directory cache and check in the filesystem.
*/
fun findChapterDir(source: Source, manga: Manga, chapter: Chapter): UniFile? {
return provider.findChapterDir(source, manga, chapter)
fun isChapterDownloaded(chapter: Chapter, manga: Manga, skipCache: Boolean = false): Boolean {
return cache.isChapterDownloaded(chapter, manga, skipCache)
}
/**
* Returns the amount of downloaded chapters for a manga.
*
* @param manga the manga to check.
*/
fun getDownloadCount(manga: Manga): Int {
return cache.getDownloadCount(manga)
}
/**
* Deletes the directory of a downloaded chapter.
*
* @param source the source of the chapter.
* @param manga the manga of the chapter.
* @param chapter the chapter to delete.
* @param manga the manga of the chapter.
* @param source the source of the chapter.
*/
fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) {
provider.findChapterDir(source, manga, chapter)?.delete()
fun deleteChapter(chapter: Chapter, manga: Manga, source: Source) {
provider.findChapterDir(chapter, manga, source)?.delete()
cache.removeChapter(chapter, manga)
}
/**
* Deletes the directory of a downloaded manga.
*
* @param manga the manga to delete.
* @param source the source of the manga.
*/
fun deleteManga(manga: Manga, source: Source) {
provider.findMangaDir(manga, source)?.delete()
cache.removeManga(manga)
}
}

View File

@ -3,12 +3,12 @@ package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.graphics.BitmapFactory
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.chop
import eu.kanade.tachiyomi.util.notificationManager
import java.util.regex.Pattern
@ -23,7 +23,7 @@ internal class DownloadNotifier(private val context: Context) {
* Notification builder.
*/
private val notification by lazy {
NotificationCompat.Builder(context)
NotificationCompat.Builder(context, Notifications.CHANNEL_DOWNLOADER)
.setLargeIcon(BitmapFactory.decodeResource(context.resources, R.mipmap.ic_launcher))
}
@ -69,7 +69,7 @@ internal class DownloadNotifier(private val context: Context) {
*
* @param id the id of the notification.
*/
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID) {
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_DOWNLOAD_CHAPTER) {
context.notificationManager.notify(id, build())
}
@ -86,7 +86,7 @@ internal class DownloadNotifier(private val context: Context) {
* those can only be dismissed by the user.
*/
fun dismiss() {
context.notificationManager.cancel(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID)
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
}
/**
@ -262,7 +262,7 @@ internal class DownloadNotifier(private val context: Context) {
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
}
notification.show(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID)
notification.show(Notifications.ID_DOWNLOAD_CHAPTER_ERROR)
// Reset download information
errorThrown = true

View File

@ -40,10 +40,10 @@ class DownloadProvider(private val context: Context) {
/**
* Returns the download directory for a manga. For internal use only.
*
* @param source the source of the manga.
* @param manga the manga to query.
* @param source the source of the manga.
*/
internal fun getMangaDir(source: Source, manga: Manga): UniFile {
internal fun getMangaDir(manga: Manga, source: Source): UniFile {
return downloadsDir
.createDirectory(getSourceDirName(source))
.createDirectory(getMangaDirName(manga))
@ -61,10 +61,10 @@ class DownloadProvider(private val context: Context) {
/**
* Returns the download directory for a manga if it exists.
*
* @param source the source of the manga.
* @param manga the manga to query.
* @param source the source of the manga.
*/
fun findMangaDir(source: Source, manga: Manga): UniFile? {
fun findMangaDir(manga: Manga, source: Source): UniFile? {
val sourceDir = findSourceDir(source)
return sourceDir?.findFile(getMangaDirName(manga))
}
@ -72,12 +72,12 @@ class DownloadProvider(private val context: Context) {
/**
* Returns the download directory for a chapter if it exists.
*
* @param source the source of the chapter.
* @param manga the manga of the chapter.
* @param chapter the chapter to query.
* @param manga the manga of the chapter.
* @param source the source of the chapter.
*/
fun findChapterDir(source: Source, manga: Manga, chapter: Chapter): UniFile? {
val mangaDir = findMangaDir(source, manga)
fun findChapterDir(chapter: Chapter, manga: Manga, source: Source): UniFile? {
val mangaDir = findMangaDir(manga, source)
return mangaDir?.findFile(getChapterDirName(chapter))
}

View File

@ -122,7 +122,7 @@ class DownloadService : Service() {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ state -> onNetworkStateChanged(state)
}, { error ->
}, { _ ->
toast(R.string.download_queue_error)
stopSelf()
})

View File

@ -16,6 +16,7 @@ import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.source.online.fetchAllImageUrlsFromPageList
import eu.kanade.tachiyomi.util.*
import kotlinx.coroutines.experimental.async
import okhttp3.Response
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
@ -36,8 +37,11 @@ import uy.kohesive.injekt.injectLazy
*
* @param context the application context.
* @param provider the downloads directory provider.
* @param cache the downloads cache, used to add the downloads to the cache after their completion.
*/
class Downloader(private val context: Context, private val provider: DownloadProvider) {
class Downloader(private val context: Context,
private val provider: DownloadProvider,
private val cache: DownloadCache) {
/**
* Store for persisting downloads across restarts.
@ -90,12 +94,10 @@ class Downloader(private val context: Context, private val provider: DownloadPro
@Volatile private var isRunning: Boolean = false
init {
Observable.fromCallable { store.restore() }
.map { downloads -> downloads.filter { isDownloadAllowed(it) } }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ downloads -> queue.addAll(downloads)
}, { error -> Timber.e(error) })
launchNow {
val chapters = async { store.restore() }
queue.addAll(chapters.await())
}
}
/**
@ -114,6 +116,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
val pending = queue.filter { it.status != Download.DOWNLOADED }
pending.forEach { if (it.status != Download.QUEUE) it.status = Download.QUEUE }
// Show download notification when simultaneous download > 1.
notifier.onProgressChange(queue)
downloadsRelay.call(pending)
return !pending.isEmpty()
}
@ -210,61 +215,57 @@ class Downloader(private val context: Context, private val provider: DownloadPro
}
/**
* Creates a download object for every chapter and adds them to the downloads queue. This method
* must be called in the main thread.
* Creates a download object for every chapter and adds them to the downloads queue.
*
* @param manga the manga of the chapters to download.
* @param chapters the list of chapters to download.
* @param autoStart whether to start the downloader after enqueing the chapters.
*/
fun queueChapters(manga: Manga, chapters: List<Chapter>) {
val source = sourceManager.get(manga.source) as? HttpSource ?: return
fun queueChapters(manga: Manga, chapters: List<Chapter>, autoStart: Boolean) = launchUI {
val source = sourceManager.get(manga.source) as? HttpSource ?: return@launchUI
val chaptersToQueue = chapters
// Avoid downloading chapters with the same name.
.distinctBy { it.name }
// Add chapters to queue from the start.
.sortedByDescending { it.source_order }
// Create a downloader for each one.
.map { Download(source, manga, it) }
// Filter out those already queued or downloaded.
.filter { isDownloadAllowed(it) }
// Called in background thread, the operation can be slow with SAF.
val chaptersWithoutDir = async {
val mangaDir = provider.findMangaDir(manga, source)
// Return if there's nothing to queue.
if (chaptersToQueue.isEmpty())
return
queue.addAll(chaptersToQueue)
// Initialize queue size.
notifier.initialQueueSize = queue.size
// Initial multi-thread
notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1
if (isRunning) {
// Send the list of downloads to the downloader.
downloadsRelay.call(chaptersToQueue)
} else {
// Show initial notification.
notifier.onProgressChange(queue)
chapters
// Avoid downloading chapters with the same name.
.distinctBy { it.name }
// Filter out those already downloaded.
.filter { mangaDir?.findFile(provider.getChapterDirName(it)) == null }
// Add chapters to queue from the start.
.sortedByDescending { it.source_order }
}
}
/**
* Returns true if the given download can be queued and downloaded.
*
* @param download the download to be checked.
*/
private fun isDownloadAllowed(download: Download): Boolean {
// If the chapter is already queued, don't add it again
if (queue.any { it.chapter.id == download.chapter.id })
return false
// Runs in main thread (synchronization needed).
val chaptersToQueue = chaptersWithoutDir.await()
// Filter out those already enqueued.
.filter { chapter -> queue.none { it.chapter.id == chapter.id } }
// Create a download for each one.
.map { Download(source, manga, it) }
val dir = provider.findChapterDir(download.source, download.manga, download.chapter)
if (dir != null && dir.exists())
return false
if (chaptersToQueue.isNotEmpty()) {
queue.addAll(chaptersToQueue)
return true
// Initialize queue size.
notifier.initialQueueSize = queue.size
// Initial multi-thread
notifier.multipleDownloadThreads = preferences.downloadThreads().getOrDefault() > 1
if (isRunning) {
// Send the list of downloads to the downloader.
downloadsRelay.call(chaptersToQueue)
} else {
// Show initial notification.
notifier.onProgressChange(queue)
}
// Start downloader if needed
if (autoStart) {
DownloadService.start(this@Downloader.context)
}
}
}
/**
@ -274,7 +275,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
*/
private fun downloadChapter(download: Download): Observable<Download> {
val chapterDirname = provider.getChapterDirName(download.chapter)
val mangaDir = provider.getMangaDir(download.source, download.manga)
val mangaDir = provider.getMangaDir(download.manga, download.source)
val tmpDir = mangaDir.createDirectory("${chapterDirname}_tmp")
val pageListObservable = if (download.pages == null) {
@ -292,7 +293,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
}
return pageListObservable
.doOnNext { pages ->
.doOnNext { _ ->
// Delete all temporary (unfinished) files
tmpDir.listFiles()
?.filter { it.name!!.endsWith(".tmp") }
@ -308,9 +309,9 @@ class Downloader(private val context: Context, private val provider: DownloadPro
// Do when page is downloaded.
.doOnNext { notifier.onProgressChange(download, queue) }
.toList()
.map { pages -> download }
.map { _ -> download }
// Do after download completes
.doOnNext { ensureSuccessfulDownload(download, tmpDir, chapterDirname) }
.doOnNext { ensureSuccessfulDownload(download, mangaDir, tmpDir, chapterDirname) }
// If the page list threw, it will resume here
.onErrorReturn { error ->
download.status = Download.ERROR
@ -416,10 +417,13 @@ class Downloader(private val context: Context, private val provider: DownloadPro
* Checks if the download was successful.
*
* @param download the download to check.
* @param mangaDir the manga directory of the download.
* @param tmpDir the directory where the download is currently stored.
* @param dirname the real (non temporary) directory name of the download.
*/
private fun ensureSuccessfulDownload(download: Download, tmpDir: UniFile, dirname: String) {
private fun ensureSuccessfulDownload(download: Download, mangaDir: UniFile,
tmpDir: UniFile, dirname: String) {
// Ensure that the chapter folder has all the images.
val downloadedImages = tmpDir.listFiles().orEmpty().filterNot { it.name!!.endsWith(".tmp") }
@ -432,6 +436,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
// Only rename the directory if it's downloaded.
if (download.status == Download.DOWNLOADED) {
tmpDir.renameTo(dirname)
cache.addChapter(dirname, mangaDir, download.manga)
}
}

View File

@ -66,6 +66,7 @@ class DownloadQueue(
val pageStatusSubject = PublishSubject.create<Int>()
setPagesSubject(download.pages, pageStatusSubject)
return@flatMap pageStatusSubject
.onBackpressureBuffer()
.filter { it == Page.READY }
.map { download }

View File

@ -1,35 +1,51 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher
import java.io.File
import java.io.IOException
import java.io.InputStream
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
private var data: InputStream? = null
override fun loadData(priority: Priority): InputStream {
data = file.inputStream()
return data!!
}
override fun cleanup() {
data?.let { data ->
try {
data.close()
} catch (e: IOException) {
// Ignore
}
}
}
override fun cancel() {
// Do nothing.
}
override fun getId(): String {
return file.toString()
}
package eu.kanade.tachiyomi.data.glide
import android.content.ContentValues.TAG
import android.util.Log
import com.bumptech.glide.Priority
import com.bumptech.glide.load.DataSource
import com.bumptech.glide.load.data.DataFetcher
import java.io.*
open class FileFetcher(private val file: File) : DataFetcher<InputStream> {
private var data: InputStream? = null
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
loadFromFile(callback)
}
protected fun loadFromFile(callback: DataFetcher.DataCallback<in InputStream>) {
try {
data = FileInputStream(file)
} catch (e: FileNotFoundException) {
if (Log.isLoggable(TAG, Log.DEBUG)) {
Log.d(TAG, "Failed to open file", e)
}
callback.onLoadFailed(e)
return
}
callback.onDataReady(data)
}
override fun cleanup() {
try {
data?.close()
} catch (e: IOException) {
// Ignored.
}
}
override fun cancel() {
// Do nothing.
}
override fun getDataClass(): Class<InputStream> {
return InputStream::class.java
}
override fun getDataSource(): DataSource {
return DataSource.LOCAL
}
}

View File

@ -0,0 +1,72 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
/**
* A [DataFetcher] for loading a cover of a library manga.
* It tries to load the cover from our custom cache, and if it's not found, it fallbacks to network
* and copies the result to the cache.
*
* @param networkFetcher the network fetcher for this cover.
* @param manga the manga of the cover to load.
* @param file the file where this cover should be. It may exists or not.
*/
class LibraryMangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
private val manga: Manga,
private val file: File)
: FileFetcher(file) {
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream>) {
if (!file.exists()) {
networkFetcher.loadData(priority, object : DataFetcher.DataCallback<InputStream> {
override fun onDataReady(data: InputStream?) {
if (data != null) {
val tmpFile = File(file.path + ".tmp")
try {
// Retrieve destination stream, create parent folders if needed.
val output = try {
tmpFile.outputStream()
} catch (e: FileNotFoundException) {
tmpFile.parentFile.mkdirs()
tmpFile.outputStream()
}
// Copy the file and rename to the original.
data.use { output.use { data.copyTo(output) } }
tmpFile.renameTo(file)
loadFromFile(callback)
} catch (e: Exception) {
tmpFile.delete()
callback.onLoadFailed(e)
}
} else {
callback.onLoadFailed(Exception("Null data"))
}
}
override fun onLoadFailed(e: Exception) {
callback.onLoadFailed(e)
}
})
} else {
loadFromFile(callback)
}
}
override fun cleanup() {
super.cleanup()
networkFetcher.cleanup()
}
override fun cancel() {
super.cancel()
networkFetcher.cancel()
}
}

View File

@ -1,18 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
open class MangaFileFetcher(private val file: File, private val manga: Manga) : FileFetcher(file) {
/**
* Returns the id for this manga's cover.
*
* Appending the file's modified date to the url, we can force Glide to skip its memory and disk
* lookup step and fetch from our custom cache. This allows us to invalidate Glide's cache when
* the file has changed. If the file doesn't exist it will append a 0.
*/
override fun getId(): String {
return manga.thumbnail_url + file.lastModified()
}
}

View File

@ -1,23 +1,24 @@
package eu.kanade.tachiyomi.data.glide
import android.content.Context
import android.util.LruCache
import com.bumptech.glide.Glide
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
import com.bumptech.glide.load.data.DataFetcher
import com.bumptech.glide.load.Options
import com.bumptech.glide.load.model.*
import com.bumptech.glide.load.model.stream.StreamModelLoader
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.io.InputStream
/**
* A class for loading a cover associated with a [Manga] that can be present in our own cache.
* Coupled with [MangaUrlFetcher], this class allows to implement the following flow:
* Coupled with [LibraryMangaUrlFetcher], this class allows to implement the following flow:
*
* - Check in RAM LRU.
* - Check in disk LRU.
@ -26,7 +27,7 @@ import java.io.InputStream
*
* @param context the application context.
*/
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
class MangaModelLoader : ModelLoader<Manga, InputStream> {
/**
* Cover cache where persistent covers are stored.
@ -39,16 +40,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
private val sourceManager: SourceManager by injectLazy()
/**
* Base network loader.
* Default network client.
*/
private val baseUrlLoader = Glide.buildModelLoader(GlideUrl::class.java,
InputStream::class.java, context)
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<String, Pair<GlideUrl, File>>(100)
private val lruCache = LruCache<GlideUrl, File>(100)
/**
* Map where request headers are stored for a source.
@ -60,12 +60,17 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
*/
class Factory : ModelLoaderFactory<Manga, InputStream> {
override fun build(context: Context, factories: GenericLoaderFactory)
= MangaModelLoader(context)
override fun build(multiFactory: MultiModelLoaderFactory): ModelLoader<Manga, InputStream> {
return MangaModelLoader()
}
override fun teardown() {}
}
override fun handles(model: Manga): Boolean {
return true
}
/**
* Returns a fetcher for the given manga or null if the url is empty.
*
@ -73,10 +78,8 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
* @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.
*/
override fun getResourceFetcher(manga: Manga,
width: Int,
height: Int): DataFetcher<InputStream>? {
override fun buildLoadData(manga: Manga, width: Int, height: Int,
options: Options?): ModelLoader.LoadData<InputStream>? {
// Check thumbnail is not null or empty
val url = manga.thumbnail_url
if (url == null || url.isEmpty()) {
@ -85,26 +88,28 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
if (url.startsWith("http")) {
val source = sourceManager.get(manga.source) as? HttpSource
// Obtain the request url and the file for this url from the LRU cache, or calculate it
// and add them to the cache.
val (glideUrl, file) = lruCache.get(url) ?:
Pair(GlideUrl(url, getHeaders(manga, source)), coverCache.getCoverFile(url)).apply {
lruCache.put(url, this)
}
val glideUrl = GlideUrl(url, getHeaders(manga, source))
// Get the resource fetcher for this request url.
val networkFetcher = source?.let { OkHttpStreamFetcher(it.client, glideUrl) }
?: baseUrlLoader.getResourceFetcher(glideUrl, width, height)
val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
if (!manga.favorite) {
return ModelLoader.LoadData(glideUrl, networkFetcher)
}
// Obtain the file for this url from the LRU cache, or retrieve and add it to the cache.
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 MangaUrlFetcher(networkFetcher, file, manga)
return ModelLoader.LoadData(MangaSignature(manga, file), libraryFetcher)
} 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 MangaFileFetcher(file, manga)
return ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
}
}
@ -127,4 +132,15 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
}
}
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

@ -0,0 +1,27 @@
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,71 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import com.bumptech.glide.Priority
import com.bumptech.glide.load.data.DataFetcher
import eu.kanade.tachiyomi.data.database.models.Manga
import java.io.File
import java.io.FileNotFoundException
import java.io.InputStream
/**
* A [DataFetcher] for loading a cover of a manga depending on its favorite status.
* If the manga is favorite, it tries to load the cover from our cache, and if it's not found, it
* fallbacks to network and copies it to the cache.
* If the manga is not favorite, it tries to delete the cover from our cache and always fallback
* to network for fetching.
*
* @param networkFetcher the network fetcher for this cover.
* @param file the file where this cover should be. It may exists or not.
* @param manga the manga of the cover to load.
*/
class MangaUrlFetcher(private val networkFetcher: DataFetcher<InputStream>,
private val file: File,
private val manga: Manga)
: MangaFileFetcher(file, manga) {
override fun loadData(priority: Priority): InputStream {
if (manga.favorite) {
synchronized(file) {
if (!file.exists()) {
val tmpFile = File(file.path + ".tmp")
try {
// Retrieve source stream.
val input = networkFetcher.loadData(priority)
?: throw Exception("Couldn't open source stream")
// Retrieve destination stream, create parent folders if needed.
val output = try {
tmpFile.outputStream()
} catch (e: FileNotFoundException) {
tmpFile.parentFile.mkdirs()
tmpFile.outputStream()
}
// Copy the file and rename to the original.
input.use { output.use { input.copyTo(output) } }
tmpFile.renameTo(file)
} catch (e: Exception) {
tmpFile.delete()
throw e
}
}
}
return super.loadData(priority)
} else {
if (file.exists()) {
file.delete()
}
return networkFetcher.loadData(priority)
}
}
override fun cancel() {
networkFetcher.cancel()
}
override fun cleanup() {
super.cleanup()
networkFetcher.cleanup()
}
}

View File

@ -1,12 +1,18 @@
package eu.kanade.tachiyomi.data.glide
import android.content.Context
import android.graphics.drawable.Drawable
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.Registry
import com.bumptech.glide.annotation.GlideModule
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.DecodeFormat
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.GlideModule
import com.bumptech.glide.load.resource.drawable.DrawableTransitionOptions
import com.bumptech.glide.module.AppGlideModule
import com.bumptech.glide.request.RequestOptions
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.network.NetworkHelper
import uy.kohesive.injekt.Injekt
@ -16,17 +22,20 @@ import java.io.InputStream
/**
* Class used to update Glide module settings
*/
class AppGlideModule : GlideModule {
@GlideModule
class TachiGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
// Set the cache size of Glide to 15 MiB
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 50 * 1024 * 1024))
builder.setDefaultRequestOptions(RequestOptions().format(DecodeFormat.PREFER_RGB_565))
builder.setDefaultTransitionOptions(Drawable::class.java,
DrawableTransitionOptions.withCrossFade())
}
override fun registerComponents(context: Context, glide: Glide) {
override fun registerComponents(context: Context, glide: Glide, registry: Registry) {
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
glide.register(GlideUrl::class.java, InputStream::class.java, networkFactory)
glide.register(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
registry.replace(GlideUrl::class.java, InputStream::class.java, networkFactory)
registry.append(Manga::class.java, InputStream::class.java, MangaModelLoader.Factory())
}
}

View File

@ -34,7 +34,6 @@ class LibraryUpdateJob : Job() {
.setRequiredNetworkType(wifiRestriction)
.setRequiresCharging(acRestriction)
.setRequirementsEnforced(true)
.setPersisted(true)
.setUpdateCurrent(true)
.build()
.schedule()

View File

@ -10,16 +10,17 @@ import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.LibraryManga
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.DownloadService
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.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.track.TrackManager
@ -80,10 +81,12 @@ class LibraryUpdateService(
/**
* Cached progress notification to avoid creating a lot.
*/
private val progressNotification by lazy { NotificationCompat.Builder(this)
private val progressNotification by lazy { NotificationCompat.Builder(this, Notifications.CHANNEL_LIBRARY)
.setContentTitle(getString(R.string.app_name))
.setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
.setLargeIcon(notificationBitmap)
.setOngoing(true)
.setOnlyAlertOnce(true)
.addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
}
@ -132,7 +135,11 @@ class LibraryUpdateService(
putExtra(KEY_TARGET, target)
category?.let { putExtra(KEY_CATEGORY, it.id) }
}
context.startService(intent)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
}
}
@ -153,6 +160,7 @@ class LibraryUpdateService(
*/
override fun onCreate() {
super.onCreate()
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
wakeLock.acquire()
@ -224,7 +232,7 @@ class LibraryUpdateService(
* @param target the target to update.
* @return a list of manga to update
*/
fun getMangaToUpdate(intent: Intent, target: Target): List<Manga> {
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
var listToUpdate = if (categoryId != -1)
@ -255,7 +263,7 @@ class LibraryUpdateService(
* @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update.
*/
fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> {
fun updateChapterList(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
// List containing new updates
@ -279,7 +287,7 @@ class LibraryUpdateService(
// If there's any error, return empty update and continue.
.onErrorReturn {
failedUpdates.add(manga)
Pair(emptyList<Chapter>(), emptyList<Chapter>())
Pair(emptyList(), emptyList())
}
// Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first.isNotEmpty() }
@ -325,7 +333,9 @@ class LibraryUpdateService(
val dbChapters = chapters.map {
mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
}
downloadManager.downloadChapters(manga, dbChapters)
// 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.
downloadManager.downloadChapters(manga, dbChapters, false)
}
/**
@ -347,7 +357,7 @@ class LibraryUpdateService(
* @param mangaToUpdate the list to update
* @return an observable delivering the progress of each update.
*/
fun updateDetails(mangaToUpdate: List<Manga>): Observable<Manga> {
fun updateDetails(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
@ -358,7 +368,7 @@ class LibraryUpdateService(
// Update the details of the manga.
.concatMap { manga ->
val source = sourceManager.get(manga.source) as? HttpSource
?: return@concatMap Observable.empty<Manga>()
?: return@concatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga)
.map { networkManga ->
@ -377,7 +387,7 @@ class LibraryUpdateService(
* Method that updates the metadata of the connected tracking services. It's called in a
* background thread, so it's safe to do heavy operations or network calls here.
*/
private fun updateTrackings(mangaToUpdate: List<Manga>): Observable<Manga> {
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates.
var count = 0
@ -417,7 +427,7 @@ class LibraryUpdateService(
* @param total the total progress.
*/
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID, progressNotification
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification
.setContentTitle(manga.title)
.setProgress(total, current, false)
.build())
@ -434,7 +444,7 @@ class LibraryUpdateService(
// Append new chapters from a previous, existing notification
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val previousNotification = notificationManager.activeNotifications
.find { it.id == Constants.NOTIFICATION_LIBRARY_RESULT_ID }
.find { it.id == Notifications.ID_LIBRARY_RESULT }
if (previousNotification != null) {
val oldUpdates = previousNotification.notification.extras
@ -446,7 +456,7 @@ class LibraryUpdateService(
}
}
notificationManager.notify(Constants.NOTIFICATION_LIBRARY_RESULT_ID, notification {
notificationManager.notify(Notifications.ID_LIBRARY_RESULT, notification(Notifications.CHANNEL_LIBRARY) {
setSmallIcon(R.drawable.ic_book_white_24dp)
setLargeIcon(notificationBitmap)
setContentTitle(getString(R.string.notification_new_chapters))
@ -466,7 +476,7 @@ class LibraryUpdateService(
* Cancels the progress notification.
*/
private fun cancelProgressNotification() {
notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_PROGRESS_ID)
notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
}
/**

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.notification
import android.app.PendingIntent
import android.content.Context
import android.content.Intent
import android.net.Uri
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.getUriCompat
import java.io.File
@ -43,11 +44,10 @@ object NotificationHandler {
* Returns [PendingIntent] that prompts user with apk install intent
*
* @param context context
* @param file file of apk that is installed
* @param uri uri of apk that is installed
*/
fun installApkPendingActivity(context: Context, file: File): PendingIntent {
fun installApkPendingActivity(context: Context, uri: Uri): PendingIntent {
val intent = Intent(Intent.ACTION_VIEW).apply {
val uri = file.getUriCompat(context)
setDataAndType(uri, "application/vnd.android.package-archive")
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_GRANT_READ_URI_PERMISSION
}

View File

@ -5,7 +5,6 @@ import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.Handler
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Chapter
@ -41,6 +40,8 @@ class NotificationReceiver : BroadcastReceiver() {
ACTION_RESUME_DOWNLOADS -> DownloadService.start(context)
// Clear the download queue
ACTION_CLEAR_DOWNLOADS -> downloadManager.clearQueue(true)
// Show message notification created
ACTION_SHORTCUT_CREATED -> context.toast(R.string.shortcut_created)
// Launch share activity and dismiss notification
ACTION_SHARE_IMAGE -> shareImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
@ -48,7 +49,7 @@ class NotificationReceiver : BroadcastReceiver() {
ACTION_DELETE_IMAGE -> deleteImage(context, intent.getStringExtra(EXTRA_FILE_LOCATION),
intent.getIntExtra(EXTRA_NOTIFICATION_ID, -1))
// Cancel library update and dismiss notification
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Constants.NOTIFICATION_LIBRARY_PROGRESS_ID)
ACTION_CANCEL_LIBRARY_UPDATE -> cancelLibraryUpdate(context, Notifications.ID_LIBRARY_PROGRESS)
// Open reader activity
ACTION_OPEN_CHAPTER -> {
openChapter(context, intent.getLongExtra(EXTRA_MANGA_ID, -1),
@ -161,6 +162,9 @@ class NotificationReceiver : BroadcastReceiver() {
// Called to clear downloads.
private const val ACTION_CLEAR_DOWNLOADS = "$ID.$NAME.ACTION_CLEAR_DOWNLOADS"
// Called to notify user shortcut is created.
private const val ACTION_SHORTCUT_CREATED = "$ID.$NAME.ACTION_SHORTCUT_CREATED"
// Called to dismiss notification.
private const val ACTION_DISMISS_NOTIFICATION = "$ID.$NAME.ACTION_DISMISS_NOTIFICATION"
@ -199,6 +203,13 @@ class NotificationReceiver : BroadcastReceiver() {
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
internal fun shortcutCreatedBroadcast(context: Context) : PendingIntent {
val intent = Intent(context, NotificationReceiver::class.java).apply {
action = ACTION_SHORTCUT_CREATED
}
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/**
* Returns [PendingIntent] that starts a service which dismissed the notification
*

View File

@ -0,0 +1,54 @@
package eu.kanade.tachiyomi.data.notification
import android.app.NotificationChannel
import android.app.NotificationManager
import android.content.Context
import android.os.Build
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.notificationManager
/**
* Class to manage the basic information of all the notifications used in the app.
*/
object Notifications {
/**
* Common notification channel and ids used anywhere.
*/
const val CHANNEL_COMMON = "common_channel"
const val ID_UPDATER = 1
const val ID_DOWNLOAD_IMAGE = 2
/**
* Notification channel and ids used by the library updater.
*/
const val CHANNEL_LIBRARY = "library_channel"
const val ID_LIBRARY_PROGRESS = 101
const val ID_LIBRARY_RESULT = 102
/**
* Notification channel and ids used by the downloader.
*/
const val CHANNEL_DOWNLOADER = "downloader_channel"
const val ID_DOWNLOAD_CHAPTER = 201
const val ID_DOWNLOAD_CHAPTER_ERROR = 202
/**
* Creates the notification channels introduced in Android Oreo.
*
* @param context The application context.
*/
fun createChannels(context: Context) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) return
val channels = listOf(
NotificationChannel(CHANNEL_COMMON, context.getString(R.string.channel_common),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel(CHANNEL_LIBRARY, context.getString(R.string.channel_library),
NotificationManager.IMPORTANCE_LOW),
NotificationChannel(CHANNEL_DOWNLOADER, context.getString(R.string.channel_downloader),
NotificationManager.IMPORTANCE_LOW)
)
context.notificationManager.createNotificationChannels(channels)
}
}

View File

@ -87,6 +87,8 @@ object PreferenceKeys {
const val filterUnread = "pref_filter_unread_key"
const val filterCompleted = "pref_filter_completed_key"
const val librarySortingMode = "library_sorting_mode"
const val automaticUpdates = "automatic_updates"
@ -103,6 +105,8 @@ object PreferenceKeys {
const val defaultCategory = "default_category"
const val downloadBadge = "display_download_badge"
fun sourceUsername(sourceId: Long) = "pref_source_username_$sourceId"
fun sourcePassword(sourceId: Long) = "pref_source_password_$sourceId"

View File

@ -141,10 +141,14 @@ class PreferencesHelper(val context: Context) {
fun libraryAsList() = rxPrefs.getBoolean(Keys.libraryAsList, false)
fun downloadBadge() = rxPrefs.getBoolean(Keys.downloadBadge, false)
fun filterDownloaded() = rxPrefs.getBoolean(Keys.filterDownloaded, false)
fun filterUnread() = rxPrefs.getBoolean(Keys.filterUnread, false)
fun filterCompleted() = rxPrefs.getBoolean(Keys.filterCompleted, false)
fun librarySortingMode() = rxPrefs.getInteger(Keys.librarySortingMode, 0)
fun librarySortingAscending() = rxPrefs.getBoolean("library_sorting_ascending", true)
@ -161,4 +165,6 @@ class PreferencesHelper(val context: Context) {
fun defaultCategory() = prefs.getInt(Keys.defaultCategory, -1)
fun migrateFlags() = rxPrefs.getInteger("migrate_flags", Int.MAX_VALUE)
}

View File

@ -11,8 +11,8 @@ import com.google.gson.annotations.SerializedName
* @param assets assets of latest release.
*/
class GithubRelease(@SerializedName("tag_name") val version: String,
@SerializedName("body") val changeLog: String,
@SerializedName("assets") val assets: List<Assets>) {
@SerializedName("body") val changeLog: String,
@SerializedName("assets") private val assets: List<Assets>) {
/**
* Get download link of latest release from the assets.

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.updater
import eu.kanade.tachiyomi.BuildConfig
import rx.Observable
class GithubUpdateChecker() {
class GithubUpdateChecker {
private val service: GithubService = GithubService.create()

View File

@ -3,5 +3,5 @@ package eu.kanade.tachiyomi.data.updater
sealed class GithubUpdateResult {
class NewUpdate(val release: GithubRelease): GithubUpdateResult()
class NoNewUpdate(): GithubUpdateResult()
class NoNewUpdate : GithubUpdateResult()
}

View File

@ -1,144 +0,0 @@
package eu.kanade.tachiyomi.data.updater
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.Constants
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.util.notificationManager
import java.io.File
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/**
* Local [BroadcastReceiver] that runs on UI thread
* Notification calls from [UpdateDownloaderService] should be made from here.
*/
internal class UpdateDownloaderReceiver(val context: Context) : BroadcastReceiver() {
companion object {
private const val NAME = "UpdateDownloaderReceiver"
// Called to show initial notification.
internal const val NOTIFICATION_UPDATER_INITIAL = "$ID.$NAME.UPDATER_INITIAL"
// Called to show progress notification.
internal const val NOTIFICATION_UPDATER_PROGRESS = "$ID.$NAME.UPDATER_PROGRESS"
// Called to show install notification.
internal const val NOTIFICATION_UPDATER_INSTALL = "$ID.$NAME.UPDATER_INSTALL"
// Called to show error notification
internal const val NOTIFICATION_UPDATER_ERROR = "$ID.$NAME.UPDATER_ERROR"
// Value containing action of BroadcastReceiver
internal const val EXTRA_ACTION = "$ID.$NAME.ACTION"
// Value containing progress
internal const val EXTRA_PROGRESS = "$ID.$NAME.PROGRESS"
// Value containing apk path
internal const val EXTRA_APK_PATH = "$ID.$NAME.APK_PATH"
// Value containing apk url
internal const val EXTRA_APK_URL = "$ID.$NAME.APK_URL"
}
/**
* Notification shown to user
*/
private val notification = NotificationCompat.Builder(context)
override fun onReceive(context: Context, intent: Intent) {
when (intent.getStringExtra(EXTRA_ACTION)) {
NOTIFICATION_UPDATER_INITIAL -> basicNotification()
NOTIFICATION_UPDATER_PROGRESS -> updateProgress(intent.getIntExtra(EXTRA_PROGRESS, 0))
NOTIFICATION_UPDATER_INSTALL -> installNotification(intent.getStringExtra(EXTRA_APK_PATH))
NOTIFICATION_UPDATER_ERROR -> errorNotification(intent.getStringExtra(EXTRA_APK_URL))
}
}
/**
* Called to show basic notification
*/
private fun basicNotification() {
// Create notification
with(notification) {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
setSmallIcon(android.R.drawable.stat_sys_download)
setOngoing(true)
}
notification.show()
}
/**
* Called to show progress notification
*
* @param progress progress of download
*/
private fun updateProgress(progress: Int) {
with(notification) {
setProgress(100, progress, false)
}
notification.show()
}
/**
* Called to show install notification
*
* @param path path of file
*/
private fun installNotification(path: String) {
// Prompt the user to install the new update.
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setProgress(0, 0, false)
// Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, File(path)))
addAction(R.drawable.ic_system_update_grey_24dp_img,
context.getString(R.string.action_install),
NotificationHandler.installApkPendingActivity(context, File(path)))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
}
notification.show()
}
/**
* Called to show error notification
*
* @param url url of apk
*/
private fun errorNotification(url: String) {
// Prompt the user to retry the download.
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
setProgress(0, 0, false)
// Retry action
addAction(R.drawable.ic_refresh_grey_24dp_img,
context.getString(R.string.action_retry),
UpdateDownloaderService.downloadApkPendingService(context, url))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Constants.NOTIFICATION_UPDATER_ID))
}
notification.show()
}
/**
* Shows a notification from this builder.
*
* @param id the id of the notification.
*/
private fun NotificationCompat.Builder.show(id: Int = Constants.NOTIFICATION_UPDATER_ID) {
context.notificationManager.notify(id, build())
}
}

View File

@ -1,68 +1,67 @@
package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Intent
import android.support.v4.app.NotificationCompat
import com.evernote.android.job.Job
import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest
import eu.kanade.tachiyomi.Constants.NOTIFICATION_UPDATER_ID
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.util.notificationManager
class UpdateCheckerJob : Job() {
override fun onRunJob(params: Params): Result {
return GithubUpdateChecker()
.checkForUpdate()
.map { result ->
if (result is GithubUpdateResult.NewUpdate) {
val url = result.release.downloadLink
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
putExtra(UpdateDownloaderService.EXTRA_DOWNLOAD_URL, url)
}
NotificationCompat.Builder(context).update {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done)
// Download action
addAction(android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
}
}
Job.Result.SUCCESS
}
.onErrorReturn { Job.Result.FAILURE }
// Sadly, the task needs to be synchronous.
.toBlocking()
.single()
}
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
context.notificationManager.notify(NOTIFICATION_UPDATER_ID, build())
}
companion object {
const val TAG = "UpdateChecker"
fun setupTask() {
JobRequest.Builder(TAG)
.setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.setPersisted(true)
.setUpdateCurrent(true)
.build()
.schedule()
}
fun cancelTask() {
JobManager.instance().cancelAllForTag(TAG)
}
}
package eu.kanade.tachiyomi.data.updater
import android.app.PendingIntent
import android.content.Intent
import android.support.v4.app.NotificationCompat
import com.evernote.android.job.Job
import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager
class UpdaterJob : Job() {
override fun onRunJob(params: Params): Result {
return GithubUpdateChecker()
.checkForUpdate()
.map { result ->
if (result is GithubUpdateResult.NewUpdate) {
val url = result.release.downloadLink
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(UpdaterService.EXTRA_DOWNLOAD_URL, url)
}
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON).update {
setContentTitle(context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_update_available))
setSmallIcon(android.R.drawable.stat_sys_download_done)
// Download action
addAction(android.R.drawable.stat_sys_download_done,
context.getString(R.string.action_download),
PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT))
}
}
Job.Result.SUCCESS
}
.onErrorReturn { Job.Result.FAILURE }
// Sadly, the task needs to be synchronous.
.toBlocking()
.single()
}
fun NotificationCompat.Builder.update(block: NotificationCompat.Builder.() -> Unit) {
block()
context.notificationManager.notify(Notifications.ID_UPDATER, build())
}
companion object {
const val TAG = "UpdateChecker"
fun setupTask() {
JobRequest.Builder(TAG)
.setPeriodic(24 * 60 * 60 * 1000, 60 * 60 * 1000)
.setRequiredNetworkType(JobRequest.NetworkType.CONNECTED)
.setRequirementsEnforced(true)
.setUpdateCurrent(true)
.build()
.schedule()
}
fun cancelTask() {
JobManager.instance().cancelAllForTag(TAG)
}
}
}

View File

@ -0,0 +1,109 @@
package eu.kanade.tachiyomi.data.updater
import android.content.Context
import android.net.Uri
import android.support.v4.app.NotificationCompat
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.NotificationHandler
import eu.kanade.tachiyomi.data.notification.NotificationReceiver
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.util.notificationManager
/**
* DownloadNotifier is used to show notifications when downloading and update.
*
* @param context context of application.
*/
internal class UpdaterNotifier(private val context: Context) {
/**
* Builder to manage notifications.
*/
private val notification by lazy {
NotificationCompat.Builder(context, Notifications.CHANNEL_COMMON)
}
/**
* Call to show notification.
*
* @param id id of the notification channel.
*/
private fun NotificationCompat.Builder.show(id: Int = Notifications.ID_UPDATER) {
context.notificationManager.notify(id, build())
}
/**
* Call when apk download starts.
*
* @param title tile of notification.
*/
fun onDownloadStarted(title: String) {
with(notification) {
setContentTitle(title)
setContentText(context.getString(R.string.update_check_notification_download_in_progress))
setSmallIcon(android.R.drawable.stat_sys_download)
setOngoing(true)
}
notification.show()
}
/**
* Call when apk download progress changes.
*
* @param progress progress of download (xx%/100).
*/
fun onProgressChange(progress: Int) {
with(notification) {
setProgress(100, progress, false)
setOnlyAlertOnce(true)
}
notification.show()
}
/**
* Call when apk download is finished.
*
* @param uri path location of apk.
*/
fun onDownloadFinished(uri: Uri) {
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Install action
setContentIntent(NotificationHandler.installApkPendingActivity(context, uri))
addAction(R.drawable.ic_system_update_grey_24dp_img,
context.getString(R.string.action_install),
NotificationHandler.installApkPendingActivity(context, uri))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
}
notification.show()
}
/**
* Call when apk download throws a error
*
* @param url web location of apk to download.
*/
fun onDownloadError(url: String) {
with(notification) {
setContentText(context.getString(R.string.update_check_notification_download_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
setOnlyAlertOnce(false)
setProgress(0, 0, false)
// Retry action
addAction(R.drawable.ic_refresh_grey_24dp_img,
context.getString(R.string.action_retry),
UpdaterService.downloadApkPendingService(context, url))
// Cancel action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_cancel),
NotificationReceiver.dismissNotificationPendingBroadcast(context, Notifications.ID_UPDATER))
}
notification.show(Notifications.ID_UPDATER)
}
}

View File

@ -2,52 +2,37 @@ package eu.kanade.tachiyomi.data.updater
import android.app.IntentService
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.network.GET
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.network.ProgressListener
import eu.kanade.tachiyomi.network.newCallWithProgress
import eu.kanade.tachiyomi.util.registerLocalReceiver
import eu.kanade.tachiyomi.util.getUriCompat
import eu.kanade.tachiyomi.util.saveTo
import eu.kanade.tachiyomi.util.sendLocalBroadcastSync
import eu.kanade.tachiyomi.util.unregisterLocalReceiver
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File
class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.java.name) {
class UpdaterService : IntentService(UpdaterService::class.java.name) {
/**
* Network helper
*/
private val network: NetworkHelper by injectLazy()
/**
* Local [BroadcastReceiver] that runs on UI thread
* Notifier for the updater state and progress.
*/
private val updaterNotificationReceiver = UpdateDownloaderReceiver(this)
override fun onCreate() {
super.onCreate()
// Register receiver
registerLocalReceiver(updaterNotificationReceiver, IntentFilter(INTENT_FILTER_NAME))
}
override fun onDestroy() {
// Unregister receiver
unregisterLocalReceiver(updaterNotificationReceiver)
super.onDestroy()
}
private val notifier by lazy { UpdaterNotifier(this) }
override fun onHandleIntent(intent: Intent?) {
if (intent == null) return
val title = intent.getStringExtra(EXTRA_DOWNLOAD_TITLE) ?: getString(R.string.app_name)
val url = intent.getStringExtra(EXTRA_DOWNLOAD_URL) ?: return
downloadApk(url)
downloadApk(title, url)
}
/**
@ -55,9 +40,9 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
*
* @param url url location of file
*/
fun downloadApk(url: String) {
private fun downloadApk(title: String, url: String) {
// Show notification download starting.
sendInitialBroadcast()
notifier.onDownloadStarted(title)
val progressListener = object : ProgressListener {
@ -73,7 +58,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
if (progress > savedProgress && currentTime - 200 > lastTick) {
savedProgress = progress
lastTick = currentTime
sendProgressBroadcast(progress)
notifier.onProgressChange(progress)
}
}
}
@ -91,80 +76,32 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
response.close()
throw Exception("Unsuccessful response")
}
sendInstallBroadcast(apkFile.absolutePath)
notifier.onDownloadFinished(apkFile.getUriCompat(this))
} catch (error: Exception) {
Timber.e(error)
sendErrorBroadcast(url)
notifier.onDownloadError(url)
}
}
/**
* Show notification download starting.
*/
private fun sendInitialBroadcast() {
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INITIAL)
}
sendLocalBroadcastSync(intent)
}
/**
* Show notification progress changed
*
* @param progress progress of download
*/
private fun sendProgressBroadcast(progress: Int) {
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_PROGRESS)
putExtra(UpdateDownloaderReceiver.EXTRA_PROGRESS, progress)
}
sendLocalBroadcastSync(intent)
}
/**
* Show install notification.
*
* @param path location of file
*/
private fun sendInstallBroadcast(path: String){
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_INSTALL)
putExtra(UpdateDownloaderReceiver.EXTRA_APK_PATH, path)
}
sendLocalBroadcastSync(intent)
}
/**
* Show error notification.
*
* @param url url of file
*/
private fun sendErrorBroadcast(url: String){
val intent = Intent(INTENT_FILTER_NAME).apply {
putExtra(UpdateDownloaderReceiver.EXTRA_ACTION, UpdateDownloaderReceiver.NOTIFICATION_UPDATER_ERROR)
putExtra(UpdateDownloaderReceiver.EXTRA_APK_URL, url)
}
sendLocalBroadcastSync(intent)
}
companion object {
/**
* Name of Local BroadCastReceiver.
*/
private val INTENT_FILTER_NAME = UpdateDownloaderService::class.java.name
/**
* Download url.
*/
internal const val EXTRA_DOWNLOAD_URL = "${BuildConfig.APPLICATION_ID}.UpdateDownloaderService.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"
/**
* Downloads a new update and let the user install the new version from a notification.
* @param context the application context.
* @param url the url to the new update.
*/
fun downloadUpdate(context: Context, url: String) {
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
fun downloadUpdate(context: Context, url: String, title: String = context.getString(R.string.app_name)) {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_TITLE, title)
putExtra(EXTRA_DOWNLOAD_URL, url)
}
context.startService(intent)
@ -177,7 +114,7 @@ class UpdateDownloaderService : IntentService(UpdateDownloaderService::class.jav
* @return [PendingIntent]
*/
internal fun downloadApkPendingService(context: Context, url: String): PendingIntent {
val intent = Intent(context, UpdateDownloaderService::class.java).apply {
val intent = Intent(context, UpdaterService::class.java).apply {
putExtra(EXTRA_DOWNLOAD_URL, url)
}
return PendingIntent.getService(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)

View File

@ -14,12 +14,14 @@ class CloudflareInterceptor : Interceptor {
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
private val serverCheck = arrayOf("cloudflare-nginx", "cloudflare")
@Synchronized
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
// Check if Cloudflare anti-bot is on
if (response.code() == 503 && "cloudflare-nginx" == response.header("Server")) {
if (response.code() == 503 && serverCheck.contains(response.header("Server"))) {
return chain.proceed(resolveChallenge(response))
}

View File

@ -18,16 +18,6 @@ class NetworkHelper(context: Context) {
.cache(Cache(cacheDir, cacheSize))
.build()
val forceCacheClient = client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=600")
.build()
}
.build()
val cloudflareClient = client.newBuilder()
.addInterceptor(CloudflareInterceptor())
.build()

View File

@ -17,7 +17,7 @@ class PersistentCookieStore(context: Context) {
val cookies = value as? Set<String>
if (cookies != null) {
try {
val url = HttpUrl.parse("http://$key")
val url = HttpUrl.parse("http://$key") ?: continue
val nonExpiredCookies = cookies.mapNotNull { Cookie.parse(url, it) }
.filter { !it.hasExpired() }
cookieMap.put(key, nonExpiredCookies)

View File

@ -56,7 +56,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
}
override val id = ID
override val name = "LocalSource"
override val name = context.getString(R.string.local_source)
override val lang = ""
override val supportsLatest = true
@ -76,13 +76,13 @@ class LocalSource(private val context: Context) : CatalogueSource {
val state = ((if (filters.isEmpty()) POPULAR_FILTERS else filters)[0] as OrderBy).state
when (state?.index) {
0 -> {
if (state!!.ascending)
if (state.ascending)
mangaDirs = mangaDirs.sortedBy { it.name.toLowerCase(Locale.ENGLISH) }
else
mangaDirs = mangaDirs.sortedByDescending { it.name.toLowerCase(Locale.ENGLISH) }
}
1 -> {
if (state!!.ascending)
if (state.ascending)
mangaDirs = mangaDirs.sortedBy(File::lastModified)
else
mangaDirs = mangaDirs.sortedByDescending(File::lastModified)
@ -144,7 +144,7 @@ class LocalSource(private val context: Context) : CatalogueSource {
} else {
chapterFile.nameWithoutExtension
}
val chapNameCut = chapName.replace(manga.title, "", true).trim()
val chapNameCut = chapName.replace(manga.title, "", true).trim(' ', '-', '_')
name = if (chapNameCut.isEmpty()) chapName else chapNameCut
date_upload = chapterFile.lastModified()
ChapterRecognition.parseChapterNumber(this, manga)

View File

@ -28,7 +28,11 @@ class Page(
@Transient private var statusSubject: Subject<Int, Int>? = null
override fun update(bytesRead: Long, contentLength: Long, done: Boolean) {
progress = (100 * bytesRead / contentLength).toInt()
progress = if (contentLength > 0) {
(100 * bytesRead / contentLength).toInt()
} else {
-1
}
}
fun setStatusSubject(subject: Subject<Int, Int>?) {

View File

@ -12,11 +12,14 @@ interface SChapter : Serializable {
var chapter_number: Float
var scanlator: String?
fun copyFrom(other: SChapter) {
name = other.name
url = other.url
date_upload = other.date_upload
chapter_number = other.chapter_number
scanlator = other.scanlator
}
companion object {

View File

@ -10,4 +10,6 @@ class SChapterImpl : SChapter {
override var chapter_number: Float = -1f
override var scanlator: String? = null
}

View File

@ -13,6 +13,7 @@ import okhttp3.Request
import okhttp3.Response
import rx.Observable
import uy.kohesive.injekt.injectLazy
import java.lang.Exception
import java.net.URI
import java.net.URISyntaxException
import java.security.MessageDigest
@ -51,7 +52,7 @@ abstract class HttpSource : CatalogueSource {
override val id by lazy {
val key = "${name.toLowerCase()}/$lang/$versionId"
val bytes = MessageDigest.getInstance("MD5").digest(key.toByteArray())
(0..7).map { bytes[it].toLong() and 0xff shl 8*(7-it) }.reduce(Long::or) and Long.MAX_VALUE
(0..7).map { bytes[it].toLong() and 0xff shl 8 * (7 - it) }.reduce(Long::or) and Long.MAX_VALUE
}
/**
@ -197,16 +198,20 @@ abstract class HttpSource : CatalogueSource {
/**
* Returns an observable with the updated chapter list for a manga. Normally it's not needed to
* override this method.
* override this method. If a manga is licensed an empty chapter list observable is returned
*
* @param manga the manga to look for chapters.
*/
override fun fetchChapterList(manga: SManga): Observable<List<SChapter>> {
return client.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response ->
chapterListParse(response)
}
if (manga.status != SManga.LICENSED) {
return client.newCall(chapterListRequest(manga))
.asObservableSuccess()
.map { response ->
chapterListParse(response)
}
} else {
return Observable.error(Exception("Licensed - No chapters to show"))
}
}
/**

View File

@ -11,6 +11,7 @@ import eu.kanade.tachiyomi.util.asJsoup
import eu.kanade.tachiyomi.util.selectText
import okhttp3.FormBody
import okhttp3.HttpUrl
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
@ -48,6 +49,8 @@ class Batoto : ParsedHttpSource(), LoginSource {
private val staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE)
override val client: OkHttpClient = network.cloudflareClient
override fun headersBuilder() = super.headersBuilder()
.add("Cookie", "lang_option=English")
@ -69,7 +72,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
element.select("a[href^=$baseUrl]").first().let {
element.select("a[href*=bato.to]").first().let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text().trim()
}
@ -189,7 +192,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
override fun chapterListSelector() = "tr.row.lang_English.chapter_row"
override fun chapterFromElement(element: Element): SChapter {
val urlElement = element.select("a[href^=$baseUrl/reader").first()
val urlElement = element.select("a[href*=bato.to/reader").first()
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href"))
@ -197,6 +200,7 @@ class Batoto : ParsedHttpSource(), LoginSource {
chapter.date_upload = element.select("td").getOrNull(4)?.let {
parseDateFromElement(it)
} ?: 0
chapter.scanlator = element.select("td").getOrNull(2)?.text()
return chapter
}

View File

@ -11,6 +11,7 @@ import okhttp3.Request
import okhttp3.Response
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import timber.log.Timber
import java.text.SimpleDateFormat
import java.util.regex.Pattern
@ -44,7 +45,33 @@ class Kissmanga : ParsedHttpSource() {
val manga = SManga.create()
element.select("td a:eq(0)").first().let {
manga.setUrlWithoutDomain(it.attr("href"))
manga.title = it.text()
val title = it.text()
//check if cloudfire email obfuscation is affecting title name
if (title.contains("[email protected]", true)) {
try {
var str: String = it.html()
//get the number
str = str.substringAfter("data-cfemail=\"")
str = str.substringBefore("\">[email")
val sb = StringBuilder()
//convert number to char
val r = Integer.valueOf(str.substring(0, 2), 16)!!
var i = 2
while (i < str.length) {
val c = (Integer.valueOf(str.substring(i, i + 2), 16) xor r).toChar()
sb.append(c)
i += 2
}
//replace the new word into the title
manga.title = title.replace("[email protected]", sb.toString(), true)
} catch (e: Exception) {
//on error just default to obfuscated title
Timber.e("error parsing [email protected]", e)
manga.title = title
}
} else {
manga.title = title
}
}
return manga
}
@ -199,6 +226,7 @@ class Kissmanga : ParsedHttpSource() {
Genre("Mystery"),
Genre("One shot"),
Genre("Psychological"),
Genre("Reincarnation"),
Genre("Romance"),
Genre("School Life"),
Genre("Sci-fi"),
@ -212,7 +240,9 @@ class Kissmanga : ParsedHttpSource() {
Genre("Smut"),
Genre("Sports"),
Genre("Supernatural"),
Genre("Time Travel"),
Genre("Tragedy"),
Genre("Transported"),
Genre("Webtoon"),
Genre("Yaoi"),
Genre("Yuri")

View File

@ -17,7 +17,7 @@ class Mangafox : ParsedHttpSource() {
override val name = "Mangafox"
override val baseUrl = "http://mangafox.me"
override val baseUrl = "http://mangafox.la"
override val lang = "en"
@ -61,7 +61,7 @@ class Mangafox : ParsedHttpSource() {
is Status -> url.addQueryParameter(filter.id, filter.state.toString())
is GenreList -> filter.state.forEach { genre -> url.addQueryParameter(genre.id, genre.state.toString()) }
is TextField -> url.addQueryParameter(filter.key, filter.state)
is Type -> url.addQueryParameter("type", if(filter.state == 0) "" else filter.state.toString())
is Type -> url.addQueryParameter("type", if (filter.state == 0) "" else filter.state.toString())
is OrderBy -> {
url.addQueryParameter("sort", arrayOf("name", "rating", "views", "total_chapters", "last_chapter_time")[filter.state!!.index])
url.addQueryParameter("order", if (filter.state?.ascending == true) "az" else "za")
@ -89,13 +89,20 @@ class Mangafox : ParsedHttpSource() {
val infoElement = document.select("div#title").first()
val rowElement = infoElement.select("table > tbody > tr:eq(1)").first()
val sideInfoElement = document.select("#series_info").first()
val licensedElement = document.select("div.warning").first()
val manga = SManga.create()
manga.author = rowElement.select("td:eq(1)").first()?.text()
manga.artist = rowElement.select("td:eq(2)").first()?.text()
manga.genre = rowElement.select("td:eq(3)").first()?.text()
manga.description = infoElement.select("p.summary").first()?.text()
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
val isLicensed = licensedElement?.text()?.contains("licensed")
if (isLicensed == true) {
manga.status = SManga.LICENSED
} else {
manga.status = sideInfoElement.select(".data").first()?.text().orEmpty().let { parseStatus(it) }
}
manga.thumbnail_url = sideInfoElement.select("div.cover > img").first()?.attr("src")
return manga
}
@ -113,7 +120,7 @@ class Mangafox : ParsedHttpSource() {
val chapter = SChapter.create()
chapter.setUrlWithoutDomain(urlElement.attr("href"))
chapter.name = urlElement.text()
chapter.name = element.select("span.title.nowrap").first()?.text()?.let { urlElement.text() + " - " + it } ?: urlElement.text()
chapter.date_upload = element.select("span.date").first()?.text()?.let { parseChapterDate(it) } ?: 0
return chapter
}
@ -169,6 +176,7 @@ class Mangafox : ParsedHttpSource() {
private class OrderBy : Filter.Sort("Order by",
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
Filter.Sort.Selection(2, false))
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
override fun getFilterList() = FilterList(

View File

@ -7,9 +7,13 @@ import okhttp3.HttpUrl
import okhttp3.Request
import org.jsoup.nodes.Document
import org.jsoup.nodes.Element
import java.security.SecureRandom
import java.security.cert.X509Certificate
import java.text.ParseException
import java.text.SimpleDateFormat
import java.util.*
import javax.net.ssl.SSLContext
import javax.net.ssl.X509TrustManager
class Mangahere : ParsedHttpSource() {
@ -23,6 +27,26 @@ class Mangahere : ParsedHttpSource() {
override val supportsLatest = true
private val trustManager = object : X509TrustManager {
override fun getAcceptedIssuers(): Array<X509Certificate> {
return emptyArray()
}
override fun checkClientTrusted(chain: Array<X509Certificate>, authType: String) {
}
override fun checkServerTrusted(chain: Array<X509Certificate>, authType: String) {
}
}
private val sslContext = SSLContext.getInstance("SSL").apply {
init(null, arrayOf(trustManager), SecureRandom())
}
override val client = super.client.newBuilder()
.sslSocketFactory(sslContext.socketFactory, trustManager)
.build()
override fun popularMangaSelector() = "div.directory_list > ul > li"
override fun latestUpdatesSelector() = "div.directory_list > ul > li"
@ -87,8 +111,8 @@ class Mangahere : ParsedHttpSource() {
val infoElement = detailElement.select(".detail_topText").first()
val manga = SManga.create()
manga.author = infoElement.select("a[href^=http://www.mangahere.co/author/]").first()?.text()
manga.artist = infoElement.select("a[href^=http://www.mangahere.co/artist/]").first()?.text()
manga.author = infoElement.select("a[href^=//www.mangahere.co/author/]").first()?.text()
manga.artist = infoElement.select("a[href^=//www.mangahere.co/artist/]").first()?.text()
manga.genre = infoElement.select("li:eq(3)").first()?.text()?.substringAfter("Genre(s):")
manga.description = infoElement.select("#show").first()?.text()?.substringBeforeLast("Show less")
manga.status = infoElement.select("li:eq(6)").first()?.text().orEmpty().let { parseStatus(it) }
@ -159,7 +183,9 @@ class Mangahere : ParsedHttpSource() {
val pages = mutableListOf<Page>()
document.select("select.wid60").first()?.getElementsByTag("option")?.forEach {
pages.add(Page(pages.size, it.attr("value")))
if (!it.attr("value").contains("featured.html")) {
pages.add(Page(pages.size, "http:" + it.attr("value")))
}
}
pages.getOrNull(0)?.imageUrl = imageUrlParse(document)
return pages
@ -174,6 +200,7 @@ class Mangahere : ParsedHttpSource() {
private class OrderBy : Filter.Sort("Order by",
arrayOf("Series name", "Rating", "Views", "Total chapters", "Last chapter"),
Filter.Sort.Selection(2, false))
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Genres", genres)
override fun getFilterList() = FilterList(

View File

@ -17,7 +17,7 @@ class Readmangatoday : ParsedHttpSource() {
override val name = "ReadMangaToday"
override val baseUrl = "http://www.readmanga.today"
override val baseUrl = "http://www.readmng.com/"
override val lang = "en"
@ -161,7 +161,7 @@ class Readmangatoday : ParsedHttpSource() {
return pages
}
override fun imageUrlParse(document: Document) = document.select("img.img-responsive-2").first().attr("src")
override fun imageUrlParse(document: Document) = document.select("#chapter_img").first().attr("src")
private class Status : Filter.TriState("Completed")
private class Genre(name: String, val id: Int) : Filter.TriState(name)

View File

@ -23,9 +23,8 @@ class Mangachan : ParsedHttpSource() {
override val supportsLatest = true
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
}
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/mostfavorites?offset=${20 * (page - 1)}", headers)
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
var pageNum = 1
@ -36,21 +35,65 @@ class Mangachan : ParsedHttpSource() {
val url = if (query.isNotEmpty()) {
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum"
} else {
val filt = filters.filterIsInstance<Genre>().filter { !it.isIgnored() }
if (filt.isNotEmpty()) {
var genres = ""
filt.forEach { genres += (if (it.isExcluded()) "-" else "") + it.id + '+' }
"$baseUrl/tags/${genres.dropLast(1)}?offset=${20 * (pageNum - 1)}"
var genres = ""
var order = ""
var statusParam = true
var status = ""
for (filter in if (filters.isEmpty()) getFilterList() else filters) {
when (filter) {
is GenreList -> {
filter.state.forEach { f ->
if (!f.isIgnored()) {
genres += (if (f.isExcluded()) "-" else "") + f.id + '+'
}
}
}
is OrderBy -> { if (filter.state!!.ascending && filter.state!!.index == 0) { statusParam = false } }
is Status -> status = arrayOf("", "all_done", "end", "ongoing", "new_ch")[filter.state]
}
}
if (genres.isNotEmpty()) {
for (filter in filters) {
when (filter) {
is OrderBy -> {
order = if (filter.state!!.ascending) {
arrayOf("", "&n=favasc", "&n=abcdesc", "&n=chasc")[filter.state!!.index]
} else {
arrayOf("&n=dateasc", "&n=favdesc", "&n=abcasc", "&n=chdesc")[filter.state!!.index]
}
}
}
}
if (statusParam) {
"$baseUrl/tags/${genres.dropLast(1)}$order?offset=${20 * (pageNum - 1)}&status=$status"
} else {
"$baseUrl/tags/$status/${genres.dropLast(1)}/$order?offset=${20 * (pageNum - 1)}"
}
} else {
"$baseUrl/?do=search&subaction=search&story=$query&search_start=$pageNum"
for (filter in filters) {
when (filter) {
is OrderBy -> {
order = if (filter.state!!.ascending) {
arrayOf("manga/new", "manga/new&n=favasc", "manga/new&n=abcdesc", "manga/new&n=chasc")[filter.state!!.index]
} else {
arrayOf("manga/new&n=dateasc", "mostfavorites", "catalog", "sortch")[filter.state!!.index]
}
}
}
}
if (statusParam) {
"$baseUrl/$order?offset=${20 * (pageNum - 1)}&status=$status"
} else {
"$baseUrl/$order/$status?offset=${20 * (pageNum - 1)}"
}
}
}
return GET(url, headers)
}
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/newestch?page=$page")
}
override fun latestUpdatesRequest(page: Int): Request = GET("$baseUrl/newestch?page=$page")
override fun popularMangaSelector() = "div.content_row"
@ -76,9 +119,7 @@ class Mangachan : ParsedHttpSource() {
return manga
}
override fun searchMangaFromElement(element: Element): SManga {
return popularMangaFromElement(element)
}
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
override fun popularMangaNextPageSelector() = "a:contains(Вперед)"
@ -125,16 +166,14 @@ class Mangachan : ParsedHttpSource() {
manga.genre = infoElement.select("tr:eq(5) > td:eq(1)").text()
manga.status = parseStatus(infoElement.select("tr:eq(4) > td:eq(1)").text())
manga.description = descElement.textNodes().first().text()
manga.thumbnail_url = baseUrl + imgElement.attr("src")
manga.thumbnail_url = imgElement.attr("src")
return manga
}
private fun parseStatus(element: String): Int {
when {
element.contains("перевод завершен") -> return SManga.COMPLETED
element.contains("перевод продолжается") -> return SManga.ONGOING
else -> return SManga.UNKNOWN
}
private fun parseStatus(element: String): Int = when {
element.contains("перевод завершен") -> SManga.COMPLETED
element.contains("перевод продолжается") -> SManga.ONGOING
else -> SManga.UNKNOWN
}
override fun chapterListSelector() = "table.table_cha tr:gt(1)"
@ -167,18 +206,39 @@ class Mangachan : ParsedHttpSource() {
override fun imageUrlParse(document: Document) = ""
private class GenreList(genres: List<Genre>) : Filter.Group<Genre>("Тэги", genres)
private class Genre(name: String, val id: String = name.replace(' ', '_')) : Filter.TriState(name)
private class Status : Filter.Select<String>("Статус", arrayOf("Все", "Перевод завершен", "Выпуск завершен", "Онгоинг", "Новые главы"))
private class OrderBy : Filter.Sort("Сортировка",
arrayOf("Дата", "Популярность", "Имя", "Главы"),
Filter.Sort.Selection(1, false))
override fun getFilterList() = FilterList(
Status(),
OrderBy(),
GenreList(getGenreList())
)
// private class StatusList(status: List<Status>) : Filter.Group<Status>("Статус", status)
// private class Status(name: String, val id: String) : Filter.CheckBox(name, false)
// private fun getStatusList() = listOf(
// Status("Перевод завершен", "/all_done"),
// Status("Выпуск завершен", "/end"),
// Status("Онгоинг", "/ongoing"),
// Status("Новые главы", "/new_ch")
// )
/* [...document.querySelectorAll("li.sidetag > a:nth-child(1)")].map((el,i) =>
* { const link=el.getAttribute('href');const id=link.substr(6,link.length);
* return `Genre("${id.replace("_", " ")}")` }).join(',\n')
* on http://mangachan.me/
*/
override fun getFilterList() = FilterList(
private fun getGenreList() = listOf(
Genre("18 плюс"),
Genre("bdsm"),
Genre("арт"),
Genre("биография"),
Genre("боевик"),
Genre("боевые искусства"),
Genre("вампиры"),
@ -198,7 +258,6 @@ class Mangachan : ParsedHttpSource() {
Genre("кодомо"),
Genre("комедия"),
Genre("литРПГ"),
Genre("магия"),
Genre("махо-сёдзё"),
Genre("меха"),
Genre("мистика"),
@ -220,6 +279,7 @@ class Mangachan : ParsedHttpSource() {
Genre("сёдзё-ай"),
Genre("сёнэн"),
Genre("сёнэн-ай"),
Genre("темное фэнтези"),
Genre("тентакли"),
Genre("трагедия"),
Genre("триллер"),
@ -233,4 +293,4 @@ class Mangachan : ParsedHttpSource() {
Genre("яой"),
Genre("ёнкома")
)
}
}

View File

@ -23,13 +23,11 @@ class Mintmanga : ParsedHttpSource() {
override val supportsLatest = true
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
}
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
}
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
override fun popularMangaSelector() = "div.desc"
@ -44,24 +42,21 @@ class Mintmanga : ParsedHttpSource() {
return manga
}
override fun latestUpdatesFromElement(element: Element): SManga {
return popularMangaFromElement(element)
}
override fun latestUpdatesFromElement(element: Element): SManga =
popularMangaFromElement(element)
override fun popularMangaNextPageSelector() = "a.nextLink"
override fun latestUpdatesNextPageSelector() = "a.nextLink"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
return GET("$baseUrl/search?q=$query&$genres", headers)
val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] }
return GET("$baseUrl/search/advanced?q=$query&$genres", headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga {
return popularMangaFromElement(element)
}
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
// max 200 results
override fun searchMangaNextPageSelector() = null
@ -78,13 +73,11 @@ class Mintmanga : ParsedHttpSource() {
return manga
}
private fun parseStatus(element: String): Int {
when {
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
else -> return SManga.UNKNOWN
}
private fun parseStatus(element: String): Int = when {
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
else -> SManga.UNKNOWN
}
override fun chapterListSelector() = "div.chapters-link tbody tr"
@ -125,7 +118,7 @@ class Mintmanga : ParsedHttpSource() {
val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex)
val p = Pattern.compile("'.+?','.+?',\".+?\"")
val p = Pattern.compile("'.*?','.*?',\".*?\"")
val m = p.matcher(trimmedHtml)
val pages = mutableListOf<Page>()
@ -149,7 +142,7 @@ class Mintmanga : ParsedHttpSource() {
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
* on http://mintmanga.com/search
* on http://mintmanga.com/search/advanced
*/
override fun getFilterList() = FilterList(
Genre("арт", "el_2220"),
@ -171,6 +164,7 @@ class Mintmanga : ParsedHttpSource() {
Genre("меха", "el_1318"),
Genre("мистика", "el_1324"),
Genre("научная фантастика", "el_1325"),
Genre("омегаверс", "el_5676"),
Genre("повседневность", "el_1327"),
Genre("постапокалиптика", "el_1342"),
Genre("приключения", "el_1322"),

View File

@ -27,13 +27,11 @@ class Readmanga : ParsedHttpSource() {
override fun latestUpdatesSelector() = "div.desc"
override fun popularMangaRequest(page: Int): Request {
return GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
}
override fun popularMangaRequest(page: Int): Request =
GET("$baseUrl/list?sortType=rate&offset=${70 * (page - 1)}&max=70", headers)
override fun latestUpdatesRequest(page: Int): Request {
return GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
}
override fun latestUpdatesRequest(page: Int): Request =
GET("$baseUrl/list?sortType=updated&offset=${70 * (page - 1)}&max=70", headers)
override fun popularMangaFromElement(element: Element): SManga {
val manga = SManga.create()
@ -44,24 +42,21 @@ class Readmanga : ParsedHttpSource() {
return manga
}
override fun latestUpdatesFromElement(element: Element): SManga {
return popularMangaFromElement(element)
}
override fun latestUpdatesFromElement(element: Element): SManga =
popularMangaFromElement(element)
override fun popularMangaNextPageSelector() = "a.nextLink"
override fun latestUpdatesNextPageSelector() = "a.nextLink"
override fun searchMangaRequest(page: Int, query: String, filters: FilterList): Request {
val genres = filters.filterIsInstance<Genre>().map { it.id + arrayOf("=", "=in", "=ex")[it.state] }.joinToString("&")
return GET("$baseUrl/search?q=$query&$genres", headers)
val genres = filters.filterIsInstance<Genre>().joinToString("&") { it.id + arrayOf("=", "=in", "=ex")[it.state] }
return GET("$baseUrl/search/advanced?q=$query&$genres", headers)
}
override fun searchMangaSelector() = popularMangaSelector()
override fun searchMangaFromElement(element: Element): SManga {
return popularMangaFromElement(element)
}
override fun searchMangaFromElement(element: Element): SManga = popularMangaFromElement(element)
// max 200 results
override fun searchMangaNextPageSelector() = null
@ -78,13 +73,11 @@ class Readmanga : ParsedHttpSource() {
return manga
}
private fun parseStatus(element: String): Int {
when {
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> return SManga.LICENSED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> return SManga.COMPLETED
element.contains("<b>Перевод:</b> продолжается") -> return SManga.ONGOING
else -> return SManga.UNKNOWN
}
private fun parseStatus(element: String): Int = when {
element.contains("<h3>Запрещена публикация произведения по копирайту</h3>") -> SManga.LICENSED
element.contains("<h1 class=\"names\"> Сингл") || element.contains("<b>Перевод:</b> завершен") -> SManga.COMPLETED
element.contains("<b>Перевод:</b> продолжается") -> SManga.ONGOING
else -> SManga.UNKNOWN
}
override fun chapterListSelector() = "div.chapters-link tbody tr"
@ -125,7 +118,7 @@ class Readmanga : ParsedHttpSource() {
val endIndex = html.indexOf("], 0, false);", beginIndex)
val trimmedHtml = html.substring(beginIndex, endIndex)
val p = Pattern.compile("'.+?','.+?',\".+?\"")
val p = Pattern.compile("'.*?','.*?',\".*?\"")
val m = p.matcher(trimmedHtml)
val pages = mutableListOf<Page>()
@ -149,7 +142,7 @@ class Readmanga : ParsedHttpSource() {
/* [...document.querySelectorAll("tr.advanced_option:nth-child(1) > td:nth-child(3) span.js-link")].map((el,i) => {
* const onClick=el.getAttribute('onclick');const id=onClick.substr(31,onClick.length-33);
* return `Genre("${el.textContent.trim()}", "${id}")` }).join(',\n')
* on http://readmanga.me/search
* on http://readmanga.me/search/advanced
*/
override fun getFilterList() = FilterList(
Genre("арт", "el_5685"),

View File

@ -3,23 +3,42 @@ package eu.kanade.tachiyomi.ui.base.controller
import android.os.Bundle
import android.support.v7.app.AppCompatActivity
import android.view.LayoutInflater
import android.view.MenuItem
import android.view.View
import android.view.ViewGroup
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.ControllerChangeHandler
import com.bluelinelabs.conductor.ControllerChangeType
import com.bluelinelabs.conductor.RestoreViewOnCreateController
import kotlinx.android.extensions.LayoutContainer
import kotlinx.android.synthetic.*
abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle) {
abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateController(bundle),
LayoutContainer {
init {
addLifecycleListener(object : LifecycleListener() {
override fun postCreateView(controller: Controller, view: View) {
onViewCreated(view)
}
})
}
override val containerView: View?
get() = view
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup, savedViewState: Bundle?): View {
val view = inflateView(inflater, container)
onViewCreated(view, savedViewState)
return view
return inflateView(inflater, container)
}
override fun onDestroyView(view: View) {
super.onDestroyView(view)
clearFindViewByIdCache()
}
abstract fun inflateView(inflater: LayoutInflater, container: ViewGroup): View
open fun onViewCreated(view: View, savedViewState: Bundle?) { }
open fun onViewCreated(view: View) { }
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
if (type.isEnter) {
@ -32,7 +51,7 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
return null
}
private fun setTitle() {
fun setTitle() {
var parentController = parentController
while (parentController != null) {
if (parentController is BaseController && parentController.getTitle() != null) {
@ -44,4 +63,22 @@ abstract class BaseController(bundle: Bundle? = null) : RestoreViewOnCreateContr
(activity as? AppCompatActivity)?.supportActionBar?.title = getTitle()
}
/**
* Workaround for disappearing menu items when collapsing an expandable item like a SearchView.
* This method should be removed when fixed upstream.
* Issue link: https://issuetracker.google.com/issues/37657375
*/
fun MenuItem.fixExpand() {
setOnActionExpandListener(object : MenuItem.OnActionExpandListener {
override fun onMenuItemActionExpand(item: MenuItem): Boolean {
return true
}
override fun onMenuItemActionCollapse(item: MenuItem): Boolean {
activity?.invalidateOptionsMenu()
return true
}
})
}
}

View File

@ -1,6 +1,12 @@
package eu.kanade.tachiyomi.ui.base.controller
import android.content.pm.PackageManager.PERMISSION_GRANTED
import android.os.Build
import android.support.v4.content.ContextCompat
import com.bluelinelabs.conductor.Controller
import com.bluelinelabs.conductor.Router
import com.bluelinelabs.conductor.RouterTransaction
import com.bluelinelabs.conductor.changehandler.FadeChangeHandler
fun Router.popControllerWithTag(tag: String): Boolean {
val controller = getControllerWithTag(tag)
@ -9,4 +15,21 @@ fun Router.popControllerWithTag(tag: String): Boolean {
return true
}
return false
}
}
fun Controller.requestPermissionsSafe(permissions: Array<String>, requestCode: Int) {
val activity = activity ?: return
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
permissions.forEach { permission ->
if (ContextCompat.checkSelfPermission(activity, permission) != PERMISSION_GRANTED) {
requestPermissions(arrayOf(permission), requestCode)
}
}
}
}
fun Controller.withFadeTransaction(): RouterTransaction {
return RouterTransaction.with(this)
.pushChangeHandler(FadeChangeHandler())
.popChangeHandler(FadeChangeHandler())
}

View File

@ -7,7 +7,7 @@ import nucleus.factory.PresenterFactory
import nucleus.presenter.Presenter
@Suppress("LeakingThis")
abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(),
abstract class NucleusController<P : Presenter<*>>(val bundle: Bundle? = null) : RxController(bundle),
PresenterFactory<P> {
private val delegate = NucleusConductorDelegate(this)

View File

@ -30,7 +30,7 @@ abstract class RxController(bundle: Bundle? = null) : BaseController(bundle) {
}
@CallSuper
override fun onViewCreated(view: View, savedViewState: Bundle?) {
override fun onViewCreated(view: View) {
if (untilDestroySubscriptions.isUnsubscribed) {
untilDestroySubscriptions = CompositeSubscription()
}

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.view.View
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.viewholders.FlexibleViewHolder
import kotlinx.android.extensions.LayoutContainer
abstract class BaseFlexibleViewHolder(view: View,
adapter: FlexibleAdapter<*>,
stickyHeader: Boolean = false) :
FlexibleViewHolder(view, adapter, stickyHeader), LayoutContainer {
override val containerView: View?
get() = itemView
}

View File

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

View File

@ -0,0 +1,71 @@
package eu.kanade.tachiyomi.ui.base.holder
import android.os.Build
import android.view.View
import android.view.ViewGroup
import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.davidea.flexibleadapter.items.IFlexible
import eu.davidea.flexibleadapter.items.ISectionable
import eu.kanade.tachiyomi.util.dpToPx
import io.github.mthli.slice.Slice
interface SlicedHolder {
val slice: Slice
val adapter: FlexibleAdapter<IFlexible<*>>
val viewToSlice: View
fun setCardEdges(item: ISectionable<*, *>) {
// Position of this item in its header. Defaults to 0 when header is null.
var position = 0
// Number of items in the header of this item. Defaults to 1 when header is null.
var count = 1
if (item.header != null) {
val sectionItems = adapter.getSectionItems(item.header)
position = sectionItems.indexOf(item)
count = sectionItems.size
}
when {
// Only one item in the card
count == 1 -> applySlice(2f, false, false, true, true)
// First item of the card
position == 0 -> applySlice(2f, false, true, true, false)
// Last item of the card
position == count - 1 -> applySlice(2f, true, false, false, true)
// Middle item
else -> applySlice(0f, false, false, false, false)
}
}
private fun applySlice(radius: Float, topRect: Boolean, bottomRect: Boolean,
topShadow: Boolean, bottomShadow: Boolean) {
val margin = margin
slice.setRadius(radius)
slice.showLeftTopRect(topRect)
slice.showRightTopRect(topRect)
slice.showLeftBottomRect(bottomRect)
slice.showRightBottomRect(bottomRect)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
slice.showTopEdgeShadow(topShadow)
slice.showBottomEdgeShadow(bottomShadow)
}
setMargins(margin, if (topShadow) margin else 0, margin, if (bottomShadow) margin else 0)
}
private fun setMargins(left: Int, top: Int, right: Int, bottom: Int) {
if (viewToSlice.layoutParams is ViewGroup.MarginLayoutParams) {
val p = viewToSlice.layoutParams as ViewGroup.MarginLayoutParams
p.setMargins(left, top, right, bottom)
}
}
val margin
get() = 8.dpToPx
}

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.base.presenter
import nucleus.presenter.RxPresenter
import nucleus.presenter.delivery.Delivery
import rx.Observable
open class BasePresenter<V> : RxPresenter<V>() {
@ -35,4 +36,29 @@ open class BasePresenter<V> : RxPresenter<V>() {
fun <T> Observable<T>.subscribeReplay(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null)
= compose(deliverReplay<T>()).subscribe(split(onNext, onError)).apply { add(this) }
/**
* Subscribes an observable with [DeliverWithView] and adds it to the presenter's lifecycle
* subscription list.
*
* @param onNext function to execute when the observable emits an item.
* @param onError function to execute when the observable throws an error.
*/
fun <T> Observable<T>.subscribeWithView(onNext: (V, T) -> Unit, onError: ((V, Throwable) -> Unit)? = null)
= compose(DeliverWithView<V, T>(view())).subscribe(split(onNext, onError)).apply { add(this) }
/**
* A deliverable that only emits to the view if attached, otherwise the event is ignored.
*/
class DeliverWithView<View, T>(private val view: Observable<View>) : Observable.Transformer<T, Delivery<View, T>> {
override fun call(observable: Observable<T>): Observable<Delivery<View, T>> {
return observable
.materialize()
.filter { notification -> !notification.isOnCompleted }
.flatMap { notification ->
view.take(1).filter { it != null }.map { Delivery(it, notification) }
}
}
}
}

View File

@ -10,7 +10,6 @@ public class NucleusConductorDelegate<P extends Presenter> {
@Nullable private P presenter;
@Nullable private Bundle bundle;
private boolean presenterHasView = false;
private PresenterFactory<P> factory;
@ -22,8 +21,8 @@ public class NucleusConductorDelegate<P extends Presenter> {
if (presenter == null) {
presenter = factory.createPresenter();
presenter.create(bundle);
bundle = null;
}
bundle = null;
return presenter;
}
@ -37,31 +36,26 @@ public class NucleusConductorDelegate<P extends Presenter> {
}
void onRestoreInstanceState(Bundle presenterState) {
if (presenter != null)
throw new IllegalArgumentException("onRestoreInstanceState() should be called before onResume()");
bundle = presenterState;
}
void onTakeView(Object view) {
getPresenter();
if (presenter != null && !presenterHasView) {
if (presenter != null) {
//noinspection unchecked
presenter.takeView(view);
presenterHasView = true;
}
}
void onDropView() {
if (presenter != null && presenterHasView) {
if (presenter != null) {
presenter.dropView();
presenterHasView = false;
}
}
void onDestroy() {
if (presenter != null) {
presenter.destroy();
presenter = null;
}
}
}

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