Compare commits

...

419 Commits

Author SHA1 Message Date
f8a03226ee Release v0.7.3 2018-04-28 11:10:29 +02:00
32db1e3045 Run downloader in foreground service 2018-04-28 10:54:27 +02:00
303e6c0102 Reorganize reader settings. Update Conductor version 2018-04-28 10:40:08 +02:00
18883f1ba3 Crop borders for webtoon now have a separate setting. Close #972 2018-04-27 16:55:12 +02:00
5c31271e91 Workaround a crash related to saving instance state and child controllers 2018-04-25 16:26:46 +02:00
00981cf4e8 Include firebase analytics 2018-04-25 13:46:57 +02:00
968f4a69e8 Separate 'en' locale into 'en-US' and 'en-GB' for displaying dates 2018-04-22 13:15:47 +02:00
e7e1a9bf50 Fix #1073 2018-04-15 13:26:33 +02:00
fe1becb001 Update strings.xml (#1324) 2018-04-15 12:59:52 +02:00
7789171c71 Remove F-Droid.org link (#1357)
* remove fdroid.org

* Update README.md

* fix broken links
2018-04-15 12:48:03 +02:00
3fd2222c99 Update russian sources (#1362)
1) moved headerBuilder to imageRequest
2) changed the method of gets thumbnails
3) updated js for genres
4) update genre list
2018-04-15 12:47:39 +02:00
6de36a88c0 Fix tracking search layout 2018-04-13 16:28:09 +02:00
b37685542d Add new translation link (#1355) 2018-04-09 16:25:30 +02:00
a2b1b9e746 Release v0.7.2 2018-04-08 19:10:18 +02:00
8017324033 Fix #1351 2018-04-08 18:58:28 +02:00
7464497c88 Use our OkHttpClient in updates checker. It should fix the updater on KitKat due to TLS 2018-04-06 10:02:01 +02:00
499def3daa Fix downloaded text drawn outside the screen 2018-04-06 08:29:17 +02:00
6931b75cc5 Release v0.7.1 2018-04-05 23:01:32 +02:00
f853610578 Show last update if date > 0 2018-04-05 22:55:23 +02:00
69f51b88bf Fix typo in layout 2018-04-05 22:08:23 +02:00
e0d680201a Update constraint layout & fix broken layouts 2018-04-05 21:50:44 +02:00
1566b8f8b8 Provide accept & accept-language to cloudflare 2018-04-05 19:12:17 +02:00
4bbf78e840 Don't send cache control with cloudflare challenge 2018-04-05 11:58:28 +02:00
7ab16a69df Update travis script 2018-04-05 10:56:16 +02:00
95e60ed775 Update cloudflare interceptor and android studio 2018-04-05 10:36:29 +02:00
d38cd2547a Enable TLS 1.1 and TLS 1.2 on Android KitKat (and older) (#1316)
* Enable TLS 1.1 and TLS 1.2 on Android KitKat (and older)

* enable SSLv3

* use extension function
2018-03-25 17:08:29 +02:00
2159b72e69 Dialog color fix (#1308) 2018-03-14 18:01:30 +01:00
81c23bbf9d Update Batoto toString() method to support downloaded chapters 2018-03-14 12:54:31 +01:00
0d5b8edf31 Release v0.7.0 2018-03-11 12:10:56 +01:00
Sai
fcdb80830b New colors + theme attrs (#1272)
* New colors + theme attrs

Added new colors.xml values and modified some themes.xml fields for more customisability when switching between themes.

* Small fix for dialogs

It should look more distinguishable for the Dark theme now
2018-03-10 20:17:26 +01:00
50b48ab25c Fix info layout + disable tag clicks for now 2018-03-09 22:35:10 +01:00
31b45666b0 Kotlin update 2018-03-09 21:47:02 +01:00
233e76724a Fixes and Tweaks to Info Page (#1198)
* Fixes inorichi/tachiyomi#1194 by putting the name in a auto size

* removed ellipsizing of title

* moved the genre tags inside of the scroll view for inorichi/tachiyomi#1196

* saving my layout options for the night
2018-03-09 21:42:39 +01:00
af637a82c3 Fix subtle bugs when installing/loading extensions 2018-03-09 18:56:02 +01:00
ea32ea11f2 Fixed marked previous as read not deleting chapters (#1283) 2018-03-07 20:02:59 +01:00
1b7a0de745 Added country/region support for locale when displayed for sources (#1240)
* Added country support for locale when displayed for sources

* code review changes/comments fix
2018-03-05 19:46:18 +01:00
50e0cb65d9 Anilist search fix (#1289)
* fixed issue where some anilist results not showing due to null description.

* remove blank line
2018-03-05 19:45:02 +01:00
ba4807f62c Add logging to controller lifecycle to help reproducing bugs 2018-03-04 21:04:41 +01:00
5efc02a238 Update Kissmanga genres (#1278) 2018-03-02 19:38:25 +01:00
8e50ac67bc Bugfixes and extension installation improvements 2018-03-02 18:10:10 +01:00
a3c03e8ceb Fix imports from last commits 2018-02-27 19:07:33 +01:00
5a3e30b30a Update conductor to latest snapshot (with a minor fix) 2018-02-27 19:06:34 +01:00
e3ab90042d Add missing languages in settings (#1275) 2018-02-26 08:47:36 +01:00
f35c15f7d2 [WIP] Translations (#1134)
Translations
2018-02-24 15:39:46 +01:00
32387cd034 Update available extesions whenever the screen is opened 2018-02-24 15:38:19 +01:00
cf5c816483 fix restore from old backup to updated trackimpl. (#1269)
* fix restore from old backup to updated trackimpl.
added backup of tracking url for new backups

* assignment not needed
2018-02-22 21:54:05 +01:00
bf9b9ca54c Change Source Migration menu item to use string resource (#1268) 2018-02-22 11:31:22 +01:00
0ca2ca33c2 add override status back in (#1260) 2018-02-19 08:17:59 +01:00
51f25e96e9 Travis update 2018-02-18 20:20:53 +01:00
1875047638 Forgot the backup manager isn't injected 2018-02-18 20:16:06 +01:00
fa4d61eaf0 Run periodic backups without launching services 2018-02-18 20:14:12 +01:00
49eb638e15 Dependency updates 2018-02-18 20:02:31 +01:00
fc1f290b85 removed extra blank lines (#1259)
fixed results not showing for jellybean
made edit text max line 1 to prevent it newlines being added and moving the edit text into the list view
2018-02-18 19:36:34 +01:00
9194dc0161 Chapter Metadata update (#1257)
* change chapter update to refresh on any metadata change

* moved check into private function
2018-02-18 19:20:05 +01:00
0d480dbf7c Remove debug log 2018-02-18 19:19:59 +01:00
183e83684a Remove batoto from catalogues 2018-02-18 17:39:45 +01:00
7b4ac7998a Remove simultaneous downloads 2018-02-18 17:34:22 +01:00
d75c6b0c36 Fix duplicate entries in source migration. Closes #1190 2018-02-17 19:06:15 +01:00
40b222f8bc Improve tracking search results (#1178)
* initial commit
changed tracking info screen
added ability to click logo to launch website

* added publishing status and type to description.
adjusted layout some

* added start date to track info

* tweaked layout

* tweaked layout

* tweaked layout

* code review changes

* code review changes part 2

* code review changes
2018-02-17 13:04:49 +01:00
aa7dfb7bee Update README.md (#1241)
Include CONTRIBUTING.md guidelines as collapseable elements.
Remove white background from screenshots
Optimize app-icon.png and screens.png using `optipng -o2`
2018-02-17 13:01:46 +01:00
6c1453eb54 Library filter UI change (#1211)
* similar library filter to catalog filter

* removed some commented out code

* code review changes

* fixed accidentally removing title
2018-02-16 15:23:15 +01:00
c1845aec83 Sort extensions by package name. Minor changes to extension installer 2018-02-08 15:16:13 +01:00
eb8479ac9a Timeout the installation of extensions after 10s 2018-02-06 22:11:36 +01:00
636c027298 Fix extensions installer on old Android versions. Fix deadlock on devices with 1-2 cores 2018-02-06 11:42:38 +01:00
02e187f066 Add notice for updating extensions 2018-02-05 22:55:29 +01:00
854112095b Downloading extensions from Github Repo. (#1101)
Downloading extensions from Github Repo.
2018-02-05 22:50:56 +01:00
a71c805959 fix author/artist not showing in mangahere (#1228) 2018-02-05 11:19:24 +01:00
c3ced0d089 adjusted chapters item since Android 16 doesnt support right and left (#1221) 2018-01-31 14:38:05 +01:00
80996ea63e Add page down/page up hardware detection (#1212)
* Added page down and page up key event.  Have it always on since page down and page up buttons are only on readers or keyboards

* moved code to different method

* added spaces back to comments
2018-01-31 14:37:02 +01:00
aff51f8af1 hide latest button when source doesnt support latest (#1217) 2018-01-28 18:37:58 +01:00
ccbb81e9f5 Ask for permission if necessary when browsing local sources. (#1216) 2018-01-28 12:23:40 +01:00
f88dd28c51 Add option to change double tap animation speed in the reader (#974)
* Add option to change double tap animation speed in the reader

* address requests from review
2018-01-26 20:22:31 +01:00
a65a71df5d updated mangahere to show licensed status (#1214) 2018-01-26 17:09:20 +01:00
22f2ecc433 fix genre tags to be delimited correctly (#1215) 2018-01-26 17:09:08 +01:00
7f90ad7847 Fix chapter recognition regex and detail number (#1213)
* Update basic filter for sources that include space between numbers

Wasnts matching on  vol. 1 ch. 10 previously so mangadex last chapter was showing volume number.

* Don't show last chapter number when there are 0 chapters or chapters with no numbers.

This prevents one shots from showing with -1 as last chapter and instead just leaves it blank

* added else to be Unknown instead of blank

* removed empty line
added test case

* switched to null safe ?.

* Revert "switched to null safe ?."

This reverts commit 97a9300d1bedc8e01efb439c180eced8eaa1da5b.
undo

* switched to null safe ?.
2018-01-26 14:32:34 +01:00
1292c0ecea Fix library query being lost 2018-01-25 19:59:15 +01:00
55b7d5025b fixed 3 dot icon (#1209) 2018-01-24 07:19:55 +01:00
6a310bbaa9 Added custom download option (#1185)
* Added custom download option

* Implemented new design. TODO comments (like always...)

* W00t comments

* Implemented code review.

* Fixed commit breaking mistake :O

* Small design fix
2018-01-23 21:18:55 +01:00
bc8753da85 Remove teal background 2018-01-23 19:03:50 +01:00
7f63e318f1 Catalog visuals update 1155 (#1167)
* adjusted search to be lower in navview

* close drawer on search
moved search and reset to bottom

* switched sort icon to arrow

* allow secondary drawer to swipe open and close

* fixed click to collapse for sortgroup, and group item
updated to rc4 flexibleadapter

* added header to drawer

* changed string to Search filters

* collapsed sort group

* fixed arrow size

* added divider line

* fixed vector size

* add divider id and tools text
2018-01-23 18:50:48 +01:00
6c749319cf increase touch area for 3 dot in chapter list (#1205)
* increase touch area for 3 dot in chapter list

* moved 3 dot over and made it vertical

* adjusted location slightly
2018-01-23 18:49:26 +01:00
7a4463e104 fixed alpha not showing for manga in library during global search (#1203) 2018-01-21 19:15:24 +01:00
e1be4ba925 fixed ReadMangaToday search issue (#1200) 2018-01-21 17:24:24 +01:00
34d21c1de3 Information Page Improvements (click to search, copy to clipboard, etc) (#1139)
* adds long click to copy details per inorichi/tachiyomi#1127

* Added the latest update date for inorichi/tachiyomi#1098 and possible fix for inorichi/tachiyomi#1141

* cleanup some mistakes I left

* adds modifications to full name display for inorichi/tachiyomi#1141 and click to search on various information pieces for inorichi/tachiyomi#860

* This modifies how the full title shows up in the info pages and also properly ellipsizes the titles in the catalogue/library list views

* Changes full title layout in horizontal mode

* Adds the tags in using AndroidTagGroup library

* reverting the sdk version in the gradle build

* code cleanup

* added back status update
2018-01-18 19:15:33 +01:00
fae36aebf4 Add referer to readmanga/mintmanga requests header (#1192) 2018-01-17 13:32:54 +01:00
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
len
fab7967018 Release 0.6.0 2017-07-08 18:07:43 +02:00
len
bb40a4d6b8 Dependency updates. Enable new translations. Minor fixes 2017-07-08 16:44:01 +02:00
len
90d27147e6 Fix gradle build warnings. Remove unused strings 2017-07-05 17:21:27 +02:00
634247c590 New translation system (#844)
* Change translation part

* Translated using Weblate (Russian)

Currently translated at 99.1% (355 of 358 strings)

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

* Added translation using Weblate (Dutch)

* Translated using Weblate (Spanish)

Currently translated at 67.8% (243 of 358 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 68.1% (244 of 358 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 68.4% (245 of 358 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 68.4% (245 of 358 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 68.7% (246 of 358 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 68.7% (246 of 358 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 48.6% (174 of 358 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 48.8% (175 of 358 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 68.9% (247 of 358 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 69.8% (250 of 358 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 71.2% (255 of 358 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 55.5% (199 of 358 strings)

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

* Translated using Weblate (Spanish)

Currently translated at 99.7% (357 of 358 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (358 of 358 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (358 of 358 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (358 of 358 strings)

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

* Translated using Weblate (Italian)

Currently translated at 86.3% (309 of 358 strings)

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

* Translated using Weblate (Italian)

Currently translated at 86.8% (311 of 358 strings)

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

* Translated using Weblate (Italian)

Currently translated at 87.4% (313 of 358 strings)

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

* Translated using Weblate (Italian)

Currently translated at 87.9% (315 of 358 strings)

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

* Translated using Weblate (Italian)

Currently translated at 92.4% (331 of 358 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (358 of 358 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (358 of 358 strings)

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

* Added translation using Weblate (Latvian)

* Translated using Weblate (Dutch)

Currently translated at 100.0% (358 of 358 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (358 of 358 strings)

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

* Translated using Weblate (Italian)

Currently translated at 98.6% (353 of 358 strings)

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

* 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 (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 (Dutch)

Currently translated at 100.0% (358 of 358 strings)

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

* Added translation using Weblate (Arabic)

* Translated using Weblate (Latvian)

Currently translated at 25.1% (90 of 358 strings)

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

* Translated using Weblate (Dutch)

Currently translated at 100.0% (358 of 358 strings)

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

* 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 (French)

Currently translated at 100.0% (358 of 358 strings)

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

* Added translation using Weblate (Korean)

* Translated using Weblate (Korean)

Currently translated at 1.1% (4 of 358 strings)

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

* Translated using Weblate (Korean)

Currently translated at 4.1% (15 of 358 strings)

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

트래킹? 동기화? 추적?

* Translated using Weblate (Korean)

Currently translated at 14.2% (51 of 358 strings)

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

* Added translation using Weblate (Portuguese (Brazil))

* Translated using Weblate (Portuguese (Brazil))

Currently translated at 72.9% (261 of 358 strings)

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

* 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 (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/
2017-07-05 17:09:27 +02:00
len
fd8f7ea693 Fix #880. Downgrade conductor to 2.1.2 for now 2017-07-05 14:19:58 +02:00
len
5eeb497f2b Fallback batoto urls to http (a library update took ages). Kotlin update to 1.1.3 2017-07-04 15:50:53 +02:00
505e642691 Fix issues with Batoto; some links now use https rather than http, so parsing was affected. (#873) 2017-07-02 20:35:07 +02:00
len
74a7e2a17e Fix local source not working if english was disabled. Closes #848 2017-06-17 12:46:34 +02:00
len
5fec956ce6 Main activity now uses single task. Fixes #850. Actually use new support library 2017-06-17 12:34:46 +02:00
len
1794782323 Add option to invert volume keys. Closes #834 2017-06-11 11:36:12 +02:00
len
0210ee8747 Fix #819. Update support lib 2017-06-10 16:49:40 +02:00
len
ca412832ef Library notification now opens recent updates. Closes #808 2017-06-04 18:56:24 +02:00
1089c25b8f allow sorting by total chapters for library view (#811)
* allow sorting by total chapters for library view

* allow sorting by total chapters for library view

* Changed to remove query per manga.
2017-05-31 21:50:21 +02:00
e85841784c Russian strings update (#820)
* Russian strings update

* Fix russian string

* Russian string. Резеврное копирование -> бэкап
2017-05-29 08:00:27 +02:00
ca2236958a Reorganize layouts by feature 2017-05-25 12:16:58 +02:00
c7686323b7 Remove activity mixin class 2017-05-24 14:00:26 +02:00
73d1a1a05e Fix backup issue. Closes #806 2017-05-24 11:15:40 +02:00
len
211f7b591b Dependency updates. OkHttp nullability changes 2017-05-23 20:39:02 +02:00
len
72ea256906 Downloads with conductor. Remove flexible adapter 4 dependency and unused classes. 2017-05-23 20:03:16 +02:00
f521622d4d Minor changes to tabs animator 2017-05-23 14:28:07 +02:00
dc5283ce9a Use an object animator for the tabs 2017-05-22 11:28:41 +02:00
len
256a4197c9 Replace changelog dialog with controller, move migration logic to a separate class 2017-05-21 13:42:06 +02:00
len
a5a12f8b3a Add landscape layout for manga info. Fix portrait layout image paddings when the tab layout was expanded 2017-05-20 14:56:31 +02:00
len
bbe180ecd1 Improve tab layout animation. Fixes #800 and #801 2017-05-20 12:15:44 +02:00
67678cd49e Add an option to refresh all tracking metadata 2017-05-17 13:36:42 +02:00
097d4fe34c Fix memory leak 2017-05-16 14:44:18 +02:00
5914346ace Remove unused classes and arrays resources 2017-05-16 14:11:23 +02:00
062788f222 Fixed tracking cardview + readded AMOLED theme. (#798)
* Fixed cardview margin for sources

* Added AMOLED again

* changed padding to margin
2017-05-16 10:18:46 +02:00
55be9b9ca5 Fix settings crashes before Lollipop 2017-05-15 16:32:53 +02:00
len
0da2f91771 Info shows last chapter instead of chapter count. Resolves #765 2017-05-14 19:55:48 +02:00
ff190e02d4 Preferences with conductor (#792)
* Settings with conductor WIP

* Add downloads preference controller. Implement source/track login

* Improve settings controllers

* Backup settings controller

* Delete preferences xml

* Remove keys from xml

* PreferenceKeys is now an object

* Remove now unused dependency
2017-05-14 00:45:14 +02:00
29fd5747eb Recent chapters with constraint layout 2017-05-08 13:06:40 +02:00
fa8f5bc0d8 Add images and 'view chapters' to library updates (#785)
* added option to open manga directly from library update
added covers for manga in library update
added ability to click covers to open manga directly from library update

* Removed 3 dot option to open manga
Adjusted covers to circles and material standard for recent chapter

* fixed potential null pointer on cover click

* adjusted circle imageview size for recently read
2017-05-08 09:22:49 +02:00
2118434823 Initial AMOLED theme + some CardView fixes (#787)
* Initial AMOLED theme + some CardView fixes

* small fix
2017-05-07 12:36:25 +02:00
2eeac0bf8b UI with Conductor (#784) 2017-05-06 15:49:39 +02:00
len
89b293fecd Kitsu: remove limit from query 2017-05-05 16:36:54 +02:00
len
3f758d5981 Kitsu: replace media_id with manga_id 2017-05-01 00:29:08 +02:00
e838bb43d2 Deleting categories hides manga until switch from and to library #686 (#769)
* Fixed bug 686
https://github.com/inorichi/tachiyomi/issues/686

* Fixed bug 686
https://github.com/inorichi/tachiyomi/issues/686

* undo previous changes to resubscribe
add table to mangaQueries to update on table category change

* cleaned up formatting
2017-04-27 20:16:05 +02:00
b7b83305b2 Allow multiple sources in each extension source apk (#761)
* Allow multiple sources in each extension source apk

Minor code cleanup

* Add runtime defined sources functionality

* Undo extensions library major version number bump
2017-04-27 20:15:55 +02:00
len
8df3080e0d Pass backup uri as parcelable to restore service 2017-04-25 16:29:39 +02:00
len
f88794c752 Keep models from source package 2017-04-21 00:19:10 +02:00
len
cc9e2cee1f Fix webtoon scroll jumps. Closes #751 2017-04-17 20:01:07 +02:00
len
91cb892c74 Release 0.5.2 2017-04-14 12:46:58 +02:00
len
a26f908370 Dependency updates 2017-04-09 18:25:05 +02:00
len
4d14f56fa8 Improve webtoon reader scroll up 2017-04-09 18:24:52 +02:00
d9a2255be9 Retain last read page when using the webtoon mode (#738)
* Retain last read page when using the webtoon mode, see issue #453

* #738 inorichi's request change to webtoonreader pull request

* #738 per inorichi recycler could be null at the point
scrollToLastPageRead was called, moved to below the check in the view
had been initialized.
2017-04-09 16:01:07 +02:00
len
5e3d71c6c5 Fix shortcuts 2017-04-09 15:53:47 +02:00
len
619d94bf36 Kitsu: use new rating system. Fixes #743 2017-04-09 13:56:52 +02:00
6069659e0f Small fixes (#740) 2017-04-07 21:17:21 +02:00
f6a79bde6f Add manga straight into a category from catalogues (#737)
* Add feature mention in issue #625
2017-04-07 20:39:09 +02:00
len
bb9e230b35 Fix #708 2017-04-07 20:32:22 +02:00
len
bc9417e16b Notify licensed content in mangahere 2017-04-06 20:23:03 +02:00
len
a4313d388d Fix activity leaks in backup, restore dialogs and properly handle db transactions 2017-04-06 17:26:24 +02:00
4ebb3a894d Added round icon + added shortcuts (#732)
* Added round icon + added shortcuts

* Moved values to companion
2017-04-04 17:42:39 +02:00
0642889b64 Rewrote Backup (#650)
* Rewrote Backup

* Save automatic backups with datetime

* Minor improvements

* Remove suggested directories for backup and hardcoded strings. Rename JSON -> Backup

* Bugfix

* Fix tests

* Run restore inside a transaction, use external cache dir for log and other minor changes
2017-04-04 17:42:17 +02:00
len
3094d084d6 Library notification: handle only one update as a special case 2017-04-01 12:05:09 +02:00
len
f9fec74ffd Add short description to library update notification 2017-03-30 20:02:48 +02:00
8ef3ab0d49 Cancel library progress notification after posting the result 2017-03-29 09:17:53 +02:00
len
e9a6f8ef46 Update app icon with shadow 2017-03-26 11:42:32 +02:00
len
68724752f8 Separate some changes unrelated to backup from PR 2017-03-26 11:26:10 +02:00
len
de8fa09366 Keep new chapters notification across updates 2017-03-25 22:00:55 +01:00
len
e619870eec Fix #716 2017-03-20 20:34:26 +01:00
len
4be5f0dab3 Release 0.5.1 2017-03-19 11:58:56 +01:00
len
abe1929b49 Update vietnamese strings. Document Kissmanga changes 2017-03-19 10:51:38 +01:00
68c4116327 Category-specific auto download (#701)
* Category-specific auto download
2017-03-18 13:09:40 -04:00
len
3be9881997 Kissmanga fix. Kotlin 1.1.1 2017-03-18 14:11:16 +01:00
2e44f29882 Prevent some manga breaking the download notifier (#711) 2017-03-15 22:19:06 +01:00
len
a5520c1936 Manga info with constraint layout 2017-03-12 13:00:47 +01:00
len
112cdd54e3 Update chapters adapter 2017-03-11 21:20:46 +01:00
len
b512c67b5d Fix #704. Dependency updates 2017-03-11 16:00:07 +01:00
len
d8fa7bc9d2 Post updater notification before starting downloads 2017-03-10 20:36:43 +01:00
len
41397ab41d Don't post too many notifications in the updater 2017-03-10 20:21:09 +01:00
len
c437f1473c Add dev flavor. Bugfix in reader 2017-03-08 18:56:27 +01:00
6020cd011d Fix #692. Mangasee needs proper headers for data requests. (#694) 2017-03-03 22:53:54 +01:00
len
582bb3e2ca Handle a few more possible external directories before Lollipop 2017-03-03 18:45:25 +01:00
len
5c67161dce Minor changes for Kotlin 1.1 2017-03-03 18:18:06 +01:00
len
c00eaae62b AS 2.3 and Kotlin 1.1 2017-03-03 17:42:46 +01:00
len
8e4dd030d0 Release v0.5.0 2017-02-26 13:58:45 +01:00
len
4d7b188999 Fix #636. Minor improvements. Dependency updates 2017-02-23 22:30:43 +01:00
len
6de260d73f Fix #660. Update subsampling 2017-02-19 23:39:53 +01:00
len
2230ad59f1 Minor fixes and improvements. Dependency updates. Drop support for the old armeabi and use arm64-v8a instead 2017-02-16 23:22:30 +01:00
len
e22b1661f4 Borders crop improvements 2017-02-15 21:56:05 +01:00
bb723076ee Chapter Recognition for Read/Mintmanga (#682) 2017-02-12 19:43:00 +01:00
fd9c24413d Update CONTRIBUTING.md (#684)
Add catalogue redirection to tachiyomi-extensions
2017-02-12 19:31:38 +01:00
len
91c58640a7 Add crop borders functionality, #219 2017-02-12 00:30:32 +01:00
len
c8e3375248 Rar/cbr support 2017-02-08 22:12:00 +01:00
len
aeef8c02d8 Basic epub support 2017-02-05 12:01:58 +01:00
len
08f2cd2472 Error drawable now looks better with the dark theme 2017-02-04 20:49:07 +01:00
len
fe413d52d6 Show loading/error for images in catalogue grid view. Update support lib 2017-02-04 19:07:06 +01:00
add2ca0b8f Fixed multi-threaded initial download not showing. (#675)
Fixed library notification cancel PendingIntent.
2017-02-04 17:57:26 +01:00
len
ad6cdc9017 Allow glide to use source's network client. Catalogue fixes 2017-02-04 13:44:18 +01:00
dd8cab4562 improve local manga chapter sorting (#672) 2017-02-04 12:03:54 +01:00
len
f7c791d153 Ask for chapter deletion when removing from library 2017-02-03 20:14:33 +01:00
len
b66f06d9dc Use the first 3 bytes for jpeg 2017-02-02 23:44:25 +01:00
len
89940677cc Use custom mime discovery in downloader 2017-02-02 20:21:38 +01:00
56e7a1e2a0 Language hot fix (#668) 2017-02-02 17:01:18 +01:00
len
810f5ad531 Fix FAB behavior 2017-02-02 16:48:18 +01:00
495f5d03ac Enable Vietnamese language 2017-02-02 16:13:42 +01:00
f63d2cebfc Update Vietnamese Language (#664) 2017-02-02 16:02:25 +01:00
ae60e8cbd5 Fix Mangachan issue from #628 (#667) 2017-02-02 16:02:02 +01:00
len
772bef05fc Fix #661 2017-01-30 21:04:56 +01:00
len
9320d1f7a4 Update subsampling 2017-01-29 23:05:55 +01:00
len
e8912c5dc9 Local chapter url relative. Other minor changes 2017-01-29 20:51:11 +01:00
2b73a9d2a4 Local manga in zip/cbz/folder format (#648)
* add local source

* small fixes

* change Chapter to SChapter and and Manga to SManga in ChapterRecognition.
Use ChapterRecognition.parseChapterNumber() to recognize chapter numbers.

* use thread poll

* update isImage()

* add isImage() function to DiskUtil

* improve cover handling

* Support external SD cards

* use R.string.app_name as root folder name
2017-01-29 20:48:55 +01:00
len
e25ce768bb Migrate covers to external files dir. Fix #647 2017-01-26 22:15:17 +01:00
05c628b33c Bulgarian translation typos and fixes for better understanding (#655) 2017-01-26 08:12:43 +01:00
len
fda0aa3ce1 Fix sticky headers in recent chapters not working after rotation 2017-01-24 16:01:43 +01:00
len
cdf5bbadea Disable elevation in recent chapters. Improve downloads discovery 2017-01-22 23:13:07 +01:00
len
c6b89a826c Update recent chapters adapter 2017-01-22 20:06:43 +01:00
6264e56148 Added translatable="false" to keys so that they don't show up as untranslated (#645) 2017-01-21 20:34:34 +01:00
len
7f8bfd759f Fix filter input text introducing a new line. Also fix filter drawer clicks propagated to the views below 2017-01-21 20:31:25 +01:00
len
c3c2013944 Add bulgarian language #644 2017-01-21 19:51:40 +01:00
len
1e4a01399d Catalogue fixes 2017-01-21 18:48:27 +01:00
len
03967b67cf Update extension loader 2017-01-21 17:49:34 +01:00
len
7425478a55 Optimize imports 2017-01-20 21:34:15 +01:00
len
f807447de1 Rename OnlineSource to HttpSource 2017-01-20 21:27:53 +01:00
len
706163e7a6 Move source and network outside data 2017-01-20 21:24:31 +01:00
len
a4c145c1ef Optimize imports 2017-01-20 21:22:17 +01:00
c445ea90ba Notification Improvements (#594)
* Download notifier improvements

* Notification improvements

Added a Notification Service.

Added a Notification Activity Handler.

* Removed service. Everything is now managed by single broadcast

* Fixed some flags

* Fixed ReaderActivity call

* Code review

* Added Handler. Removed dismiss onDestroy
2017-01-20 21:18:15 +01:00
len
52c50398b8 Fix Rapid region decoder 2017-01-19 23:34:54 +01:00
fb89f77db7 French language (#634)
* Added french string values in res

* Added french in the list of languages available

* Fixed remaining //

* Update manga plural according to http://www.academie-francaise.fr/niky-orange

* Update translation based on M2ck's remarks

* Translating remaining english bits and fixing some typos

* Further fixes according to M2ck's suggestions

* Ninja typo fix !

* Updated tapping string with french translation and updated a pretty awkward literal translation

* Updated translation to new strings
2017-01-19 22:48:11 +01:00
len
f7b94179a4 Update subsampling with some fixes 2017-01-19 21:12:49 +01:00
len
e045ca8538 Dependency updates 2017-01-18 21:27:01 +01:00
len
871e17c2f5 Rewrite catalogue adapter 2017-01-17 20:13:29 +01:00
len
f86c3c81bf Catalogue with only one recycler 2017-01-16 22:18:15 +01:00
len
71ab6d38e4 Complete group filters 2017-01-15 17:04:31 +01:00
len
e76fb7a524 Minor changes 2017-01-15 01:08:35 +01:00
len
90a99dde1f Filters with flexible adapter 2017-01-14 22:38:31 +01:00
len
7b9f5d0e9f Update category adapter 2017-01-13 19:35:20 +01:00
len
e4d4dbbeb6 Change package name to flexible adapter v4 2017-01-12 16:24:40 +01:00
len
6ef94fb59b Kitsu: include manga instead of media 2017-01-12 15:38:42 +01:00
a03dceff7d Add Sort filter [Catalogs] (#633)
* Add Sort filter

* remove old views

* onClick default descending

* update remaining catalogs
2017-01-12 15:37:38 +01:00
f717c57648 Added Russian language (#631)
* added russian res/values

* Russian strings fix typo

* added Russian lang in list of lang's

* Russian string.xml: fixes

* Russian string.xml: fixes

* Russian string.xml: fixes

* strings-ru fixes
2017-01-10 20:34:45 +01:00
ca8fdad422 Fixed a typo in an Italian string (#629) (#630) 2017-01-09 14:00:12 +01:00
len
faa61923fb Fix #620 2017-01-08 23:40:54 +01:00
len
96a39f5c54 Remove custom presenter class 2017-01-08 20:56:42 +01:00
72f8c4d5e2 Added add to library dialog when downloading from catalogue (#618)
* Now show snackbar when adding from catalogue

* Code cleanup + added manga favorite event to update favorite drawable when added via snack

* Update SettingsAdvancedFragment.kt

Forgot to check optimize import. I think(hope) I got them all ;).

* Now uses PublishRelay. Manga favorite is now handled in info presenter

* Update MangaInfoFragment.kt
2017-01-08 20:07:19 +01:00
len
07cae4d684 Merge and remove util classes 2017-01-08 18:50:51 +01:00
dd56d7c0bb Initial support for external sources 2017-01-08 18:12:19 +01:00
len
77d986f213 Kitsu: also filter included by manga 2017-01-07 00:54:57 +01:00
len
13bcefe5cd Filter manga library entries in Kitsu 2017-01-07 00:29:52 +01:00
len
49d0e06704 Replace bad image url with the logo in mangafox. Related with #626 2017-01-04 19:12:47 +01:00
2c8790c545 Added circular thumbnails to the catalogue list view, like the ones in the library list view (#616)
* Added circular thumbnails to the catalogue list view, like the ones in the library list view

* Moved setImage to CatalogueHolder parent class and adapted the code so that when the manga is initialized, the thumbnail is set both in the case of grid and list

* In catalog, when switching between grid and list, initialize mangas only if going to grid view or if over wifi
2017-01-04 12:50:31 +01:00
len
d0260acd3d Set IME action done 2017-01-03 15:44:32 +01:00
44ec6184c8 README.md table update (#627) 2017-01-03 15:27:24 +01:00
len
cfa9729831 Remove unused resources 2017-01-02 18:59:15 +01:00
len
c25af3d5ad Change filters dialog with a drawer 2017-01-02 18:35:54 +01:00
d3e9200a7f Improve catalog search filters (#615)
* Add three state (include/exclude/ignore) search filters (works for now only on MangaFox and MangaHere)

* checkbox icons in xml format

* fix checkbox icons referencing

* fix three states filters in remaining catalogs

* use Spinner for filter with more than three states (Mangasee)

* use EditText for freetext filters (Mangasee)

* remove pngs

* Filter class/subclass

* add Filter.Header

* English catalogs
2017-01-02 18:30:10 +01:00
len
2032ba3ba3 Now using subsampling 3.6.0 2017-01-01 21:30:29 +01:00
len
9bcde69ee0 Release 0.4.2 2017-01-01 21:00:52 +01:00
len
beca2b429c Minor changes 2017-01-01 20:54:41 +01:00
len
3a1699f0b3 Fix #373 and a few crashes 2016-12-31 16:19:32 +01:00
len
a7192e866f Locale fix. Kotlin update to 1.0.6 2016-12-27 20:18:38 +01:00
len
dc882b4dce Make clear error codes are from HTTP 2016-12-26 18:12:15 +01:00
len
77b4de3941 Minor changes 2016-12-26 17:21:17 +01:00
len
006d17aac7 Fix locale not applied outside activities 2016-12-26 16:56:19 +01:00
len
1a3a1db4ff Remove Language class. App's language and hidden languages settings were reset 2016-12-26 15:44:59 +01:00
len
97fa659283 Kitsu fixes 2016-12-24 11:32:45 +01:00
f1d84ccb49 Add "Completed" filter; fix Mangahere; fix Mangafox (#604)
* Add "Compled" filter to all english sources; fix Mangahere manga title extraction; fix Mangafox search.

* update Mangasee

* update Batoto
2016-12-24 00:08:49 +01:00
1f14240251 Translated some strings to Italian (#602)
* Translated some strings to Italian

* Added missing strings and fixed a couple of errors
2016-12-24 00:08:27 +01:00
len
ea6fed6ecf Exclude novels from Kitsu results 2016-12-23 16:58:36 +01:00
len
d09eca7833 Anilist/Kitsu Fixes 2016-12-23 16:15:09 +01:00
2c6f64c5ae Refresh option in the library updates tab (#606)
* Solves #550

* Make sure only refresh can only happen when pulling down at top of update library list

* Removed unused import
2016-12-23 15:58:53 +01:00
ec87e4359b Drawerfix/readme update (#601)
* Fixed back button on navigational drawers

* Removing an unused import

* Cleaned up code

* little clean up
2016-12-23 15:56:10 +01:00
len
2b63bae989 Show login errors 2016-12-22 22:11:17 +01:00
len
82f4e3157a Minor changes 2016-12-22 21:57:15 +01:00
len
725ceab00b Hide API implementation from MAL service. Reorder methods and minor changes 2016-12-22 21:17:47 +01:00
len
ba428c401d Fix Kitsu refresh method 2016-12-22 16:34:34 +01:00
len
510669ee2c Fix wrong anilist decimal scores 2016-12-22 16:22:08 +01:00
len
8d749df290 Score formatting. Hide API from Anilist/Kitsu services. 2016-12-21 22:39:46 +01:00
len
091c0c0c71 Fix system language setting always using english 2016-12-21 00:42:46 +01:00
7fdd2cacd7 Fixed updater on Android N. Closes #592 (#595) 2016-12-21 00:34:31 +01:00
2241a0b2de Using title instead of text for Mangahere titles (#591)
Fixes #571
The text on the popular manga page of Mangahere contains escaped HTML characters. The title attributes of the links do not contain them.
2016-12-20 20:37:45 +01:00
len
d21a93123b Dependency updates 2016-12-20 18:58:21 +01:00
len
e542a8d8e2 Fix tab gravity 2016-12-20 16:54:56 +01:00
94ee4e7fb5 Experimental Anilist and Kitsu support (#586)
* Tracking tab with anilist support

* Rename MangaSync to Track

* Rename variables and methods to track

* Kitsu implementation

* Variables refactoring

* Travis fix?
2016-12-18 22:56:28 +01:00
len
e3d430eb5e Fix #587 2016-12-18 22:31:20 +01:00
len
fd76255cf6 Release 0.4.1 2016-12-18 21:05:33 +01:00
len
d180631877 Add ripple effect to filter nav view 2016-12-18 20:29:46 +01:00
len
1977e21363 Fix method conflicts 2016-12-18 16:59:06 +01:00
len
e1a3ee1b81 Bugfixes 2016-12-18 16:35:39 +01:00
cc43d9daed fixes wrong getBroadcast calls from imageNotification (#585) 2016-12-18 15:15:44 +01:00
len
79705df499 Apply material design guidelines to categories 2016-12-18 13:08:56 +01:00
len
36bbb906c1 Library sort change doesn't trigger filtering 2016-12-15 18:51:12 +01:00
len
816cc17ed3 Fix #577. Fix language not applied in reader activity. 2016-12-14 22:33:24 +01:00
len
97e3b5d2ab Add unread sorting 2016-12-13 22:23:49 +01:00
79ab9d80f2 Improved last_read sorting (#576) 2016-12-13 21:36:26 +01:00
len
32511149d1 Format fixes. Move lang setting to the first entry (looks better IMO) 2016-12-13 21:07:48 +01:00
cc9fd53abb Implement language switcher (#563)
* Implement language switching using BaseActivity

* Add requested changes

* Cleanup App.kt Imports and add pref_language_key

* Acutally use @string for key

* Use string resource for language preference title
2016-12-13 20:47:46 +01:00
len
4061c7450b Better network error handling 2016-12-12 20:53:44 +01:00
len
9ad535bde6 Optimize library downloaded filter 2016-12-11 23:59:25 +01:00
b067096fc7 Add drawer to filter and sort the library (#570)
* Add additional drawer to filter and sort the library

* Tint icon when there's a filter active

* Comments and minor changes
2016-12-11 12:43:44 +01:00
len
2dd58e5f7d Ask for confirmation before changing the cover. Fixes #562 2016-12-10 23:16:46 +01:00
len
7c42ab885b Readers know how to move to each side. Fix #566 2016-12-10 14:49:56 +01:00
len
26b283d44d Fix webtoon reader touch events. #561 2016-12-10 14:01:16 +01:00
len
8c1b07c4ba Handle null directories as empty arrays 2016-12-10 12:22:44 +01:00
len
f98e0858a7 Improve download discovery performance in library updates view 2016-12-09 20:23:48 +01:00
8b60d5bfcb Add optional to automatically download new chapers (#538)
* Add optional to automatically download new chapers

* Only trigger download once
2016-12-06 17:22:03 +01:00
len
30b4c6e755 Remove some state from the library view 2016-12-04 23:58:46 +01:00
len
3d2a98451b Avoid going to db when a library filter is changed 2016-12-04 23:48:29 +01:00
aba528b227 Added option to sort library (#536)
* Initial code

* Added all sort options

* Fixes

* Removed sort by added. Some renaming

* Removed date added database calls

* Fixes
2016-12-04 20:22:12 +01:00
725 changed files with 38044 additions and 18649 deletions

View File

@ -1,20 +1,27 @@
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#readme, not here
# Bugs
* Include version (Setting > About > Version)
* If not latest, try updating, it may have already been solved
* Dev version is equal to the number of commits as seen in the main page
* 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
@ -24,8 +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
File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated.
Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs)

View File

@ -1,7 +1,13 @@
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
**Please fill out this form and remove the first two lines before posting.**
**If your issue is a request for a catalogue it belongs here https://github.com/inorichi/tachiyomi-extensions/**
**App version:**
Remove line above and describe your issue here. Fill out version below. Use Preview.
**Issue/Request:**
**Steps to reproduce (if applicable)**
Version: r000 or v0.0.0
(other relevant info like OS)
1.
2.
3.
**Other details:**

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 730 KiB

View File

@ -1,31 +1,58 @@
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
jdk:
- oraclejdk8
before_script:
- chmod +x gradlew
#Build, and run tests
script: "./gradlew clean buildStandardDebug"
sudo: false
- build-tools-27.0.3
- android-27
- extra-android-m2repository
- extra-google-m2repository
- extra-android-support
- extra-google-google_play_services
licenses:
- android-sdk-license-.+
before_install:
- yes | sdkmanager "platforms;android-27" # workaround for accepting the license
- if [ "$TRAVIS_PULL_REQUEST" = "false" ]; then
openssl aes-256-cbc -K $encrypted_e56be693d4fd_key -iv $encrypted_e56be693d4fd_iv -in "$PWD/.travis/secrets.tar.enc" -out secrets.tar -d;
tar xf secrets.tar;
mv debug.keystore "$HOME/.android";
fi
- 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=

22
.travis/build.sh Executable file
View File

@ -0,0 +1,22 @@
#!/bin/bash
git fetch --unshallow #required for commit count
cp .travis/google-services.json app/
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

View File

@ -0,0 +1,73 @@
{
"project_info": {
"project_number": "777921915939",
"firebase_url": "https://tachiyomi-47364.firebaseio.com",
"project_id": "tachiyomi-47364",
"storage_bucket": "tachiyomi-47364.appspot.com"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:777921915939:android:36544cd2d96c50c7",
"android_client_info": {
"package_name": "eu.kanade.tachiyomi"
}
},
"oauth_client": [
{
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAHr8RxyeiSPC_MxJTnivz-hmdo5oX0QQQ"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
},
{
"client_info": {
"mobilesdk_app_id": "1:777921915939:android:564fdc1d62efd1de",
"android_client_info": {
"package_name": "eu.kanade.tachiyomi.debug"
}
},
"oauth_client": [
{
"client_id": "777921915939-9q25jvgbdtpk91daqlk7sa1cbdcg77o6.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAHr8RxyeiSPC_MxJTnivz-hmdo5oX0QQQ"
}
],
"services": {
"analytics_service": {
"status": 1
},
"appinvite_service": {
"status": 1,
"other_platform_oauth_client": []
},
"ads_service": {
"status": 2
}
}
}
],
"configuration_version": "1"
}

BIN
.travis/secrets.tar.enc Normal file

Binary file not shown.

View File

@ -1,24 +1,70 @@
| Build | Download | Auto Update |
|-------|----------|-------------|
| [![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-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) [![fdroid debug](https://img.shields.io/badge/dev-F--Droid-blue.svg)](//github.com/inorichi/tachiyomi/wiki/FDroid-for-dev-versions) |
| Build | Stable | Dev | 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=download%20(autoupdate%20included))](https://github.com/inorichi/tachiyomi/releases) | [![latest dev build](https://img.shields.io/badge/download-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest) [![fdroid dev](https://img.shields.io/badge/autoupdate-wiki-blue.svg)](//github.com/inorichi/tachiyomi/wiki/F-Droid-for-dev-versions) | [![Translation status](https://hosted.weblate.org/widgets/tachiyomi/-/svg-badge.svg)](https://hosted.weblate.org/engage/tachiyomi/?utm_source=widget) | [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/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 such as 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).
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/F-Droid-for-dev-versions).
## Issues, Feature Requests and Contributing
Please make sure to read the full guidelines. Your issue may be closed without warning if you do not.
<details><summary>Issues</summary>
1. **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
2. If you are unsure, ask here: [![Discord](https://img.shields.io/discord/349436576037732353.svg)](https://discord.gg/WrBkRk4)
</details>
<details><summary>Bugs</summary>
* Include version (Setting > About > Version)
* If not latest, try updating, it may have already been solved
* Dev version is equal to the number of commits as seen in the main page
* 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)
* For large logs use http://pastebin.com/ (or similar)
* Don't group unrelated requests into one issue
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
DON'T: https://github.com/inorichi/tachiyomi/issues/75
</details>
<details><summary>Feature Requests</summary>
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed)
Catalogue requests should be created at https://github.com/inorichi/tachiyomi-extensions, they do not belong in this repository.
</details>
## 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

3
app/.gitignore vendored
View File

@ -1,4 +1,5 @@
/build
*iml
*.iml
custom.gradle
custom.gradle
google-services.json

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,26 +29,27 @@ ext {
}
android {
compileSdkVersion 25
buildToolsVersion "25.0.1"
compileSdkVersion 27
buildToolsVersion '27.0.3'
publishNonDefault true
defaultConfig {
applicationId "eu.kanade.tachiyomi"
minSdkVersion 16
targetSdkVersion 25
targetSdkVersion 27
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 16
versionName "0.4.0"
versionCode 36
versionName "0.7.3"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
buildConfigField "boolean", "INCLUDE_UPDATER", "false"
vectorDrawables.useSupportLibrary = true
ndk {
abiFilters "armeabi", "armeabi-v7a", "x86"
abiFilters "armeabi-v7a", "arm64-v8a", "x86"
}
}
@ -66,13 +67,20 @@ android {
}
}
flavorDimensions "default"
productFlavors {
standard {
buildConfigField "boolean", "INCLUDE_UPDATER", "true"
dimension "default"
}
fdroid {
buildConfigField "boolean", "INCLUDE_UPDATER", "false"
dimension "default"
}
dev {
minSdkVersion 21
resConfigs "en", "xxhdpi"
dimension "default"
}
}
@ -89,120 +97,144 @@ android {
checkReleaseBuilds false
}
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
}
dependencies {
// Modified dependencies
compile 'com.github.inorichi:subsampling-scale-image-view:f687b74'
implementation 'com.github.inorichi:subsampling-scale-image-view:81b9d68'
implementation 'com.github.inorichi:junrar-android:634c1f5'
// Android support library
final support_library_version = '25.0.1'
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:multidex:1.0.1'
implementation 'com.android.support.constraint:constraint-layout:1.1.0-beta6'
implementation 'com.android.support:multidex:1.0.2'
standardImplementation 'com.google.firebase:firebase-core:12.0.1'
// ReactiveX
compile 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.2.3'
compile 'com.jakewharton.rxrelay:rxrelay:1.2.0'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
compile 'com.github.pwittchen:reactivenetwork:0.6.0'
implementation 'io.reactivex:rxandroid:1.2.1'
implementation 'io.reactivex:rxjava:1.3.6'
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.5.0"
compile 'com.squareup.okio:okio:1.11.0'
implementation "com.squareup.okhttp3:okhttp:3.9.1"
implementation 'com.squareup.okio:okio:1.14.0'
// REST
final retrofit_version = '2.1.0'
compile "com.squareup.retrofit2:retrofit:$retrofit_version"
compile "com.squareup.retrofit2:converter-gson:$retrofit_version"
compile "com.squareup.retrofit2:adapter-rxjava:$retrofit_version"
final retrofit_version = '2.3.0'
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.0'
compile 'com.github.salomonbrys.kotson:kotson:2.4.0'
// YAML
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
implementation 'com.google.code.gson:gson:2.8.2'
implementation 'com.github.salomonbrys.kotson:kotson:2.5.0'
// 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.1'
implementation 'org.jsoup:jsoup:1.10.2'
// Job scheduling
compile 'com.evernote:android-job:1.1.3'
compile 'com.google.android.gms:play-services-gcm:10.0.1'
implementation 'com.evernote:android-job:1.2.4'
implementation 'com.google.android.gms:play-services-gcm:12.0.1'
// 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.11.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-v4:$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 "com.github.inorichi.injekt:injekt-core:65b0440"
// Image library
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
final glide_version = '4.6.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.1'
implementation 'jp.wasabeef:glide-transformations:3.1.1'
// Logging
compile 'com.jakewharton.timber:timber:4.3.1'
implementation 'com.jakewharton.timber:timber:4.6.1'
// Crash reports
compile 'ch.acra:acra:4.9.1'
implementation 'ch.acra:acra:4.9.2'
// Sort
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:4.2.0'
compile 'com.nononsenseapps:filepicker:2.5.2'
compile 'com.github.amulyakhare:TextDrawable:558677e'
compile 'com.afollestad.material-dialogs:core:0.9.1.0'
compile 'net.xpece.android:support-preference:1.2.0'
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
compile 'de.hdodenhof:circleimageview:2.1.0'
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-rc4'
implementation 'eu.davidea:flexible-adapter-ui:1.0.0-b1'
implementation 'com.nononsenseapps:filepicker:2.5.2'
implementation 'com.github.amulyakhare:TextDrawable:558677e'
implementation 'com.afollestad.material-dialogs:core:0.9.6.0'
implementation 'me.zhanghai.android.systemuihelper:library:1.0.0'
implementation 'com.nightlynexus.viewstatepageradapter:viewstatepageradapter:1.0.4'
implementation 'com.github.mthli:Slice:v1.2'
implementation 'me.gujun.android.taggroup:library:1.4@aar'
// Conductor
implementation "com.github.inorichi.Conductor:conductor:be8b3c5"
implementation ("com.bluelinelabs:conductor-support:2.1.5-SNAPSHOT") {
exclude group: "com.bluelinelabs", module: "conductor"
}
implementation 'com.github.inorichi:conductor-support-preference:27.0.2'
// RxBindings
final rxbindings_version = '1.0.1'
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.22.2'
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$coroutines_version"
implementation "org.jetbrains.kotlinx:kotlinx-coroutines-android:$coroutines_version"
}
buildscript {
ext.kotlin_version = '1.0.5-2'
ext.kotlin_version = '1.2.30'
repositories {
mavenCentral()
}
@ -214,3 +246,17 @@ buildscript {
repositories {
mavenCentral()
}
kotlin {
experimental {
coroutines 'enable'
}
}
androidExtensions {
experimental = true
}
if (getGradle().getStartParameter().getTaskRequests().toString().contains("Standard")) {
apply plugin: 'com.google.gms.google-services'
}

View File

@ -1,27 +1,30 @@
-dontobfuscate
-dontwarn eu.kanade.tachiyomi.**
-keep class eu.kanade.tachiyomi.**
-keep class eu.kanade.tachiyomi.source.model.** { *; }
-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
-keepattributes Signature
-keepattributes *Annotation*
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
-dontwarn okio.**
# Okio
-keep class sun.misc.Unsafe { *; }
-dontwarn java.nio.file.*
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.**
-dontwarn javax.annotation.**
-dontwarn retrofit2.Platform$Java8
# Glide specific rules #
# https://github.com/bumptech/glide
-keep public class * implements com.bumptech.glide.module.GlideModule
-keep public class * extends com.bumptech.glide.AppGlideModule
-keep public enum com.bumptech.glide.load.resource.bitmap.ImageHeaderParser$** {
**[] $VALUES;
public *;
@ -43,27 +46,26 @@
rx.internal.util.atomic.LinkedQueueNode consumerNode;
}
# Retrofit 2.X
## https://square.github.io/retrofit/ ##
### Support v7, Design
# http://stackoverflow.com/questions/29679177/cardview-shadow-not-appearing-in-lollipop-after-obfuscate-with-proguard/29698051
-keep class android.support.v7.widget.RoundRectDrawable { *; }
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# AppCombat
-keep public class android.support.v7.widget.** { *; }
-keep public class android.support.v7.internal.widget.** { *; }
-keep public class android.support.v7.internal.view.menu.** { *; }
-keep public class android.support.v7.graphics.drawable.** { *; }
-keep public class * extends android.support.v4.view.ActionProvider {
public <init>(android.content.Context);
}
-dontwarn android.support.**
-dontwarn android.support.design.**
-keep class android.support.design.** { *; }
-keep interface android.support.design.** { *; }
-keep public class android.support.design.R$* { *; }
# ReactiveNetwork
-dontwarn com.github.pwittchen.reactivenetwork.**
@ -73,15 +75,8 @@
# removes such information by default, so configure it to keep all of it.
-keepattributes Signature
# For using GSON @Expose annotation
-keepattributes *Annotation*
# Gson specific classes
-keep class sun.misc.Unsafe { *; }
#-keep class com.google.gson.stream.** { *; }
# Application classes that will be serialized/deserialized over Gson
-keep class com.google.gson.examples.android.model.** { *; }
# Prevent proguard from stripping interface information from TypeAdapterFactory,
# JsonSerializer, JsonDeserializer instances (so they can be used in @JsonAdapter)
@ -91,7 +86,6 @@
# SnakeYaml
-keep class org.yaml.snakeyaml.** { public protected private *; }
-keep class org.yaml.snakeyaml.** { public protected private *; }
-dontwarn org.yaml.snakeyaml.**
# Duktape

47
app/shortcuts.xml Normal file
View File

@ -0,0 +1,47 @@
<shortcuts xmlns:android="http://schemas.android.com/apk/res/android">
<shortcut
android:enabled="true"
android:icon="@drawable/sc_book_48dp"
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_library"
android:shortcutLongLabel="@string/label_library"
android:shortcutShortLabel="@string/label_library">
<intent
android:action="eu.kanade.tachiyomi.SHOW_LIBRARY"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/sc_update_48dp"
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_recently_updated"
android:shortcutLongLabel="@string/label_recent_updates"
android:shortcutShortLabel="@string/short_recent_updates">
<intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_UPDATED"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/sc_glasses_48dp"
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_recently_read"
android:shortcutLongLabel="@string/label_recent_manga"
android:shortcutShortLabel="@string/label_recent_manga">
<intent
android:action="eu.kanade.tachiyomi.SHOW_RECENTLY_READ"
android:targetClass="eu.kanade.tachiyomi.ui.main.MainActivity" />
</shortcut>
<shortcut
android:enabled="true"
android:icon="@drawable/sc_explore_48dp"
android:shortcutDisabledMessage="@string/app_not_available"
android:shortcutId="show_catalogues"
android:shortcutLongLabel="@string/label_catalogues"
android:shortcutShortLabel="@string/label_catalogues">
<intent
android:action="eu.kanade.tachiyomi.SHOW_CATALOGUES"
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

@ -1,16 +1,14 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi">
<uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE"/>
<uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
<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.READ_PHONE_STATE" tools:node="remove" />
<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
@ -18,41 +16,45 @@
android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher"
android:roundIcon="@mipmap/ic_launcher_round"
android:label="@string/app_name"
android:largeHeap="true"
android:theme="@style/Theme.Tachiyomi" >
android:theme="@style/Theme.Tachiyomi">
<activity
android:name=".ui.main.MainActivity">
android:name=".ui.main.MainActivity"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity
android:name=".ui.manga.MangaActivity"
android:parentActivityName=".ui.main.MainActivity"
android:exported="true">
<!--suppress AndroidDomInspection -->
<meta-data android:name="android.app.shortcuts" android:resource="@xml/shortcuts"/>
</activity>
<activity
android:name=".ui.reader.ReaderActivity"
android:theme="@style/Theme.Reader">
</activity>
android:theme="@style/Theme.Reader" />
<activity
android:name=".ui.setting.SettingsActivity"
android:label="@string/label_settings"
android:parentActivityName=".ui.main.MainActivity" >
</activity>
<activity
android:name=".ui.category.CategoryActivity"
android:label="@string/label_categories"
android:parentActivityName=".ui.main.MainActivity">
</activity>
<activity
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
android:name=".widget.CustomLayoutPickerActivity"
android:label="@string/app_name"
android:theme="@style/FilePickerTheme">
android:theme="@style/FilePickerTheme" />
<activity
android:name=".ui.setting.AnilistLoginActivity"
android:label="Anilist">
<intent-filter>
<action android:name="android.intent.action.VIEW" />
<category android:name="android.intent.category.DEFAULT" />
<category android:name="android.intent.category.BROWSABLE" />
<data
android:host="anilist-auth"
android:scheme="tachiyomi" />
</intent-filter>
</activity>
<activity
android:name=".extension.util.ExtensionInstallActivity"
android:theme="@android:style/Theme.Translucent.NoTitleBar"/>
<provider
android:name="android.support.v4.content.FileProvider"
@ -61,31 +63,43 @@
android:grantUriPermissions="true">
<meta-data
android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths"/>
android:resource="@xml/provider_paths" />
</provider>
<service android:name=".data.library.LibraryUpdateService"
<provider
android:name="eu.kanade.tachiyomi.util.ZipContentProvider"
android:authorities="${applicationId}.zip-provider"
android:exported="false" />
<provider
android:name="eu.kanade.tachiyomi.util.RarContentProvider"
android:authorities="${applicationId}.rar-provider"
android:exported="false" />
<receiver
android:name=".data.notification.NotificationReceiver"
android:exported="false" />
<service
android:name=".data.library.LibraryUpdateService"
android:exported="false" />
<service
android:name=".data.download.DownloadService"
android:exported="false" />
<service
android:name=".data.updater.UpdaterService"
android:exported="false" />
<service
android:name=".data.backup.BackupCreateService"
android:exported="false"/>
<service android:name=".data.download.DownloadService"
<service
android:name=".data.backup.BackupRestoreService"
android:exported="false"/>
<service android:name=".data.mangasync.UpdateMangaSyncService"
android:exported="false"/>
<service android:name=".data.updater.UpdateDownloaderService"
android:exported="false"/>
<receiver android:name=".data.updater.UpdateNotificationReceiver"/>
<receiver android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver" />
<receiver android:name=".ui.reader.notification.ImageNotificationReceiver" />
<meta-data
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
android:value="GlideModule" />
</application>
</manifest>

Binary file not shown.

Before

Width:  |  Height:  |  Size: 25 KiB

After

Width:  |  Height:  |  Size: 22 KiB

View File

@ -2,10 +2,14 @@ package eu.kanade.tachiyomi
import android.app.Application
import android.content.Context
import android.content.res.Configuration
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
import timber.log.Timber
@ -18,19 +22,22 @@ import uy.kohesive.injekt.registry.default.DefaultRegistrar
reportType = org.acra.sender.HttpSender.Type.JSON,
httpMethod = org.acra.sender.HttpSender.Method.PUT,
buildConfigClass = BuildConfig::class,
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*")
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*", ".*token.*")
)
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)
}
override fun attachBaseContext(base: Context) {
@ -40,6 +47,11 @@ open class App : Application() {
}
}
override fun onConfigurationChanged(newConfig: Configuration) {
super.onConfigurationChanged(newConfig)
LocaleHelper.updateConfiguration(this, newConfig, true)
}
protected open fun setupAcra() {
ACRA.init(this)
}
@ -48,10 +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

@ -6,36 +6,57 @@ import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.cache.CoverCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager
import uy.kohesive.injekt.api.InjektModule
import uy.kohesive.injekt.api.InjektRegistrar
import uy.kohesive.injekt.api.addSingletonFactory
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.extension.ExtensionManager
import eu.kanade.tachiyomi.network.NetworkHelper
import eu.kanade.tachiyomi.source.SourceManager
import rx.Observable
import rx.schedulers.Schedulers
import uy.kohesive.injekt.api.*
class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingletonFactory { PreferencesHelper(app) }
addSingleton(app)
addSingletonFactory { DatabaseHelper(app) }
addSingletonFactory { PreferencesHelper(app) }
addSingletonFactory { ChapterCache(app) }
addSingletonFactory { DatabaseHelper(app) }
addSingletonFactory { CoverCache(app) }
addSingletonFactory { ChapterCache(app) }
addSingletonFactory { NetworkHelper(app) }
addSingletonFactory { CoverCache(app) }
addSingletonFactory { SourceManager(app) }
addSingletonFactory { NetworkHelper(app) }
addSingletonFactory { DownloadManager(app) }
addSingletonFactory { SourceManager(app).also { get<ExtensionManager>().init(it) } }
addSingletonFactory { MangaSyncManager(app) }
addSingletonFactory { ExtensionManager(app) }
addSingletonFactory { Gson() }
addSingletonFactory { DownloadManager(app) }
addSingletonFactory { TrackManager(app) }
addSingletonFactory { Gson() }
// Asynchronously init expensive components for a faster cold start
rxAsync { get<PreferencesHelper>() }
rxAsync { get<NetworkHelper>() }
rxAsync { get<SourceManager>() }
rxAsync { get<DatabaseHelper>() }
rxAsync { get<DownloadManager>() }
}
}
private fun rxAsync(block: () -> Unit) {
Observable.fromCallable { block() }.subscribeOn(Schedulers.computation()).subscribe()
}
}

View File

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

View File

@ -0,0 +1,63 @@
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.UpdaterJob
import java.io.File
object Migrations {
/**
* Performs a migration when the application is updated.
*
* @param preferences Preferences of the application.
* @return true if a migration is performed, false otherwise.
*/
fun upgrade(preferences: PreferencesHelper): Boolean {
val context = preferences.context
val oldVersion = preferences.lastVersionCode().getOrDefault()
if (oldVersion < BuildConfig.VERSION_CODE) {
preferences.lastVersionCode().set(BuildConfig.VERSION_CODE)
if (oldVersion == 0) return false
if (oldVersion < 14) {
// Restore jobs after upgrading to evernote's job scheduler.
if (BuildConfig.INCLUDE_UPDATER && preferences.automaticUpdates()) {
UpdaterJob.setupTask()
}
LibraryUpdateJob.setupTask()
}
if (oldVersion < 15) {
// Delete internal chapter cache dir.
File(context.cacheDir, "chapter_disk_cache").deleteRecursively()
}
if (oldVersion < 19) {
// Move covers to external files dir.
val oldDir = File(context.externalCacheDir, "cover_disk_cache")
if (oldDir.exists()) {
val destDir = context.getExternalFilesDir("covers")
if (destDir != null) {
oldDir.listFiles().forEach {
it.renameTo(File(destDir, it.name))
}
}
}
}
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

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.data.backup
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
object BackupConst {
const val INTENT_FILTER = "SettingsBackupFragment"
const val ACTION_BACKUP_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_BACKUP_COMPLETED_DIALOG"
const val ACTION_SET_PROGRESS_DIALOG = "$ID.$INTENT_FILTER.ACTION_SET_PROGRESS_DIALOG"
const val ACTION_ERROR_BACKUP_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_BACKUP_DIALOG"
const val ACTION_ERROR_RESTORE_DIALOG = "$ID.$INTENT_FILTER.ACTION_ERROR_RESTORE_DIALOG"
const val ACTION_RESTORE_COMPLETED_DIALOG = "$ID.$INTENT_FILTER.ACTION_RESTORE_COMPLETED_DIALOG"
const val ACTION = "$ID.$INTENT_FILTER.ACTION"
const val EXTRA_PROGRESS = "$ID.$INTENT_FILTER.EXTRA_PROGRESS"
const val EXTRA_AMOUNT = "$ID.$INTENT_FILTER.EXTRA_AMOUNT"
const val EXTRA_ERRORS = "$ID.$INTENT_FILTER.EXTRA_ERRORS"
const val EXTRA_CONTENT = "$ID.$INTENT_FILTER.EXTRA_CONTENT"
const val EXTRA_ERROR_MESSAGE = "$ID.$INTENT_FILTER.EXTRA_ERROR_MESSAGE"
const val EXTRA_URI = "$ID.$INTENT_FILTER.EXTRA_URI"
const val EXTRA_TIME = "$ID.$INTENT_FILTER.EXTRA_TIME"
const val EXTRA_ERROR_FILE_PATH = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE_PATH"
const val EXTRA_ERROR_FILE = "$ID.$INTENT_FILTER.EXTRA_ERROR_FILE"
}

View File

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.data.backup
import android.app.IntentService
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.google.gson.JsonArray
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.BuildConfig.APPLICATION_ID as ID
/**
* [IntentService] used to backup [Manga] information to [JsonArray]
*/
class BackupCreateService : IntentService(NAME) {
companion object {
// Name of class
private const val NAME = "BackupCreateService"
// Options for backup
private const val EXTRA_FLAGS = "$ID.$NAME.EXTRA_FLAGS"
// Filter options
internal const val BACKUP_CATEGORY = 0x1
internal const val BACKUP_CATEGORY_MASK = 0x1
internal const val BACKUP_CHAPTER = 0x2
internal const val BACKUP_CHAPTER_MASK = 0x2
internal const val BACKUP_HISTORY = 0x4
internal const val BACKUP_HISTORY_MASK = 0x4
internal const val BACKUP_TRACK = 0x8
internal const val BACKUP_TRACK_MASK = 0x8
internal const val BACKUP_ALL = 0xF
/**
* Make a backup from library
*
* @param context context of application
* @param uri path of Uri
* @param flags determines what to backup
*/
fun makeBackup(context: Context, uri: Uri, flags: Int) {
val intent = Intent(context, BackupCreateService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri)
putExtra(EXTRA_FLAGS, flags)
}
context.startService(intent)
}
}
private val backupManager by lazy { BackupManager(this) }
override fun onHandleIntent(intent: Intent?) {
if (intent == null) return
// Get values
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
val flags = intent.getIntExtra(EXTRA_FLAGS, 0)
// Create backup
backupManager.createBackup(uri, flags, false)
}
}

View File

@ -0,0 +1,42 @@
package eu.kanade.tachiyomi.data.backup
import android.net.Uri
import com.evernote.android.job.Job
import com.evernote.android.job.JobManager
import com.evernote.android.job.JobRequest
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
class BackupCreatorJob : Job() {
override fun onRunJob(params: Params): Result {
val preferences = Injekt.get<PreferencesHelper>()
val backupManager = BackupManager(context)
val uri = Uri.parse(preferences.backupsDirectory().getOrDefault())
val flags = BackupCreateService.BACKUP_ALL
backupManager.createBackup(uri, flags, true)
return Result.SUCCESS
}
companion object {
const val TAG = "BackupCreator"
fun setupTask(prefInterval: Int? = null) {
val preferences = Injekt.get<PreferencesHelper>()
val interval = prefInterval ?: preferences.backupInterval().getOrDefault()
if (interval > 0) {
JobRequest.Builder(TAG)
.setPeriodic(interval * 60 * 60 * 1000L, 10 * 60 * 1000)
.setUpdateCurrent(true)
.build()
.schedule()
}
}
fun cancelTask() {
JobManager.instance().cancelAllForTag(TAG)
}
}
}

View File

@ -1,203 +1,308 @@
package eu.kanade.tachiyomi.data.backup
import com.github.salomonbrys.kotson.fromJson
import android.content.Context
import android.content.Intent
import android.net.Uri
import com.github.salomonbrys.kotson.*
import com.google.gson.*
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.data.backup.serializer.BooleanSerializer
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
import eu.kanade.tachiyomi.data.backup.serializer.LongSerializer
import com.hippo.unifile.UniFile
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CATEGORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_CHAPTER_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_HISTORY_MASK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK
import eu.kanade.tachiyomi.data.backup.BackupCreateService.Companion.BACKUP_TRACK_MASK
import eu.kanade.tachiyomi.data.backup.models.Backup
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
import eu.kanade.tachiyomi.data.backup.models.Backup.CURRENT_VERSION
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.backup.serializer.*
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.*
import java.io.*
import java.util.*
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.sendLocalBroadcast
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import rx.Observable
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
/**
* This class provides the necessary methods to create and restore backups for the data of the
* application. The backup follows a JSON structure, with the following scheme:
*
* {
* "mangas": [
* {
* "manga": {"id": 1, ...},
* "chapters": [{"id": 1, ...}, {...}],
* "sync": [{"id": 1, ...}, {...}],
* "categories": ["cat1", "cat2", ...]
* },
* { ... }
* ],
* "categories": [
* {"id": 1, ...},
* {"id": 2, ...}
* ]
* }
*
* @param db the database helper.
*/
class BackupManager(private val db: DatabaseHelper) {
private val MANGA = "manga"
private val MANGAS = "mangas"
private val CHAPTERS = "chapters"
private val MANGA_SYNC = "sync"
private val CATEGORIES = "categories"
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
private val gson = GsonBuilder()
.registerTypeAdapter(java.lang.Integer::class.java, IntegerSerializer())
.registerTypeAdapter(java.lang.Boolean::class.java, BooleanSerializer())
.registerTypeAdapter(java.lang.Long::class.java, LongSerializer())
.setExclusionStrategies(IdExclusion())
.create()
class BackupManager(val context: Context, version: Int = CURRENT_VERSION) {
/**
* Backups the data of the application to a file.
*
* @param file the file where the backup will be saved.
* @throws IOException if there's any IO error.
* Database.
*/
@Throws(IOException::class)
fun backupToFile(file: File) {
val root = backupToJson()
internal val databaseHelper: DatabaseHelper by injectLazy()
FileWriter(file).use {
gson.toJson(root, it)
}
/**
* Source manager.
*/
internal val sourceManager: SourceManager by injectLazy()
/**
* Tracking manager
*/
internal val trackManager: TrackManager by injectLazy()
/**
* Version of parser
*/
var version: Int = version
private set
/**
* Json Parser
*/
var parser: Gson = initParser()
/**
* Preferences
*/
private val preferences: PreferencesHelper by injectLazy()
/**
* Set version of parser
*
* @param version version of parser
*/
internal fun setVersion(version: Int) {
this.version = version
parser = initParser()
}
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")
}
/**
* Creates a JSON object containing the backup of the app's data.
* Create backup Json file from database
*
* @return the backup as a JSON object.
* @param uri path of Uri
* @param isJob backup called from job
*/
fun backupToJson(): JsonObject {
fun createBackup(uri: Uri, flags: Int, isJob: Boolean) {
// Create root object
val root = JsonObject()
// Backup library mangas and its dependencies
// Create manga array
val mangaEntries = JsonArray()
root.add(MANGAS, mangaEntries)
for (manga in db.getFavoriteMangas().executeAsBlocking()) {
mangaEntries.add(backupManga(manga))
}
// Backup categories
// Create category array
val categoryEntries = JsonArray()
root.add(CATEGORIES, categoryEntries)
for (category in db.getCategories().executeAsBlocking()) {
categoryEntries.add(backupCategory(category))
// Add value's to root
root[Backup.VERSION] = Backup.CURRENT_VERSION
root[Backup.MANGAS] = mangaEntries
root[CATEGORIES] = categoryEntries
databaseHelper.inTransaction {
// Get manga from database
val mangas = getFavoriteManga()
// Backup library manga and its dependencies
mangas.forEach { manga ->
mangaEntries.add(backupMangaObject(manga, flags))
}
// Backup categories
if ((flags and BACKUP_CATEGORY_MASK) == BACKUP_CATEGORY) {
backupCategories(categoryEntries)
}
}
return root
try {
// When BackupCreatorJob
if (isJob) {
// Get dir of file and create
var dir = UniFile.fromUri(context, uri)
dir = dir.createDirectory("automatic")
// Delete older backups
val numberOfBackups = numberOfBackups()
val backupRegex = Regex("""tachiyomi_\d+-\d+-\d+_\d+-\d+.json""")
dir.listFiles { _, filename -> backupRegex.matches(filename) }
.orEmpty()
.sortedByDescending { it.name }
.drop(numberOfBackups - 1)
.forEach { it.delete() }
// Create new file to place backup
val newFile = dir.createFile(Backup.getDefaultFilename())
?: throw Exception("Couldn't create backup file")
newFile.openOutputStream().bufferedWriter().use {
parser.toJson(root, it)
}
} else {
val file = UniFile.fromUri(context, uri)
?: throw Exception("Couldn't create backup file")
file.openOutputStream().bufferedWriter().use {
parser.toJson(root, it)
}
// Show completed dialog
val intent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.ACTION, BackupConst.ACTION_BACKUP_COMPLETED_DIALOG)
putExtra(BackupConst.EXTRA_URI, file.uri.toString())
}
context.sendLocalBroadcast(intent)
}
} catch (e: Exception) {
Timber.e(e)
if (!isJob) {
// Show error dialog
val intent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_BACKUP_DIALOG)
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, e.message)
}
context.sendLocalBroadcast(intent)
}
}
}
/**
* Backups a manga and its related data (chapters, categories this manga is in, sync...).
* Backup the categories of library
*
* @param manga the manga to backup.
* @return a JSON object containing all the data of the manga.
* @param root root of categories json
*/
private fun backupManga(manga: Manga): JsonObject {
internal fun backupCategories(root: JsonArray) {
val categories = databaseHelper.getCategories().executeAsBlocking()
categories.forEach { root.add(parser.toJsonTree(it)) }
}
/**
* Convert a manga to Json
*
* @param manga manga that gets converted
* @return [JsonElement] containing manga information
*/
internal fun backupMangaObject(manga: Manga, options: Int): JsonElement {
// Entry for this manga
val entry = JsonObject()
// Backup manga fields
entry.add(MANGA, gson.toJsonTree(manga))
entry[MANGA] = parser.toJsonTree(manga)
// Backup all the chapters
val chapters = db.getChapters(manga).executeAsBlocking()
if (!chapters.isEmpty()) {
entry.add(CHAPTERS, gson.toJsonTree(chapters))
}
// Backup manga sync
val mangaSync = db.getMangasSync(manga).executeAsBlocking()
if (!mangaSync.isEmpty()) {
entry.add(MANGA_SYNC, gson.toJsonTree(mangaSync))
}
// Backup categories for this manga
val categoriesForManga = db.getCategoriesForManga(manga).executeAsBlocking()
if (!categoriesForManga.isEmpty()) {
val categoriesNames = ArrayList<String>()
for (category in categoriesForManga) {
categoriesNames.add(category.name)
// Check if user wants chapter information in backup
if (options and BACKUP_CHAPTER_MASK == BACKUP_CHAPTER) {
// Backup all the chapters
val chapters = databaseHelper.getChapters(manga).executeAsBlocking()
if (!chapters.isEmpty()) {
val chaptersJson = parser.toJsonTree(chapters)
if (chaptersJson.asJsonArray.size() > 0) {
entry[CHAPTERS] = chaptersJson
}
}
}
// Check if user wants category information in backup
if (options and BACKUP_CATEGORY_MASK == BACKUP_CATEGORY) {
// Backup categories for this manga
val categoriesForManga = databaseHelper.getCategoriesForManga(manga).executeAsBlocking()
if (!categoriesForManga.isEmpty()) {
val categoriesNames = categoriesForManga.map { it.name }
entry[CATEGORIES] = parser.toJsonTree(categoriesNames)
}
}
// Check if user wants track information in backup
if (options and BACKUP_TRACK_MASK == BACKUP_TRACK) {
val tracks = databaseHelper.getTracks(manga).executeAsBlocking()
if (!tracks.isEmpty()) {
entry[TRACK] = parser.toJsonTree(tracks)
}
}
// Check if user wants history information in backup
if (options and BACKUP_HISTORY_MASK == BACKUP_HISTORY) {
val historyForManga = databaseHelper.getHistoryByMangaId(manga.id!!).executeAsBlocking()
if (!historyForManga.isEmpty()) {
val historyData = historyForManga.mapNotNull { history ->
val url = databaseHelper.getChapter(history.chapter_id).executeAsBlocking()?.url
url?.let { DHistory(url, history.last_read) }
}
val historyJson = parser.toJsonTree(historyData)
if (historyJson.asJsonArray.size() > 0) {
entry[HISTORY] = historyJson
}
}
entry.add(CATEGORIES, gson.toJsonTree(categoriesNames))
}
return entry
}
/**
* Backups a category.
*
* @param category the category to backup.
* @return a JSON object containing the data of the category.
*/
private fun backupCategory(category: Category): JsonElement {
return gson.toJsonTree(category)
fun restoreMangaNoFetch(manga: Manga, dbManga: Manga) {
manga.id = dbManga.id
manga.copyFrom(dbManga)
manga.favorite = true
insertManga(manga)
}
/**
* Restores a backup from a file.
* [Observable] that fetches manga information
*
* @param file the file containing the backup.
* @throws IOException if there's any IO error.
* @param source source of manga
* @param manga manga that needs updating
* @return [Observable] that contains manga
*/
@Throws(IOException::class)
fun restoreFromFile(file: File) {
JsonReader(FileReader(file)).use {
val root = JsonParser().parse(it).asJsonObject
restoreFromJson(root)
}
fun restoreMangaFetchObservable(source: Source, manga: Manga): Observable<Manga> {
return source.fetchMangaDetails(manga)
.map { networkManga ->
manga.copyFrom(networkManga)
manga.favorite = true
manga.initialized = true
manga.id = insertManga(manga)
manga
}
}
/**
* Restores a backup from an input stream.
* [Observable] that fetches chapter information
*
* @param stream the stream containing the backup.
* @throws IOException if there's any IO error.
* @param source source of manga
* @param manga manga that needs updating
* @return [Observable] that contains manga
*/
@Throws(IOException::class)
fun restoreFromStream(stream: InputStream) {
JsonReader(InputStreamReader(stream)).use {
val root = JsonParser().parse(it).asJsonObject
restoreFromJson(root)
}
fun restoreChapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return source.fetchChapterList(manga)
.map { syncChaptersWithSource(databaseHelper, it, manga, source) }
.doOnNext {
if (it.first.isNotEmpty()) {
chapters.forEach { it.manga_id = manga.id }
insertChapters(chapters)
}
}
}
/**
* Restores a backup from a JSON object. Everything executes in a single transaction so that
* nothing is modified if there's an error.
* Restore the categories from Json
*
* @param root the root of the JSON.
* @param jsonCategories array containing categories
*/
fun restoreFromJson(root: JsonObject) {
db.inTransaction {
// Restore categories
root.get(CATEGORIES)?.let {
restoreCategories(it.asJsonArray)
}
// Restore mangas
root.get(MANGAS)?.let {
restoreMangas(it.asJsonArray)
}
}
}
/**
* Restores the categories.
*
* @param jsonCategories the categories of the json.
*/
private fun restoreCategories(jsonCategories: JsonArray) {
internal fun restoreCategories(jsonCategories: JsonArray) {
// Get categories from file and from db
val dbCategories = db.getCategories().executeAsBlocking()
val backupCategories = gson.fromJson<List<CategoryImpl>>(jsonCategories)
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
val backupCategories = parser.fromJson<List<CategoryImpl>>(jsonCategories)
// Iterate over them
for (category in backupCategories) {
backupCategories.forEach { category ->
// Used to know if the category is already in the db
var found = false
for (dbCategory in dbCategories) {
@ -214,102 +319,20 @@ class BackupManager(private val db: DatabaseHelper) {
if (!found) {
// Let the db assign the id
category.id = null
val result = db.insertCategory(category).executeAsBlocking()
val result = databaseHelper.insertCategory(category).executeAsBlocking()
category.id = result.insertedId()?.toInt()
}
}
}
/**
* Restores all the mangas and its related data.
*
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
*/
private fun restoreMangas(jsonMangas: JsonArray) {
for (backupManga in jsonMangas) {
// Map every entry to objects
val element = backupManga.asJsonObject
val manga = gson.fromJson(element.get(MANGA), MangaImpl::class.java)
val chapters = gson.fromJson<List<ChapterImpl>>(element.get(CHAPTERS) ?: JsonArray())
val sync = gson.fromJson<List<MangaSyncImpl>>(element.get(MANGA_SYNC) ?: JsonArray())
val categories = gson.fromJson<List<String>>(element.get(CATEGORIES) ?: JsonArray())
// Restore everything related to this manga
restoreManga(manga)
restoreChaptersForManga(manga, chapters)
restoreSyncForManga(manga, sync)
restoreCategoriesForManga(manga, categories)
}
}
/**
* Restores a manga.
*
* @param manga the manga to restore.
*/
private fun restoreManga(manga: Manga) {
// Try to find existing manga in db
val dbManga = db.getManga(manga.url, manga.source).executeAsBlocking()
if (dbManga == null) {
// Let the db assign the id
manga.id = null
val result = db.insertManga(manga).executeAsBlocking()
manga.id = result.insertedId()
} else {
// If it exists already, we copy only the values related to the source from the db
// (they can be up to date). Local values (flags) are kept from the backup.
manga.id = dbManga.id
manga.copyFrom(dbManga)
manga.favorite = true
db.insertManga(manga).executeAsBlocking()
}
}
/**
* Restores the chapters of a manga.
*
* @param manga the manga whose chapters have to be restored.
* @param chapters the chapters to restore.
*/
private fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>) {
// Fix foreign keys with the current manga id
for (chapter in chapters) {
chapter.manga_id = manga.id
}
val dbChapters = db.getChapters(manga).executeAsBlocking()
val chaptersToUpdate = ArrayList<Chapter>()
for (backupChapter in chapters) {
// Try to find existing chapter in db
val pos = dbChapters.indexOf(backupChapter)
if (pos != -1) {
// The chapter is already in the db, only update its fields
val dbChapter = dbChapters[pos]
// If one of them was read, the chapter will be marked as read
dbChapter.read = backupChapter.read || dbChapter.read
dbChapter.last_page_read = Math.max(backupChapter.last_page_read, dbChapter.last_page_read)
chaptersToUpdate.add(dbChapter)
} else {
// Insert new chapter. Let the db assign the id
backupChapter.id = null
chaptersToUpdate.add(backupChapter)
}
}
// Update database
if (!chaptersToUpdate.isEmpty()) {
db.insertChapters(chaptersToUpdate).executeAsBlocking()
}
}
/**
* Restores the categories a manga is in.
*
* @param manga the manga whose categories have to be restored.
* @param categories the categories to restore.
*/
private fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
val dbCategories = db.getCategories().executeAsBlocking()
internal fun restoreCategoriesForManga(manga: Manga, categories: List<String>) {
val dbCategories = databaseHelper.getCategories().executeAsBlocking()
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
for (backupCategoryStr in categories) {
for (dbCategory in dbCategories) {
@ -324,45 +347,149 @@ class BackupManager(private val db: DatabaseHelper) {
if (!mangaCategoriesToUpdate.isEmpty()) {
val mangaAsList = ArrayList<Manga>()
mangaAsList.add(manga)
db.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
databaseHelper.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
databaseHelper.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
}
}
/**
* Restore history from Json
*
* @param history list containing history to be restored
*/
internal fun restoreHistoryForManga(history: List<DHistory>) {
// List containing history to be updated
val historyToBeUpdated = ArrayList<History>()
for ((url, lastRead) in history) {
val dbHistory = databaseHelper.getHistoryByChapterUrl(url).executeAsBlocking()
// Check if history already in database and update
if (dbHistory != null) {
dbHistory.apply {
last_read = Math.max(lastRead, dbHistory.last_read)
}
historyToBeUpdated.add(dbHistory)
} else {
// If not in database create
databaseHelper.getChapter(url).executeAsBlocking()?.let {
val historyToAdd = History.create(it).apply {
last_read = lastRead
}
historyToBeUpdated.add(historyToAdd)
}
}
}
databaseHelper.updateHistoryLastRead(historyToBeUpdated).executeAsBlocking()
}
/**
* Restores the sync of a manga.
*
* @param manga the manga whose sync have to be restored.
* @param sync the sync to restore.
* @param tracks the track list to restore.
*/
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
internal fun restoreTrackForManga(manga: Manga, tracks: List<Track>) {
// Fix foreign keys with the current manga id
for (mangaSync in sync) {
mangaSync.manga_id = manga.id!!
}
tracks.map { it.manga_id = manga.id!! }
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
val syncToUpdate = ArrayList<MangaSync>()
for (backupSync in sync) {
// Try to find existing chapter in db
val pos = dbSyncs.indexOf(backupSync)
if (pos != -1) {
// The sync is already in the db, only update its fields
val dbSync = dbSyncs[pos]
// Mark the max chapter as read and nothing else
dbSync.last_chapter_read = Math.max(backupSync.last_chapter_read, dbSync.last_chapter_read)
syncToUpdate.add(dbSync)
} else {
// Insert new sync. Let the db assign the id
backupSync.id = null
syncToUpdate.add(backupSync)
// Get tracks from database
val dbTracks = databaseHelper.getTracks(manga).executeAsBlocking()
val trackToUpdate = ArrayList<Track>()
for (track in tracks) {
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
}
}
if (!isInDatabase) {
// Insert new sync. Let the db assign the id
track.id = null
trackToUpdate.add(track)
}
}
}
// Update database
if (!syncToUpdate.isEmpty()) {
db.insertMangasSync(syncToUpdate).executeAsBlocking()
if (!trackToUpdate.isEmpty()) {
databaseHelper.insertTracks(trackToUpdate).executeAsBlocking()
}
}
/**
* Restore the chapters for manga if chapters already in database
*
* @param manga manga of chapters
* @param chapters list containing chapters that get restored
* @return boolean answering if chapter fetch is not needed
*/
internal fun restoreChaptersForManga(manga: Manga, chapters: List<Chapter>): Boolean {
val dbChapters = databaseHelper.getChapters(manga).executeAsBlocking()
// Return if fetch is needed
if (dbChapters.isEmpty() || dbChapters.size < chapters.size)
return false
for (chapter in chapters) {
val pos = dbChapters.indexOf(chapter)
if (pos != -1) {
val dbChapter = dbChapters[pos]
chapter.id = dbChapter.id
chapter.copyFrom(dbChapter)
break
}
}
// Filter the chapters that couldn't be found.
chapters.filter { it.id != null }
chapters.map { it.manga_id = manga.id }
insertChapters(chapters)
return true
}
/**
* Returns manga
*
* @return [Manga], null if not found
*/
internal fun getMangaFromDatabase(manga: Manga): Manga? =
databaseHelper.getManga(manga.url, manga.source).executeAsBlocking()
/**
* Returns list containing manga from library
*
* @return [Manga] from library
*/
internal fun getFavoriteManga(): List<Manga> =
databaseHelper.getFavoriteMangas().executeAsBlocking()
/**
* Inserts manga and returns id
*
* @return id of [Manga], null if not found
*/
internal fun insertManga(manga: Manga): Long? =
databaseHelper.insertManga(manga).executeAsBlocking().insertedId()
/**
* Inserts list of chapters
*/
private fun insertChapters(chapters: List<Chapter>) {
databaseHelper.updateChaptersBackup(chapters).executeAsBlocking()
}
/**
* Return number of backups.
*
* @return number of backups selected by user
*/
fun numberOfBackups(): Int = preferences.numberOfBackups().getOrDefault()
}

View File

@ -0,0 +1,444 @@
package eu.kanade.tachiyomi.data.backup
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.Uri
import android.os.IBinder
import android.os.PowerManager
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.JsonArray
import com.google.gson.JsonParser
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.backup.models.Backup.CATEGORIES
import eu.kanade.tachiyomi.data.backup.models.Backup.CHAPTERS
import eu.kanade.tachiyomi.data.backup.models.Backup.HISTORY
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGA
import eu.kanade.tachiyomi.data.backup.models.Backup.MANGAS
import eu.kanade.tachiyomi.data.backup.models.Backup.TRACK
import eu.kanade.tachiyomi.data.backup.models.Backup.VERSION
import eu.kanade.tachiyomi.data.backup.models.DHistory
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.*
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
import eu.kanade.tachiyomi.util.sendLocalBroadcast
import rx.Observable
import rx.Subscription
import rx.schedulers.Schedulers
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.io.File
import java.text.SimpleDateFormat
import java.util.*
import java.util.concurrent.ExecutorService
import java.util.concurrent.Executors
/**
* Restores backup from json file
*/
class BackupRestoreService : Service() {
companion object {
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
private fun isRunning(context: Context): Boolean =
context.isServiceRunning(BackupRestoreService::class.java)
/**
* Starts a service to restore a backup from Json
*
* @param context context of application
* @param uri path of Uri
*/
fun start(context: Context, uri: Uri) {
if (!isRunning(context)) {
val intent = Intent(context, BackupRestoreService::class.java).apply {
putExtra(BackupConst.EXTRA_URI, uri)
}
context.startService(intent)
}
}
/**
* Stops the service.
*
* @param context the application context.
*/
fun stop(context: Context) {
context.stopService(Intent(context, BackupRestoreService::class.java))
}
}
/**
* Wake lock that will be held until the service is destroyed.
*/
private lateinit var wakeLock: PowerManager.WakeLock
/**
* Subscription where the update is done.
*/
private var subscription: Subscription? = null
/**
* The progress of a backup restore
*/
private var restoreProgress = 0
/**
* Amount of manga in Json file (needed for restore)
*/
private var restoreAmount = 0
/**
* List containing errors
*/
private val errors = mutableListOf<Pair<Date, String>>()
/**
* Backup manager
*/
private lateinit var backupManager: BackupManager
/**
* Database
*/
private val db: DatabaseHelper by injectLazy()
/**
* 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.
*/
override fun onCreate() {
super.onCreate()
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "BackupRestoreService:WakeLock")
wakeLock.acquire()
executor = Executors.newSingleThreadExecutor()
}
/**
* Method called when the service is destroyed. It destroys the running subscription and
* releases the wake lock.
*/
override fun onDestroy() {
subscription?.unsubscribe()
executor.shutdown() // must be called after unsubscribe
if (wakeLock.isHeld) {
wakeLock.release()
}
super.onDestroy()
}
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? = null
/**
* Method called when the service receives an intent.
*
* @param intent the start intent from.
* @param flags the flags of the command.
* @param startId the start id of this command.
* @return the start value of the command.
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return Service.START_NOT_STICKY
val uri = intent.getParcelableExtra<Uri>(BackupConst.EXTRA_URI)
// Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe()
subscription = Observable.using(
{ db.lowLevel().beginTransaction() },
{ getRestoreObservable(uri).doOnNext { db.lowLevel().setTransactionSuccessful() } },
{ executor.execute { db.lowLevel().endTransaction() } })
.doAfterTerminate { stopSelf(startId) }
.subscribeOn(Schedulers.from(executor))
.subscribe()
return Service.START_NOT_STICKY
}
/**
* Returns an [Observable] containing restore process.
*
* @param uri restore file
* @return [Observable<Manga>]
*/
private fun getRestoreObservable(uri: Uri): Observable<List<Manga>> {
val startTime = System.currentTimeMillis()
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
// Initialize manager
backupManager = BackupManager(this, version)
val mangasJson = json.get(MANGAS).asJsonArray
restoreAmount = mangasJson.size() + 1 // +1 for categories
restoreProgress = 0
errors.clear()
// Restore categories
json.get(CATEGORIES)?.let {
backupManager.restoreCategories(it.asJsonArray)
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, "Categories added", errors.size)
}
mangasJson
}
.flatMap { Observable.from(it) }
.concatMap {
val obj = it.asJsonObject
val manga = backupManager.parser.fromJson<MangaImpl>(obj.get(MANGA))
val chapters = backupManager.parser.fromJson<List<ChapterImpl>>(obj.get(CHAPTERS) ?: JsonArray())
val categories = backupManager.parser.fromJson<List<String>>(obj.get(CATEGORIES) ?: JsonArray())
val history = backupManager.parser.fromJson<List<DHistory>>(obj.get(HISTORY) ?: JsonArray())
val tracks = backupManager.parser.fromJson<List<TrackImpl>>(obj.get(TRACK) ?: JsonArray())
val observable = getMangaRestoreObservable(manga, chapters, categories, history, tracks)
if (observable != null) {
observable
} else {
errors.add(Date() to "${manga.title} - ${getString(R.string.source_not_found)}")
restoreProgress += 1
val content = getString(R.string.dialog_restoring_source_not_found, manga.title.chop(15))
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size, content)
Observable.just(manga)
}
}
.toList()
.doOnNext {
val endTime = System.currentTimeMillis()
val time = endTime - startTime
val logFile = writeErrorLog()
val completeIntent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.EXTRA_TIME, time)
putExtra(BackupConst.EXTRA_ERRORS, errors.size)
putExtra(BackupConst.EXTRA_ERROR_FILE_PATH, logFile.parent)
putExtra(BackupConst.EXTRA_ERROR_FILE, logFile.name)
putExtra(BackupConst.ACTION, BackupConst.ACTION_RESTORE_COMPLETED_DIALOG)
}
sendLocalBroadcast(completeIntent)
}
.doOnError { error ->
Timber.e(error)
writeErrorLog()
val errorIntent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.ACTION, BackupConst.ACTION_ERROR_RESTORE_DIALOG)
putExtra(BackupConst.EXTRA_ERROR_MESSAGE, error.message)
}
sendLocalBroadcast(errorIntent)
}
.onErrorReturn { emptyList() }
}
/**
* Write errors to error log
*/
private fun writeErrorLog(): File {
try {
if (errors.isNotEmpty()) {
val destFile = File(externalCacheDir, "tachiyomi_restore.log")
val sdf = SimpleDateFormat("yyyy-MM-dd HH:mm:ss.SSS", Locale.getDefault())
destFile.bufferedWriter().use { out ->
errors.forEach { (date, message) ->
out.write("[${sdf.format(date)}] $message\n")
}
}
return destFile
}
} catch (e: Exception) {
// Empty
}
return File("")
}
/**
* Returns a manga restore observable
*
* @param manga manga data from json
* @param chapters chapters data from json
* @param categories categories data from json
* @param history history data from json
* @param tracks tracking data from json
* @return [Observable] containing manga restore information
*/
private fun getMangaRestoreObservable(manga: Manga, chapters: List<Chapter>,
categories: List<String>, history: List<DHistory>,
tracks: List<Track>): Observable<Manga>? {
// Get source
val source = backupManager.sourceManager.get(manga.source) ?: return null
val dbManga = backupManager.getMangaFromDatabase(manga)
return if (dbManga == null) {
// Manga not in database
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
mangaNoFetchObservable(source, manga, chapters, categories, history, tracks)
}
}
/**
* [Observable] that fetches manga information
*
* @param manga manga that needs updating
* @param chapters chapters of manga that needs updating
* @param categories categories that need updating
*/
private fun mangaFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>,
categories: List<String>, history: List<DHistory>,
tracks: List<Track>): Observable<Manga> {
return backupManager.restoreMangaFetchObservable(source, manga)
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
manga
}
.filter { it.id != null }
.flatMap {
chapterFetchObservable(source, it, chapters)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap {
trackingFetchObservable(it, tracks)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnCompleted {
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, manga.title, errors.size)
}
}
private fun mangaNoFetchObservable(source: Source, backupManga: Manga, chapters: List<Chapter>,
categories: List<String>, history: List<DHistory>,
tracks: List<Track>): Observable<Manga> {
return Observable.just(backupManga)
.flatMap { manga ->
if (!backupManager.restoreChaptersForManga(manga, chapters)) {
chapterFetchObservable(source, manga, chapters)
.map { manga }
} else {
Observable.just(manga)
}
}
.doOnNext {
restoreExtraForManga(it, categories, history, tracks)
}
.flatMap { manga ->
trackingFetchObservable(manga, tracks)
// Convert to the manga that contains new chapters.
.map { manga }
}
.doOnCompleted {
restoreProgress += 1
showRestoreProgress(restoreProgress, restoreAmount, backupManga.title, errors.size)
}
}
private fun restoreExtraForManga(manga: Manga, categories: List<String>, history: List<DHistory>, tracks: List<Track>) {
// Restore categories
backupManager.restoreCategoriesForManga(manga, categories)
// Restore history
backupManager.restoreHistoryForManga(history)
// Restore tracking
backupManager.restoreTrackForManga(manga, tracks)
}
/**
* [Observable] that fetches chapter information
*
* @param source source of manga
* @param manga manga that needs updating
* @return [Observable] that contains manga
*/
private fun chapterFetchObservable(source: Source, manga: Manga, chapters: List<Chapter>): Observable<Pair<List<Chapter>, List<Chapter>>> {
return backupManager.restoreChapterFetchObservable(source, manga, chapters)
// If there's any error, return empty update and continue.
.onErrorReturn {
errors.add(Date() to "${manga.title} - ${it.message}")
Pair(emptyList(), emptyList())
}
}
/**
* [Observable] that refreshes tracking information
* @param manga manga that needs updating.
* @param tracks list containing tracks from restore file.
* @return [Observable] that contains updated track item
*/
private fun trackingFetchObservable(manga: Manga, tracks: List<Track>): Observable<Track> {
return Observable.from(tracks)
.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]
*
* @param progress restore progress
* @param amount total restoreAmount of manga
* @param title title of restored manga
*/
private fun showRestoreProgress(progress: Int, amount: Int, title: String, errors: Int,
content: String = getString(R.string.dialog_restoring_backup, title.chop(15))) {
val intent = Intent(BackupConst.INTENT_FILTER).apply {
putExtra(BackupConst.EXTRA_PROGRESS, progress)
putExtra(BackupConst.EXTRA_AMOUNT, amount)
putExtra(BackupConst.EXTRA_CONTENT, content)
putExtra(BackupConst.EXTRA_ERRORS, errors)
putExtra(BackupConst.ACTION, BackupConst.ACTION_SET_PROGRESS_DIALOG)
}
sendLocalBroadcast(intent)
}
}

View File

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.data.backup.models
import java.text.SimpleDateFormat
import java.util.*
/**
* Json values
*/
object Backup {
const val CURRENT_VERSION = 2
const val MANGA = "manga"
const val MANGAS = "mangas"
const val TRACK = "track"
const val CHAPTERS = "chapters"
const val CATEGORIES = "categories"
const val HISTORY = "history"
const val VERSION = "version"
fun getDefaultFilename(): String {
val date = SimpleDateFormat("yyyy-MM-dd_HH-mm", Locale.getDefault()).format(Date())
return "tachiyomi_$date.json"
}
}

View File

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

View File

@ -1,16 +0,0 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
class BooleanSerializer : JsonSerializer<Boolean> {
override fun serialize(value: Boolean?, type: Type, context: JsonSerializationContext): JsonElement? {
if (value != null && value != false)
return JsonPrimitive(value)
return null
}
}

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
/**
* JSON Serializer used to write / read [CategoryImpl] to / from json
*/
object CategoryTypeAdapter {
fun build(): TypeAdapter<CategoryImpl> {
return typeAdapter {
write {
beginArray()
value(it.name)
value(it.order)
endArray()
}
read {
beginArray()
val category = CategoryImpl()
category.name = nextString()
category.order = nextInt()
endArray()
category
}
}
}
}

View File

@ -0,0 +1,61 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonToken
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
/**
* JSON Serializer used to write / read [ChapterImpl] to / from json
*/
object ChapterTypeAdapter {
private const val URL = "u"
private const val READ = "r"
private const val BOOKMARK = "b"
private const val LAST_READ = "l"
fun build(): TypeAdapter<ChapterImpl> {
return typeAdapter {
write {
if (it.read || it.bookmark || it.last_page_read != 0) {
beginObject()
name(URL)
value(it.url)
if (it.read) {
name(READ)
value(1)
}
if (it.bookmark) {
name(BOOKMARK)
value(1)
}
if (it.last_page_read != 0) {
name(LAST_READ)
value(it.last_page_read)
}
endObject()
}
}
read {
val chapter = ChapterImpl()
beginObject()
while (hasNext()) {
if (peek() == JsonToken.NAME) {
val name = nextName()
when (name) {
URL -> chapter.url = nextString()
READ -> chapter.read = nextInt() == 1
BOOKMARK -> chapter.bookmark = nextInt() == 1
LAST_READ -> chapter.last_page_read = nextInt()
}
}
}
endObject()
chapter
}
}
}
}

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import eu.kanade.tachiyomi.data.backup.models.DHistory
/**
* JSON Serializer used to write / read [DHistory] to / from json
*/
object HistoryTypeAdapter {
fun build(): TypeAdapter<DHistory> {
return typeAdapter {
write {
if (it.lastRead != 0L) {
beginArray()
value(it.url)
value(it.lastRead)
endArray()
}
}
read {
beginArray()
val url = nextString()
val lastRead = nextLong()
endArray()
DHistory(url, lastRead)
}
}
}
}

View File

@ -1,27 +0,0 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl
class IdExclusion : ExclusionStrategy {
private val categoryExclusions = listOf("id")
private val mangaExclusions = listOf("id")
private val chapterExclusions = listOf("id", "manga_id")
private val syncExclusions = listOf("id", "manga_id", "update")
override fun shouldSkipField(f: FieldAttributes) = when (f.declaringClass) {
MangaImpl::class.java -> mangaExclusions.contains(f.name)
ChapterImpl::class.java -> chapterExclusions.contains(f.name)
MangaSyncImpl::class.java -> syncExclusions.contains(f.name)
CategoryImpl::class.java -> categoryExclusions.contains(f.name)
else -> false
}
override fun shouldSkipClass(clazz: Class<*>) = false
}

View File

@ -1,17 +0,0 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
class IntegerSerializer : JsonSerializer<Int> {
override fun serialize(value: Int?, type: Type, context: JsonSerializationContext): JsonElement? {
if (value != null && value !== 0)
return JsonPrimitive(value)
return null
}
}

View File

@ -1,16 +0,0 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.google.gson.JsonElement
import com.google.gson.JsonPrimitive
import com.google.gson.JsonSerializationContext
import com.google.gson.JsonSerializer
import java.lang.reflect.Type
class LongSerializer : JsonSerializer<Long> {
override fun serialize(value: Long?, type: Type, context: JsonSerializationContext): JsonElement? {
if (value != null && value !== 0L)
return JsonPrimitive(value)
return null
}
}

View File

@ -0,0 +1,37 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import eu.kanade.tachiyomi.data.database.models.MangaImpl
/**
* JSON Serializer used to write / read [MangaImpl] to / from json
*/
object MangaTypeAdapter {
fun build(): TypeAdapter<MangaImpl> {
return typeAdapter {
write {
beginArray()
value(it.url)
value(it.title)
value(it.source)
value(it.viewer)
value(it.chapter_flags)
endArray()
}
read {
beginArray()
val manga = MangaImpl()
manga.url = nextString()
manga.title = nextString()
manga.source = nextLong()
manga.viewer = nextInt()
manga.chapter_flags = nextInt()
endArray()
manga
}
}
}
}

View File

@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.github.salomonbrys.kotson.typeAdapter
import com.google.gson.TypeAdapter
import com.google.gson.stream.JsonToken
import eu.kanade.tachiyomi.data.database.models.TrackImpl
/**
* JSON Serializer used to write / read [TrackImpl] to / from json
*/
object TrackTypeAdapter {
private const val SYNC = "s"
private const val REMOTE = "r"
private const val TITLE = "t"
private const val LAST_READ = "l"
private const val TRACKING_URL = "u"
fun build(): TypeAdapter<TrackImpl> {
return typeAdapter {
write {
beginObject()
name(TITLE)
value(it.title)
name(SYNC)
value(it.sync_id)
name(REMOTE)
value(it.remote_id)
name(LAST_READ)
value(it.last_chapter_read)
name(TRACKING_URL)
value(it.tracking_url)
endObject()
}
read {
val track = TrackImpl()
beginObject()
while (hasNext()) {
if (peek() == JsonToken.NAME) {
val name = nextName()
when (name) {
TITLE -> track.title = nextString()
SYNC -> track.sync_id = nextInt()
REMOTE -> track.remote_id = nextInt()
LAST_READ -> track.last_chapter_read = nextInt()
TRACKING_URL -> track.tracking_url = nextString()
}
}
}
endObject()
track
}
}
}
}

View File

@ -5,7 +5,8 @@ import android.text.format.Formatter
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.Gson
import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.util.DiskUtil
import eu.kanade.tachiyomi.util.saveTo
import okhttp3.Response
@ -44,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)
@ -81,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
}
}
@ -92,13 +92,13 @@ class ChapterCache(private val context: Context) {
/**
* Get page list from cache.
*
* @param chapterUrl the url of the chapter.
* @param chapter the chapter.
* @return an observable of the list of pages.
*/
fun getPageListFromCache(chapterUrl: String): Observable<List<Page>> {
return Observable.fromCallable<List<Page>> {
fun getPageListFromCache(chapter: Chapter): Observable<List<Page>> {
return Observable.fromCallable {
// Get the key for the chapter.
val key = DiskUtil.hashKeyForDisk(chapterUrl)
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
// Convert JSON string to list of objects. Throws an exception if snapshot is null
diskCache.get(key).use {
@ -110,10 +110,10 @@ class ChapterCache(private val context: Context) {
/**
* Add page list to disk cache.
*
* @param chapterUrl the url of the chapter.
* @param chapter the chapter.
* @param pages list of pages.
*/
fun putPageListToCache(chapterUrl: String, pages: List<Page>) {
fun putPageListToCache(chapter: Chapter, pages: List<Page>) {
// Convert list of pages to json string.
val cachedValue = gson.toJson(pages)
@ -122,7 +122,7 @@ class ChapterCache(private val context: Context) {
try {
// Get editor from md5 key.
val key = DiskUtil.hashKeyForDisk(chapterUrl)
val key = DiskUtil.hashKeyForDisk(getKey(chapter))
editor = diskCache.edit(key) ?: return
// Write chapter urls to cache.
@ -186,15 +186,18 @@ class ChapterCache(private val context: Context) {
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
// Get OutputStream and write image with Okio.
response.body().source().saveTo(editor.newOutputStream(0))
response.body()!!.source().saveTo(editor.newOutputStream(0))
diskCache.flush()
editor.commit()
} finally {
response.body().close()
response.body()?.close()
editor?.abortUnlessCommitted()
}
}
private fun getKey(chapter: Chapter): String {
return "${chapter.manga_id}${chapter.url}"
}
}

View File

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

View File

@ -10,13 +10,13 @@ import eu.kanade.tachiyomi.data.database.queries.*
* This class provides operations to manage the database through its interfaces.
*/
open class DatabaseHelper(context: Context)
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
: MangaQueries, ChapterQueries, TrackQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(DbOpenHelper(context))
.addTypeMapping(Manga::class.java, MangaTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
.addTypeMapping(MangaSync::class.java, MangaSyncTypeMapping())
.addTypeMapping(Track::class.java, TrackTypeMapping())
.addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping())
@ -24,4 +24,6 @@ open class DatabaseHelper(context: Context)
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
fun lowLevel() = db.lowLevel()
}

View File

@ -17,13 +17,13 @@ class DbOpenHelper(context: Context)
/**
* Version of the database.
*/
const val DATABASE_VERSION = 4
const val DATABASE_VERSION = 6
}
override fun onCreate(db: SQLiteDatabase) = with(db) {
execSQL(MangaTable.createTableQuery)
execSQL(ChapterTable.createTableQuery)
execSQL(MangaSyncTable.createTableQuery)
execSQL(TrackTable.createTableQuery)
execSQL(CategoryTable.createTableQuery)
execSQL(MangaCategoryTable.createTableQuery)
execSQL(HistoryTable.createTableQuery)
@ -51,6 +51,12 @@ class DbOpenHelper(context: Context)
if (oldVersion < 4) {
db.execSQL(ChapterTable.bookmarkUpdateQuery)
}
if (oldVersion < 5) {
db.execSQL(ChapterTable.addScanlator)
}
if (oldVersion < 6) {
db.execSQL(TrackTable.addTrackingUrl)
}
}
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

@ -10,6 +10,7 @@ import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.HistoryImpl
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_CHAPTER_ID
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_LAST_READ
@ -44,7 +45,7 @@ open class HistoryPutResolver : DefaultPutResolver<History>() {
class HistoryGetResolver : DefaultGetResolver<History>() {
override fun mapFromCursor(cursor: Cursor): History = History().apply {
override fun mapFromCursor(cursor: Cursor): History = HistoryImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
chapter_id = cursor.getLong(cursor.getColumnIndex(COL_CHAPTER_ID))
last_read = cursor.getLong(cursor.getColumnIndex(COL_LAST_READ))

View File

@ -65,11 +65,10 @@ 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.getInt(cursor.getColumnIndex(COL_SOURCE))
source = cursor.getLong(cursor.getColumnIndex(COL_SOURCE))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
artist = cursor.getString(cursor.getColumnIndex(COL_ARTIST))
author = cursor.getString(cursor.getColumnIndex(COL_AUTHOR))
@ -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

@ -9,38 +9,39 @@ import com.pushtorefresh.storio.sqlite.operations.put.DefaultPutResolver
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.InsertQuery
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.database.models.MangaSyncImpl
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_LAST_CHAPTER_READ
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_REMOTE_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SCORE
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_SYNC_ID
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TITLE
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.COL_TOTAL_CHAPTERS
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable.TABLE
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.TrackImpl
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_LAST_CHAPTER_READ
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_REMOTE_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SCORE
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_SYNC_ID
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TITLE
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TOTAL_CHAPTERS
import eu.kanade.tachiyomi.data.database.tables.TrackTable.COL_TRACKING_URL
import eu.kanade.tachiyomi.data.database.tables.TrackTable.TABLE
class MangaSyncTypeMapping : SQLiteTypeMapping<MangaSync>(
MangaSyncPutResolver(),
MangaSyncGetResolver(),
MangaSyncDeleteResolver()
class TrackTypeMapping : SQLiteTypeMapping<Track>(
TrackPutResolver(),
TrackGetResolver(),
TrackDeleteResolver()
)
class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
class TrackPutResolver : DefaultPutResolver<Track>() {
override fun mapToInsertQuery(obj: MangaSync) = InsertQuery.builder()
override fun mapToInsertQuery(obj: Track) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: MangaSync) = UpdateQuery.builder()
override fun mapToUpdateQuery(obj: Track) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: MangaSync) = ContentValues(9).apply {
override fun mapToContentValues(obj: Track) = ContentValues(10).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_SYNC_ID, obj.sync_id)
@ -49,13 +50,15 @@ class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
put(COL_STATUS, obj.status)
put(COL_TRACKING_URL, obj.tracking_url)
put(COL_SCORE, obj.score)
}
}
class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
class TrackGetResolver : DefaultGetResolver<Track>() {
override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().apply {
override fun mapFromCursor(cursor: Cursor): Track = TrackImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
sync_id = cursor.getInt(cursor.getColumnIndex(COL_SYNC_ID))
@ -65,12 +68,13 @@ class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
tracking_url = cursor.getString(cursor.getColumnIndex(COL_TRACKING_URL))
}
}
class MangaSyncDeleteResolver : DefaultDeleteResolver<MangaSync>() {
class TrackDeleteResolver : DefaultDeleteResolver<Track>() {
override fun mapToDeleteQuery(obj: MangaSync) = DeleteQuery.builder()
override fun mapToDeleteQuery(obj: Track) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)

View File

@ -1,17 +1,14 @@
package eu.kanade.tachiyomi.data.database.models
import eu.kanade.tachiyomi.source.model.SChapter
import java.io.Serializable
interface Chapter : Serializable {
interface Chapter : SChapter, Serializable {
var id: Long?
var manga_id: Long?
var url: String
var name: String
var read: Boolean
var bookmark: Boolean
@ -20,10 +17,6 @@ interface Chapter : Serializable {
var date_fetch: Long
var date_upload: Long
var chapter_number: Float
var source_order: Int
val isRecognizedNumber: Boolean

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

@ -5,27 +5,27 @@ import java.io.Serializable
/**
* Object containing the history statistics of a chapter
*/
class History : Serializable {
interface History : Serializable {
/**
* Id of history object.
*/
var id: Long? = null
var id: Long?
/**
* Chapter id of history object.
*/
var chapter_id: Long = 0
var chapter_id: Long
/**
* Last time chapter was read in time long format
*/
var last_read: Long = 0
var last_read: Long
/**
* Total time chapter was read - todo not yet implemented
*/
var time_read: Long = 0
var time_read: Long
companion object {
@ -35,10 +35,8 @@ class History : Serializable {
* @param chapter chapter object
* @return history object
*/
fun create(chapter: Chapter): History {
val history = History()
history.chapter_id = chapter.id!!
return history
fun create(chapter: Chapter): History = HistoryImpl().apply {
this.chapter_id = chapter.id!!
}
}
}

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.data.database.models
/**
* Object containing the history statistics of a chapter
*/
class HistoryImpl : History {
/**
* Id of history object.
*/
override var id: Long? = null
/**
* Chapter id of history object.
*/
override var chapter_id: Long = 0
/**
* Last time chapter was read in time long format
*/
override var last_read: Long = 0
/**
* Total time chapter was read - todo not yet implemented
*/
override var time_read: Long = 0
}

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

@ -1,64 +1,21 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
import eu.kanade.tachiyomi.source.model.SManga
interface Manga : Serializable {
interface Manga : SManga {
var id: Long?
var source: Int
var url: String
var title: String
var artist: String?
var author: String?
var description: String?
var genre: String?
var status: Int
var thumbnail_url: String?
var source: Long
var favorite: Boolean
var last_update: Long
var initialized: Boolean
var viewer: Int
var chapter_flags: Int
var unread: Int
var category: Int
fun copyFrom(other: Manga) {
if (other.author != null)
author = other.author
if (other.artist != null)
artist = other.artist
if (other.description != null)
description = other.description
if (other.genre != null)
genre = other.genre
if (other.thumbnail_url != null)
thumbnail_url = other.thumbnail_url
status = other.status
initialized = true
}
fun setChapterOrder(order: Int) {
setFlags(order, SORT_MASK)
}
@ -94,11 +51,6 @@ interface Manga : Serializable {
companion object {
const val UNKNOWN = 0
const val ONGOING = 1
const val COMPLETED = 2
const val LICENSED = 3
const val SORT_DESC = 0x00000000
const val SORT_ASC = 0x00000001
const val SORT_MASK = 0x00000001
@ -126,12 +78,13 @@ interface Manga : Serializable {
const val DISPLAY_NUMBER = 0x00100000
const val DISPLAY_MASK = 0x00100000
fun create(source: Int): Manga = MangaImpl().apply {
fun create(source: Long): Manga = MangaImpl().apply {
this.source = source
}
fun create(pathUrl: String, source: Int = 0): Manga = MangaImpl().apply {
fun create(pathUrl: String, title: String, source: Long = 0): Manga = MangaImpl().apply {
url = pathUrl
this.title = title
this.source = source
}
}

View File

@ -7,4 +7,4 @@ package eu.kanade.tachiyomi.data.database.models
* @param chapter object containing chater
* @param history object containing history
*/
class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)
data class MangaChapterHistory(val manga: Manga, val chapter: Chapter, val history: History)

View File

@ -1,10 +1,10 @@
package eu.kanade.tachiyomi.data.database.models
class MangaImpl : Manga {
open class MangaImpl : Manga {
override var id: Long? = null
override var source: Int = 0
override var source: Long = -1
override lateinit var url: String
@ -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

@ -2,7 +2,7 @@ package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
interface MangaSync : Serializable {
interface Track : Serializable {
var id: Long?
@ -22,17 +22,16 @@ interface MangaSync : Serializable {
var status: Int
var update: Boolean
var tracking_url: String
fun copyPersonalFrom(other: MangaSync) {
fun copyPersonalFrom(other: Track) {
last_chapter_read = other.last_chapter_read
score = other.score
status = other.status
}
companion object {
fun create(serviceId: Int): MangaSync = MangaSyncImpl().apply {
fun create(serviceId: Int): Track = TrackImpl().apply {
sync_id = serviceId
}
}

View File

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.models
class MangaSyncImpl : MangaSync {
class TrackImpl : Track {
override var id: Long? = null
@ -20,17 +20,17 @@ class MangaSyncImpl : MangaSync {
override var status: Int = 0
override var update: Boolean = false
override var tracking_url: String = ""
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val mangaSync = other as MangaSync
other as Track
if (manga_id != mangaSync.manga_id) return false
if (sync_id != mangaSync.sync_id) return false
return remote_id == mangaSync.remote_id
if (manga_id != other.manga_id) return false
if (sync_id != other.sync_id) return false
return remote_id == other.remote_id
}
override fun hashCode(): Int {

View File

@ -6,6 +6,7 @@ import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.database.resolvers.ChapterBackupPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
@ -42,6 +43,16 @@ interface ChapterQueries : DbProvider {
.build())
.prepare()
fun getChapter(url: String) = db.get()
.`object`(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?")
.whereArgs(url)
.build())
.prepare()
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
@ -50,6 +61,11 @@ interface ChapterQueries : DbProvider {
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
fun updateChaptersBackup(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterBackupPutResolver())
.prepare()
fun updateChapterProgress(chapter: Chapter) = db.put()
.`object`(chapter)
.withPutResolver(ChapterProgressPutResolver())

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.History
@ -40,6 +41,14 @@ interface HistoryQueries : DbProvider {
.build())
.prepare()
fun getHistoryByChapterUrl(chapterUrl: String) = db.get()
.`object`(History::class.java)
.withQuery(RawQuery.builder()
.query(getHistoryByChapterUrl())
.args(chapterUrl)
.observesTables(HistoryTable.TABLE)
.build())
.prepare()
/**
* Updates the history last read.
@ -60,4 +69,18 @@ interface HistoryQueries : DbProvider {
.objects(historyList)
.withPutResolver(HistoryLastReadPutResolver())
.prepare()
fun deleteHistory() = db.delete()
.byQuery(DeleteQuery.builder()
.table(HistoryTable.TABLE)
.build())
.prepare()
fun deleteHistoryNoLastRead() = db.delete()
.byQuery(DeleteQuery.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_LAST_READ} = ?")
.whereArgs(0)
.build())
.prepare()
}

View File

@ -4,9 +4,13 @@ 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
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable
@ -21,15 +25,15 @@ 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)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE, CategoryTable.TABLE)
.build())
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
open fun getFavoriteMangas() = db.get()
fun getFavoriteMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
@ -39,7 +43,7 @@ interface MangaQueries : DbProvider {
.build())
.prepare()
fun getManga(url: String, sourceId: Int) = db.get()
fun getManga(url: String, sourceId: Long) = db.get()
.`object`(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
@ -66,6 +70,16 @@ interface MangaQueries : DbProvider {
.withPutResolver(MangaFlagsPutResolver())
.prepare()
fun updateLastUpdated(manga: Manga) = db.put()
.`object`(manga)
.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()
@ -78,4 +92,20 @@ interface MangaQueries : DbProvider {
.build())
.prepare()
fun deleteMangas() = db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaTable.TABLE)
.build())
.prepare()
fun getLastReadManga() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder()
.query(getLastReadMangaQuery())
.observesTables(MangaTable.TABLE)
.build())
.prepare()
fun getTotalChapterManga() = db.get().listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder().query(getTotalChapterMangaQuery()).observesTables(MangaTable.TABLE).build()).prepare();
}

View File

@ -1,46 +0,0 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
interface MangaSyncQueries : DbProvider {
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
.`object`(MangaSync::class.java)
.withQuery(Query.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COL_MANGA_ID} = ? AND " +
"${MangaSyncTable.COL_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id)
.build())
.prepare()
fun getMangasSync(manga: Manga) = db.get()
.listOfObjects(MangaSync::class.java)
.withQuery(Query.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
fun insertMangaSync(manga: MangaSync) = db.put().`object`(manga).prepare()
fun insertMangasSync(mangas: List<MangaSync>) = db.put().objects(mangas).prepare()
fun deleteMangaSync(manga: MangaSync) = db.delete().`object`(manga).prepare()
fun deleteMangaSyncForManga(manga: Manga) = db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
}

View File

@ -73,6 +73,35 @@ fun getHistoryByMangaId() = """
WHERE ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
"""
fun getHistoryByChapterUrl() = """
SELECT ${History.TABLE}.*
FROM ${History.TABLE}
JOIN ${Chapter.TABLE}
ON ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
WHERE ${Chapter.TABLE}.${Chapter.COL_URL} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
"""
fun getLastReadMangaQuery() = """
SELECT ${Manga.TABLE}.*, MAX(${History.TABLE}.${History.COL_LAST_READ}) AS max
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
JOIN ${History.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
WHERE ${Manga.TABLE}.${Manga.COL_FAVORITE} = 1
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
ORDER BY max DESC
"""
fun getTotalChapterMangaQuery()= """
SELECT ${Manga.TABLE}.*
FROM ${Manga.TABLE}
JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
GROUP BY ${Manga.TABLE}.${Manga.COL_ID}
ORDER by COUNT(*)
"""
/**
* Query to get the categories for a manga.
*/

View File

@ -0,0 +1,34 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import com.pushtorefresh.storio.sqlite.queries.Query
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.tables.TrackTable
import eu.kanade.tachiyomi.data.track.TrackService
interface TrackQueries : DbProvider {
fun getTracks(manga: Manga) = db.get()
.listOfObjects(Track::class.java)
.withQuery(Query.builder()
.table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
fun insertTrack(track: Track) = db.put().`object`(track).prepare()
fun insertTracks(tracks: List<Track>) = db.put().objects(tracks).prepare()
fun deleteTrackForManga(manga: Manga, sync: TrackService) = db.delete()
.byQuery(DeleteQuery.builder()
.table(TrackTable.TABLE)
.where("${TrackTable.COL_MANGA_ID} = ? AND ${TrackTable.COL_SYNC_ID} = ?")
.whereArgs(manga.id, sync.id)
.build())
.prepare()
}

View File

@ -0,0 +1,35 @@
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.Chapter
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
class ChapterBackupPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_URL} = ?")
.whereArgs(chapter.url)
.build()
fun mapToContentValues(chapter: Chapter) = ContentValues(3).apply {
put(ChapterTable.COL_READ, chapter.read)
put(ChapterTable.COL_BOOKMARK, chapter.bookmark)
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
}
}

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

@ -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 MangaLastUpdatedPutResolver : 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_LAST_UPDATE, manga.last_update)
}
}

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

@ -1,6 +1,6 @@
package eu.kanade.tachiyomi.data.database.tables
object MangaSyncTable {
object TrackTable {
const val TABLE = "manga_sync"
@ -22,6 +22,8 @@ object MangaSyncTable {
const val COL_TOTAL_CHAPTERS = "total_chapters"
const val COL_TRACKING_URL = "remote_url"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
@ -33,9 +35,12 @@ object MangaSyncTable {
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
$COL_STATUS INTEGER NOT NULL,
$COL_SCORE FLOAT NOT NULL,
$COL_TRACKING_URL TEXT NOT NULL,
UNIQUE ($COL_MANGA_ID, $COL_SYNC_ID) ON CONFLICT REPLACE,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE
)"""
val addTrackingUrl: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_TRACKING_URL TEXT DEFAULT ''"
}

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

@ -6,8 +6,8 @@ import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.source.Source
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.Page
import rx.Observable
/**
@ -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.
@ -60,10 +65,19 @@ class DownloadManager(context: Context) {
}
/**
* Empties the download queue.
* Tells the downloader to pause downloads.
*/
fun clearQueue() {
downloader.clearQueue()
fun pauseDownloads() {
downloader.pause()
}
/**
* Empties the download queue.
*
* @param isNotification value that determines if status is set (needed for view updates)
*/
fun clearQueue(isNotification: Boolean = false) {
downloader.clearQueue(isNotification)
}
/**
@ -71,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)
}
/**
@ -85,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))
}
/**
@ -111,44 +126,45 @@ class DownloadManager(context: Context) {
}
/**
* Returns the directory name for the given chapter.
* Returns true if the chapter is downloaded.
*
* @param chapter the chapter to query.
*/
fun getChapterDirName(chapter: Chapter): String {
return provider.getChapterDirName(chapter)
}
/**
* 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,15 @@ 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
/**
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
@ -20,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))
}
@ -33,117 +36,135 @@ internal class DownloadNotifier(private val context: Context) {
* The size of queue on start download.
*/
var initialQueueSize = 0
set(value) {
if (value != 0){
isSingleChapter = (value == 1)
}
field = value
}
/**
* Simultaneous download setting > 1.
* Updated when error is thrown
*/
var multipleDownloadThreads = false
var errorThrown = false
/**
* Updated when only single page is downloaded
*/
var isSingleChapter = false
/**
* Updated when paused
*/
var paused = false
/**
* Shows a notification from this builder.
*
* @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())
}
/**
* Clear old actions if they exist.
*/
private fun clearActions() = with(notification) {
if (!mActions.isEmpty())
mActions.clear()
}
/**
* Dismiss the downloader's notification. Downloader error notifications use a different id, so
* those can only be dismissed by the user.
*/
fun dismiss() {
context.notificationManager.cancel(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID)
context.notificationManager.cancel(Notifications.ID_DOWNLOAD_CHAPTER)
}
/**
* Called when download progress changes.
* Note: Only accepted when multi download active.
*
* @param queue the queue containing downloads.
*/
fun onProgressChange(queue: DownloadQueue) {
if (multipleDownloadThreads) {
doOnProgressChange(null, queue)
}
}
/**
* Called when download progress changes.
* Note: Only accepted when single download active.
*
* @param download download object containing download information.
* @param queue the queue containing downloads.
*/
fun onProgressChange(download: Download, queue: DownloadQueue) {
if (!multipleDownloadThreads) {
doOnProgressChange(download, queue)
}
}
/**
* Show notification progress of chapter.
*
* @param download download object containing download information.
* @param queue the queue containing downloads.
*/
private fun doOnProgressChange(download: Download?, queue: DownloadQueue) {
// Check if download is completed
if (multipleDownloadThreads) {
if (queue.isEmpty()) {
onChapterCompleted(null)
return
}
} else {
if (download != null && download.pages!!.size == download.downloadedImages) {
onChapterCompleted(download)
return
}
}
fun onProgressChange(download: Download) {
// Create notification
with(notification) {
// Check if icon needs refresh
// Check if first call.
if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download)
setAutoCancel(false)
clearActions()
// Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
isDownloading = true
}
if (multipleDownloadThreads) {
setContentTitle(context.getString(R.string.app_name))
// Reset the queue size if the download progress is negative
if ((initialQueueSize - queue.size) < 0)
initialQueueSize = queue.size
setContentText(context.getString(R.string.chapter_downloading_progress)
.format(initialQueueSize - queue.size, initialQueueSize))
setProgress(initialQueueSize, initialQueueSize - queue.size, false)
} else {
download?.let {
setContentTitle(it.chapter.name.chop(30))
setContentText(context.getString(R.string.chapter_downloading_progress)
.format(it.downloadedImages, it.pages!!.size))
setProgress(it.pages!!.size, it.downloadedImages, false)
}
}
val title = download.manga.title.chop(15)
val quotedTitle = Pattern.quote(title)
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
setContentTitle("$title - $chapter".chop(30))
setContentText(context.getString(R.string.chapter_downloading_progress)
.format(download.downloadedImages, download.pages!!.size))
setProgress(download.pages!!.size, download.downloadedImages, false)
}
// Displays the progress bar on notification
notification.show()
}
/**
* Show notification when download is paused.
*/
fun onDownloadPaused() {
with(notification) {
setContentTitle(context.getString(R.string.chapter_paused))
setContentText(context.getString(R.string.download_notifier_download_paused))
setSmallIcon(R.drawable.ic_av_pause_grey_24dp_img)
setAutoCancel(false)
setProgress(0, 0, false)
clearActions()
// Open download manager when clicked
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
// Resume action
addAction(R.drawable.ic_av_play_arrow_grey_img,
context.getString(R.string.action_resume),
NotificationReceiver.resumeDownloadsPendingBroadcast(context))
//Clear action
addAction(R.drawable.ic_clear_grey_24dp_img,
context.getString(R.string.action_clear),
NotificationReceiver.clearDownloadsPendingBroadcast(context))
}
// Show notification.
notification.show()
// Reset initial values
isDownloading = false
initialQueueSize = 0
}
/**
* Called when chapter is downloaded.
*
* @param download download object containing download information.
*/
private fun onChapterCompleted(download: Download?) {
fun onDownloadCompleted(download: Download, queue: DownloadQueue) {
// Check if last download
if (!queue.isEmpty()) {
return
}
// Create notification.
with(notification) {
setContentTitle(download?.chapter?.name ?: context.getString(R.string.app_name))
val title = download.manga.title.chop(15)
val quotedTitle = Pattern.quote(title)
val chapter = download.chapter.name.replaceFirst("$quotedTitle[\\s]*[-]*[\\s]*".toRegex(RegexOption.IGNORE_CASE), "")
setContentTitle("$title - $chapter".chop(30))
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setAutoCancel(true)
clearActions()
setContentIntent(NotificationReceiver.openChapterPendingBroadcast(context, download.manga, download.chapter))
setProgress(0, 0, false)
}
@ -165,9 +186,15 @@ internal class DownloadNotifier(private val context: Context) {
setContentTitle(context.getString(R.string.download_notifier_downloader_title))
setContentText(reason)
setSmallIcon(android.R.drawable.stat_sys_warning)
setAutoCancel(true)
clearActions()
setContentIntent(NotificationHandler.openDownloadManagerPendingActivity(context))
setProgress(0, 0, false)
}
notification.show()
// Reset download information
isDownloading = false
}
/**
@ -183,11 +210,15 @@ internal class DownloadNotifier(private val context: Context) {
setContentTitle(chapter ?: context.getString(R.string.download_notifier_downloader_title))
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
clearActions()
setAutoCancel(false)
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
isDownloading = false
}
}

View File

@ -6,7 +6,8 @@ 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.source.Source
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.util.DiskUtil
import uy.kohesive.injekt.injectLazy
@ -26,45 +27,57 @@ class DownloadProvider(private val context: Context) {
/**
* The root directory for downloads.
*/
private lateinit var downloadsDir: UniFile
private var downloadsDir = preferences.downloadsDirectory().getOrDefault().let {
UniFile.fromUri(context, Uri.parse(it))
}
init {
preferences.downloadsDirectory().asObservable()
.skip(1)
.subscribe { downloadsDir = UniFile.fromUri(context, Uri.parse(it)) }
}
/**
* 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))
}
/**
* Returns the download directory for a source if it exists.
*
* @param source the source to query.
*/
fun findSourceDir(source: Source): UniFile? {
return downloadsDir.findFile(getSourceDirName(source))
}
/**
* 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? {
val sourceDir = downloadsDir.findFile(getSourceDirName(source))
fun findMangaDir(manga: Manga, source: Source): UniFile? {
val sourceDir = findSourceDir(source)
return sourceDir?.findFile(getMangaDirName(manga))
}
/**
* 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

@ -1,16 +1,20 @@
package eu.kanade.tachiyomi.data.download
import android.app.Notification
import android.app.Service
import android.content.Context
import android.content.Intent
import android.net.NetworkInfo.State.CONNECTED
import android.net.NetworkInfo.State.DISCONNECTED
import android.os.Build
import android.os.IBinder
import android.os.PowerManager
import android.support.v4.app.NotificationCompat
import com.github.pwittchen.reactivenetwork.library.Connectivity
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import com.jakewharton.rxrelay.BehaviorRelay
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.connectivityManager
import eu.kanade.tachiyomi.util.plusAssign
@ -41,7 +45,12 @@ class DownloadService : Service() {
* @param context the application context.
*/
fun start(context: Context) {
context.startService(Intent(context, DownloadService::class.java))
val intent = Intent(context, DownloadService::class.java)
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
}
/**
@ -81,6 +90,7 @@ class DownloadService : Service() {
*/
override fun onCreate() {
super.onCreate()
startForeground(Notifications.ID_DOWNLOAD_CHAPTER, getPlaceholderNotification())
runningRelay.call(true)
subscriptions = CompositeSubscription()
listenDownloaderState()
@ -122,7 +132,7 @@ class DownloadService : Service() {
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ state -> onNetworkStateChanged(state)
}, { error ->
}, { _ ->
toast(R.string.download_queue_error)
stopSelf()
})
@ -176,4 +186,10 @@ class DownloadService : Service() {
if (!isHeld) acquire()
}
private fun getPlaceholderNotification(): Notification {
return NotificationCompat.Builder(this, Notifications.CHANNEL_DOWNLOADER)
.setContentTitle(getString(R.string.download_notifier_downloader_title))
.build()
}
}

View File

@ -5,8 +5,8 @@ import com.google.gson.Gson
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.online.HttpSource
import uy.kohesive.injekt.injectLazy
/**
@ -93,7 +93,7 @@ class DownloadStore(context: Context) {
val manga = cachedManga.getOrPut(mangaId) {
db.getManga(mangaId).executeAsBlocking()
} ?: continue
val source = sourceManager.get(manga.source) as? OnlineSource ?: continue
val source = sourceManager.get(manga.source) as? HttpSource ?: continue
val chapter = db.getChapter(chapterId).executeAsBlocking() ?: continue
downloads.add(Download(source, manga, chapter))
}

View File

@ -9,23 +9,19 @@ import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator
import eu.kanade.tachiyomi.util.RetryWithDelay
import eu.kanade.tachiyomi.util.plusAssign
import eu.kanade.tachiyomi.util.saveTo
import eu.kanade.tachiyomi.source.SourceManager
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
import rx.schedulers.Schedulers
import rx.subjects.BehaviorSubject
import rx.subscriptions.CompositeSubscription
import timber.log.Timber
import uy.kohesive.injekt.injectLazy
import java.net.URLConnection
/**
* This class is the one in charge of downloading chapters.
@ -38,8 +34,13 @@ import java.net.URLConnection
*
* @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.
@ -56,11 +57,6 @@ class Downloader(private val context: Context, private val provider: DownloadPro
*/
private val sourceManager: SourceManager by injectLazy()
/**
* Preferences.
*/
private val preferences: PreferencesHelper by injectLazy()
/**
* Notifier for the downloader state and progress.
*/
@ -71,11 +67,6 @@ class Downloader(private val context: Context, private val provider: DownloadPro
*/
private val subscriptions = CompositeSubscription()
/**
* Subject to do a live update of the number of simultaneous downloads.
*/
private val threadsSubject = BehaviorSubject.create<Int>()
/**
* Relay to send a list of downloads to the downloader.
*/
@ -92,12 +83,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())
}
}
/**
@ -132,15 +121,42 @@ class Downloader(private val context: Context, private val provider: DownloadPro
if (reason != null) {
notifier.onWarning(reason)
} else {
notifier.dismiss()
if (notifier.paused) {
notifier.paused = false
notifier.onDownloadPaused()
} else if (notifier.isSingleChapter && !notifier.errorThrown) {
notifier.isSingleChapter = false
} else {
notifier.dismiss()
}
}
}
/**
* Removes everything from the queue.
* Pauses the downloader
*/
fun clearQueue() {
fun pause() {
destroySubscriptions()
queue
.filter { it.status == Download.DOWNLOADING }
.forEach { it.status = Download.QUEUE }
notifier.paused = true
}
/**
* Removes everything from the queue.
*
* @param isNotification value that determines if status is set (needed for view updates)
*/
fun clearQueue(isNotification: Boolean = false) {
destroySubscriptions()
//Needed to update the chapter view
if (isNotification) {
queue
.filter { it.status == Download.QUEUE }
.forEach { it.status = Download.NOT_DOWNLOADED }
}
queue.clear()
notifier.dismiss()
}
@ -155,14 +171,8 @@ class Downloader(private val context: Context, private val provider: DownloadPro
subscriptions.clear()
subscriptions += preferences.downloadThreads().asObservable()
.subscribe {
threadsSubject.onNext(it)
notifier.multipleDownloadThreads = it > 1
}
subscriptions += downloadsRelay.flatMap { Observable.from(it) }
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
subscriptions += downloadsRelay.concatMapIterable { it }
.concatMap { downloadChapter(it).subscribeOn(Schedulers.io()) }
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ completeDownload(it)
@ -185,58 +195,51 @@ 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? OnlineSource ?: 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
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
if (isRunning) {
// Send the list of downloads to the downloader.
downloadsRelay.call(chaptersToQueue)
}
// Start downloader if needed
if (autoStart) {
DownloadService.start(this@Downloader.context)
}
}
}
/**
@ -244,15 +247,18 @@ class Downloader(private val context: Context, private val provider: DownloadPro
*
* @param download the chapter to be downloaded.
*/
private fun downloadChapter(download: Download): Observable<Download> {
private fun downloadChapter(download: Download): Observable<Download> = Observable.defer {
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) {
// Pull page list from network and add them to download object
download.source.fetchPageListFromNetwork(download.chapter)
download.source.fetchPageList(download.chapter)
.doOnNext { pages ->
if (pages.isEmpty()) {
throw Exception("Page list is empty")
}
download.pages = pages
}
} else {
@ -260,8 +266,8 @@ class Downloader(private val context: Context, private val provider: DownloadPro
Observable.just(download.pages!!)
}
return pageListObservable
.doOnNext { pages ->
pageListObservable
.doOnNext { _ ->
// Delete all temporary (unfinished) files
tmpDir.listFiles()
?.filter { it.name!!.endsWith(".tmp") }
@ -275,18 +281,18 @@ class Downloader(private val context: Context, private val provider: DownloadPro
// Start downloading images, consider we can have downloaded images already
.concatMap { page -> getOrDownloadImage(page, download, tmpDir) }
// Do when page is downloaded.
.doOnNext { notifier.onProgressChange(download, queue) }
.doOnNext { notifier.onProgressChange(download) }
.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
notifier.onError(error.message, download.chapter.name)
download
}
.subscribeOn(Schedulers.io())
}
/**
@ -309,7 +315,7 @@ class Downloader(private val context: Context, private val provider: DownloadPro
tmpFile?.delete()
// Try to find the image file.
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.")}
val imageFile = tmpDir.listFiles()!!.find { it.name!!.startsWith("$filename.") }
// If the image is already downloaded, do nothing. Otherwise download from network
val pageObservable = if (imageFile != null)
@ -342,14 +348,14 @@ class Downloader(private val context: Context, private val provider: DownloadPro
* @param tmpDir the temporary directory of the download.
* @param filename the filename of the image.
*/
private fun downloadImage(page: Page, source: OnlineSource, tmpDir: UniFile, filename: String): Observable<UniFile> {
private fun downloadImage(page: Page, source: HttpSource, tmpDir: UniFile, filename: String): Observable<UniFile> {
page.status = Page.DOWNLOAD_IMAGE
page.progress = 0
return source.imageResponse(page)
return source.fetchImage(page)
.map { response ->
val file = tmpDir.createFile("$filename.tmp")
try {
response.body().source().saveTo(file.openOutputStream())
response.body()!!.source().saveTo(file.openOutputStream())
val extension = getImageExtension(response, file)
file.renameTo("$filename.$extension")
} catch (e: Exception) {
@ -372,13 +378,11 @@ class Downloader(private val context: Context, private val provider: DownloadPro
*/
private fun getImageExtension(response: Response, file: UniFile): String {
// Read content type if available.
val mime = response.body().contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
// Else guess from the uri.
?: context.contentResolver.getType(file.uri)
// Else read magic numbers.
?: file.openInputStream().buffered().use {
URLConnection.guessContentTypeFromStream(it)
}
val mime = response.body()?.contentType()?.let { ct -> "${ct.type()}/${ct.subtype()}" }
// Else guess from the uri.
?: context.contentResolver.getType(file.uri)
// Else read magic numbers.
?: DiskUtil.findImageMime { file.openInputStream() }
return MimeTypeMap.getSingleton().getExtensionFromMimeType(mime) ?: "jpg"
}
@ -387,10 +391,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") }
@ -403,6 +410,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)
}
}
@ -414,9 +422,11 @@ class Downloader(private val context: Context, private val provider: DownloadPro
if (download.status == Download.DOWNLOADED) {
// remove downloaded chapter from queue
queue.remove(download)
notifier.onProgressChange(queue)
}
if (areAllDownloadsFinished()) {
if (notifier.isSingleChapter && !notifier.errorThrown) {
notifier.onDownloadCompleted(download, queue)
}
DownloadService.stop(context)
}
}
@ -428,4 +438,4 @@ class Downloader(private val context: Context, private val provider: DownloadPro
return queue.none { it.status <= Download.DOWNLOADING }
}
}
}

View File

@ -2,11 +2,11 @@ package eu.kanade.tachiyomi.data.download.model
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.source.model.Page
import eu.kanade.tachiyomi.source.online.HttpSource
import rx.subjects.PublishSubject
class Download(val source: OnlineSource, val manga: Manga, val chapter: Chapter) {
class Download(val source: HttpSource, val manga: Manga, val chapter: Chapter) {
var pages: List<Page>? = null

View File

@ -3,7 +3,7 @@ package eu.kanade.tachiyomi.data.download.model
import com.jakewharton.rxrelay.PublishRelay
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.download.DownloadStore
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.source.model.Page
import rx.Observable
import rx.subjects.PublishSubject
import java.util.concurrent.CopyOnWriteArrayList
@ -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,32 +0,0 @@
package eu.kanade.tachiyomi.data.glide
import android.content.Context
import com.bumptech.glide.Glide
import com.bumptech.glide.GlideBuilder
import com.bumptech.glide.integration.okhttp3.OkHttpUrlLoader
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.load.model.GlideUrl
import com.bumptech.glide.module.GlideModule
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.network.NetworkHelper
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.InputStream
/**
* Class used to update Glide module settings
*/
class AppGlideModule : GlideModule {
override fun applyOptions(context: Context, builder: GlideBuilder) {
// Set the cache size of Glide to 15 MiB
builder.setDiskCache(InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024))
}
override fun registerComponents(context: Context, glide: Glide) {
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())
}
}

View File

@ -0,0 +1,51 @@
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,82 +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 MangaDataFetcher(private val networkFetcher: DataFetcher<InputStream>,
private val file: File,
private val manga: Manga)
: DataFetcher<InputStream> {
@Throws(Exception::class)
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 file.inputStream()
} else {
if (file.exists()) {
file.delete()
}
return networkFetcher.loadData(priority)
}
}
/**
* 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()
}
override fun cancel() {
networkFetcher.cancel()
}
override fun cleanup() {
networkFetcher.cleanup()
}
}

View File

@ -1,22 +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.load.data.DataFetcher
import com.bumptech.glide.integration.okhttp3.OkHttpStreamFetcher
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.data.source.SourceManager
import eu.kanade.tachiyomi.data.source.online.OnlineSource
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 [MangaDataFetcher], 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.
@ -25,75 +27,90 @@ 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.
*/
val coverCache: CoverCache by injectLazy()
private val coverCache: CoverCache by injectLazy()
/**
* Source manager.
*/
val sourceManager: SourceManager by injectLazy()
private val sourceManager: SourceManager by injectLazy()
/**
* Base network loader.
* Default network client.
*/
private val baseLoader = 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.
*/
private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
private val cachedHeaders = hashMapOf<Long, LazyHeaders>()
/**
* Factory class for creating [MangaModelLoader] instances.
*/
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 [MangaDataFetcher] for the given manga or null if the url is empty.
* Returns a fetcher for the given manga or null if the url is empty.
*
* @param manga the model.
* @param 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.isNullOrEmpty()) {
if (url == null || url.isEmpty()) {
return null
}
// 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)), coverCache.getCoverFile(url!!)).apply {
lruCache.put(url, this)
if (url.startsWith("http")) {
val source = sourceManager.get(manga.source) as? HttpSource
val glideUrl = GlideUrl(url, getHeaders(manga, source))
// Get the resource fetcher for this request url.
val networkFetcher = OkHttpStreamFetcher(source?.client ?: defaultClient, glideUrl)
if (!manga.favorite) {
return ModelLoader.LoadData(glideUrl, networkFetcher)
}
// Get the network fetcher for this request url.
val networkFetcher = baseLoader.getResourceFetcher(glideUrl, width, height)
// 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) }
// Return an instance of our fetcher providing the needed elements.
return MangaDataFetcher(networkFetcher, file, manga)
val libraryFetcher = LibraryMangaUrlFetcher(networkFetcher, manga, file)
// Return an instance of the fetcher providing the needed elements.
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 ModelLoader.LoadData(MangaSignature(manga, file), FileFetcher(file))
}
}
/**
@ -101,8 +118,9 @@ class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
*
* @param manga the model.
*/
fun getHeaders(manga: Manga): Headers {
val source = sourceManager.get(manga.source) as? OnlineSource ?: return LazyHeaders.DEFAULT
fun getHeaders(manga: Manga, source: HttpSource?): Headers {
if (source == null) return LazyHeaders.DEFAULT
return cachedHeaders.getOrPut(manga.source) {
LazyHeaders.Builder().apply {
val nullStr: String? = null
@ -114,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

@ -0,0 +1,41 @@
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.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
import uy.kohesive.injekt.api.get
import java.io.InputStream
/**
* Class used to update Glide module settings
*/
@GlideModule
class TachiGlideModule : AppGlideModule() {
override fun applyOptions(context: Context, builder: GlideBuilder) {
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, registry: Registry) {
val networkFactory = OkHttpUrlLoader.Factory(Injekt.get<NetworkHelper>().client)
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

@ -1,30 +1,40 @@
package eu.kanade.tachiyomi.data.library
import android.app.Notification
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.BitmapFactory
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.source.SourceManager
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.online.HttpSource
import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.*
import rx.Observable
import rx.Subscription
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
@ -36,22 +46,13 @@ import java.util.concurrent.atomic.AtomicInteger
* progress of the update, and if case of an unexpected error, this service will be silently
* destroyed.
*/
class LibraryUpdateService : Service() {
/**
* Database helper.
*/
val db: DatabaseHelper by injectLazy()
/**
* Source manager.
*/
val sourceManager: SourceManager by injectLazy()
/**
* Preferences.
*/
val preferences: PreferencesHelper by injectLazy()
class LibraryUpdateService(
val db: DatabaseHelper = Injekt.get(),
val sourceManager: SourceManager = Injekt.get(),
val preferences: PreferencesHelper = Injekt.get(),
val downloadManager: DownloadManager = Injekt.get(),
val trackManager: TrackManager = Injekt.get()
) : Service() {
/**
* Wake lock that will be held until the service is destroyed.
@ -64,26 +65,51 @@ class LibraryUpdateService : Service() {
private var subscription: Subscription? = null
/**
* Id of the library update notification.
* Pending intent of action that cancels the library update
*/
private val notificationId: Int
get() = Constants.NOTIFICATION_LIBRARY_ID
private val cancelIntent by lazy {
NotificationReceiver.cancelLibraryUpdatePendingBroadcast(this)
}
/**
* Bitmap of the app for notifications.
*/
private val notificationBitmap by lazy {
BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
}
/**
* Cached progress notification to avoid creating a lot.
*/
private val 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)
}
/**
* Defines what should be updated within a service execution.
*/
enum class Target {
CHAPTERS, // Manga chapters
DETAILS, // Manga metadata
TRACKING // Tracking metadata
}
companion object {
/**
* Key for category to update.
*/
const val UPDATE_CATEGORY = "category"
const val KEY_CATEGORY = "category"
/**
* Key for updating the details instead of the chapters.
* Key that defines what should be updated.
*/
const val UPDATE_DETAILS = "details"
const val KEY_TARGET = "target"
/**
* Returns the status of the service.
@ -92,7 +118,7 @@ class LibraryUpdateService : Service() {
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean {
return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
return context.isServiceRunning(LibraryUpdateService::class.java)
}
/**
@ -101,15 +127,19 @@ class LibraryUpdateService : Service() {
*
* @param context the application context.
* @param category a specific category to update, or null for global update.
* @param details whether to update the details instead of the list of chapters.
* @param target defines what should be updated.
*/
fun start(context: Context, category: Category? = null, details: Boolean = false) {
fun start(context: Context, category: Category? = null, target: Target = Target.CHAPTERS) {
if (!isRunning(context)) {
val intent = Intent(context, LibraryUpdateService::class.java).apply {
putExtra(UPDATE_DETAILS, details)
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
putExtra(KEY_TARGET, target)
category?.let { putExtra(KEY_CATEGORY, it.id) }
}
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
context.startService(intent)
} else {
context.startForegroundService(intent)
}
context.startService(intent)
}
}
@ -130,16 +160,21 @@ class LibraryUpdateService : Service() {
*/
override fun onCreate() {
super.onCreate()
createAndAcquireWakeLock()
startForeground(Notifications.ID_LIBRARY_PROGRESS, progressNotification.build())
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
wakeLock.acquire()
}
/**
* Method called when the service is destroyed. It destroys the running subscription, resets
* the alarm and release the wake lock.
* Method called when the service is destroyed. It destroys subscriptions and releases the wake
* lock.
*/
override fun onDestroy() {
subscription?.unsubscribe()
destroyWakeLock()
if (wakeLock.isHeld) {
wakeLock.release()
}
super.onDestroy()
}
@ -160,6 +195,8 @@ class LibraryUpdateService : Service() {
*/
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
if (intent == null) return Service.START_NOT_STICKY
val target = intent.getSerializableExtra(KEY_TARGET) as? Target
?: return Service.START_NOT_STICKY
// Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe()
@ -167,18 +204,19 @@ class LibraryUpdateService : Service() {
// Update favorite manga. Destroy service when completed or in case of an error.
subscription = Observable
.defer {
val mangaList = getMangaToUpdate(intent)
val mangaList = getMangaToUpdate(intent, target)
// Update either chapter list or manga details.
if (!intent.getBooleanExtra(UPDATE_DETAILS, false))
updateChapterList(mangaList)
else
updateDetails(mangaList)
when (target) {
Target.CHAPTERS -> updateChapterList(mangaList)
Target.DETAILS -> updateDetails(mangaList)
Target.TRACKING -> updateTrackings(mangaList)
}
}
.subscribeOn(Schedulers.io())
.subscribe({
}, {
showNotification(getString(R.string.notification_update_error), "")
Timber.e(it)
stopSelf(startId)
}, {
stopSelf(startId)
@ -191,25 +229,26 @@ class LibraryUpdateService : Service() {
* Returns the list of manga to be updated.
*
* @param intent the update intent.
* @param target the target to update.
* @return a list of manga to update
*/
fun getMangaToUpdate(intent: Intent): List<Manga> {
val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1)
fun getMangaToUpdate(intent: Intent, target: Target): List<LibraryManga> {
val categoryId = intent.getIntExtra(KEY_CATEGORY, -1)
var listToUpdate = if (categoryId != -1)
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
else {
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map { it.toInt() }
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map(String::toInt)
if (categoriesToUpdate.isNotEmpty())
db.getLibraryMangas().executeAsBlocking()
.filter { it.category in categoriesToUpdate }
.distinctBy { it.id }
else
db.getFavoriteMangas().executeAsBlocking().distinctBy { it.id }
db.getLibraryMangas().executeAsBlocking().distinctBy { it.id }
}
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED }
if (target == Target.CHAPTERS && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != SManga.COMPLETED }
}
return listToUpdate
@ -224,52 +263,89 @@ class LibraryUpdateService : Service() {
* @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
val newUpdates = ArrayList<Manga>()
// list containing failed updates
val failedUpdates = ArrayList<Manga>()
val cancelIntent = PendingIntent.getBroadcast(this, 0,
Intent(this, CancelUpdateReceiver::class.java), 0)
// List containing categories that get included in downloads.
val categoriesToDownload = preferences.downloadNewCategories().getOrDefault().map(String::toInt)
// Boolean to determine if user wants to automatically download new chapters.
val downloadNew = preferences.downloadNew().getOrDefault()
// Boolean to determine if DownloadManager has downloads
var hasDownloads = false
// Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate)
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the chapters of the manga.
.concatMap { manga ->
updateManga(manga)
// If there's any error, return empty update and continue.
.onErrorReturn {
failedUpdates.add(manga)
Pair(0, 0)
Pair(emptyList(), emptyList())
}
// Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first > 0 }
.filter { pair -> pair.first.isNotEmpty() }
.doOnNext {
if (downloadNew && (categoriesToDownload.isEmpty() ||
manga.category in categoriesToDownload)) {
downloadChapters(manga, it.first)
hasDownloads = true
}
}
// Convert to the manga that contains new chapters.
.map { manga }
}
// Add manga with new chapters to the list.
.doOnNext { newUpdates.add(it) }
.doOnNext { manga ->
// Set last updated time
manga.last_update = Date().time
db.updateLastUpdated(manga).executeAsBlocking()
// Add to the list
newUpdates.add(manga)
}
// Notify result of the overall update.
.doOnCompleted {
if (newUpdates.isEmpty()) {
cancelNotification()
} else {
showResultNotification(newUpdates, failedUpdates)
if (newUpdates.isNotEmpty()) {
showResultNotification(newUpdates)
if (downloadNew && hasDownloads) {
DownloadService.start(this)
}
}
if (failedUpdates.isNotEmpty()) {
Timber.e("Failed updating: ${failedUpdates.map { it.title }}")
}
cancelProgressNotification()
}
}
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
// we need to get the chapters from the db so we have chapter ids
val mangaChapters = db.getChapters(manga).executeAsBlocking()
val dbChapters = chapters.map {
mangaChapters.find { mangaChapter -> mangaChapter.url == it.url }!!
}
// We don't want to start downloading while the library is updating, because websites
// may don't like it and they could ban the user.
downloadManager.downloadChapters(manga, dbChapters, false)
}
/**
* Updates the chapters for the given manga and adds them to the database.
*
* @param manga the manga to update.
* @return a pair of the inserted and removed chapters.
*/
fun updateManga(manga: Manga): Observable<Pair<Int, Int>> {
val source = sourceManager.get(manga.source) as? OnlineSource ?: return Observable.empty()
fun updateManga(manga: Manga): Observable<Pair<List<Chapter>, List<Chapter>>> {
val source = sourceManager.get(manga.source) as? HttpSource ?: return Observable.empty()
return source.fetchChapterList(manga)
.map { syncChaptersWithSource(db, it, manga, source) }
}
@ -277,101 +353,70 @@ class LibraryUpdateService : Service() {
/**
* Method that updates the details of the given list of manga. It's called in a background
* thread, so it's safe to do heavy operations or network calls here.
* For each manga it calls [updateManga] and updates the notification showing the current
* progress.
*
* @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)
val cancelIntent = PendingIntent.getBroadcast(this, 0,
Intent(this, CancelUpdateReceiver::class.java), 0)
// Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate)
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size) }
// Update the details of the manga.
.concatMap { manga ->
val source = sourceManager.get(manga.source) as? OnlineSource
?: return@concatMap Observable.empty<Manga>()
val source = sourceManager.get(manga.source) as? HttpSource
?: return@concatMap Observable.empty<LibraryManga>()
source.fetchMangaDetails(manga)
.doOnNext { networkManga ->
.map { networkManga ->
manga.copyFrom(networkManga)
db.insertManga(manga).executeAsBlocking()
manga
}
.onErrorReturn { manga }
}
.doOnCompleted {
cancelNotification()
cancelProgressNotification()
}
}
/**
* Returns the text that will be displayed in the notification when there are new chapters.
*
* @param updates a list of manga that contains new chapters.
* @param failedUpdates a list of manga that failed to update.
* @return the body of the notification to display.
* 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 getUpdatedMangasBody(updates: List<Manga>, failedUpdates: List<Manga>): String {
return buildString {
if (updates.isEmpty()) {
append(getString(R.string.notification_no_new_chapters))
append("\n")
} else {
append(getString(R.string.notification_new_chapters))
for (manga in updates) {
append("\n")
append(manga.title.chop(45))
private fun updateTrackings(mangaToUpdate: List<LibraryManga>): Observable<LibraryManga> {
// Initialize the variables holding the progress of the updates.
var count = 0
val loggedServices = trackManager.services.filter { it.isLogged }
// Emit each manga and update it sequentially.
return Observable.from(mangaToUpdate)
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count++, mangaToUpdate.size) }
// Update the tracking details.
.concatMap { manga ->
val tracks = db.getTracks(manga).executeAsBlocking()
Observable.from(tracks)
.concatMap { track ->
val service = trackManager.getService(track.sync_id)
if (service != null && service in loggedServices) {
service.refresh(track)
.doOnNext { db.insertTrack(it).executeAsBlocking() }
.onErrorReturn { track }
} else {
Observable.empty()
}
}
.map { manga }
}
}
if (!failedUpdates.isEmpty()) {
append("\n\n")
append(getString(R.string.notification_manga_update_failed))
for (manga in failedUpdates) {
append("\n")
append(manga.title.chop(45))
.doOnCompleted {
cancelProgressNotification()
}
}
}
}
/**
* Creates and acquires a wake lock until the library is updated.
*/
private fun createAndAcquireWakeLock() {
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "LibraryUpdateService:WakeLock")
wakeLock.acquire()
}
/**
* Releases the wake lock if it's held.
*/
private fun destroyWakeLock() {
if (wakeLock.isHeld) {
wakeLock.release()
}
}
/**
* Shows the notification with the given title and body.
*
* @param title the title of the notification.
* @param body the body of the notification.
*/
private fun showNotification(title: String, body: String) {
notificationManager.notify(notificationId, notification {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setLargeIcon(notificationBitmap)
setContentTitle(title)
setContentText(body)
})
}
/**
@ -381,67 +426,67 @@ class LibraryUpdateService : Service() {
* @param current the current progress.
* @param total the total progress.
*/
private fun showProgressNotification(manga: Manga, current: Int, total: Int, cancelIntent: PendingIntent) {
notificationManager.notify(notificationId, notification {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setLargeIcon(notificationBitmap)
setContentTitle(manga.title)
setProgress(total, current, false)
setOngoing(true)
addAction(R.drawable.ic_clear_grey_24dp_img, getString(android.R.string.cancel), cancelIntent)
})
private fun showProgressNotification(manga: Manga, current: Int, total: Int) {
notificationManager.notify(Notifications.ID_LIBRARY_PROGRESS, progressNotification
.setContentTitle(manga.title)
.setProgress(total, current, false)
.build())
}
/**
* Shows the notification containing the result of the update done by the service.
*
* @param updates a list of manga with new updates.
* @param failed a list of manga that failed to update.
*/
private fun showResultNotification(updates: List<Manga>, failed: List<Manga>) {
val title = getString(R.string.notification_update_completed)
val body = getUpdatedMangasBody(updates, failed)
private fun showResultNotification(updates: List<Manga>) {
val newUpdates = updates.map { it.title.chop(45) }.toMutableSet()
notificationManager.notify(notificationId, notification {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
// Append new chapters from a previous, existing notification
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val previousNotification = notificationManager.activeNotifications
.find { it.id == Notifications.ID_LIBRARY_RESULT }
if (previousNotification != null) {
val oldUpdates = previousNotification.notification.extras
.getString(Notification.EXTRA_BIG_TEXT)
if (!oldUpdates.isNullOrEmpty()) {
newUpdates += oldUpdates.split("\n")
}
}
}
notificationManager.notify(Notifications.ID_LIBRARY_RESULT, notification(Notifications.CHANNEL_LIBRARY) {
setSmallIcon(R.drawable.ic_book_white_24dp)
setLargeIcon(notificationBitmap)
setContentTitle(title)
setStyle(NotificationCompat.BigTextStyle().bigText(body))
setContentIntent(notificationIntent)
setContentTitle(getString(R.string.notification_new_chapters))
if (newUpdates.size > 1) {
setContentText(getString(R.string.notification_new_chapters_text, newUpdates.size))
setStyle(NotificationCompat.BigTextStyle().bigText(newUpdates.joinToString("\n")))
} else {
setContentText(newUpdates.first())
}
priority = NotificationCompat.PRIORITY_HIGH
setContentIntent(getNotificationIntent())
setAutoCancel(true)
})
}
/**
* Cancels the notification.
* Cancels the progress notification.
*/
private fun cancelNotification() {
notificationManager.cancel(notificationId)
private fun cancelProgressNotification() {
notificationManager.cancel(Notifications.ID_LIBRARY_PROGRESS)
}
/**
* Property that returns an intent to open the main activity.
* Returns an intent to open the main activity.
*/
private val notificationIntent: PendingIntent
get() {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/**
* Class that stops updating the library.
*/
class CancelUpdateReceiver : BroadcastReceiver() {
/**
* Method called when user wants a library update.
* @param context the application context.
* @param intent the intent received.
*/
override fun onReceive(context: Context, intent: Intent) {
LibraryUpdateService.stop(context)
context.notificationManager.cancel(Constants.NOTIFICATION_LIBRARY_ID)
}
private fun getNotificationIntent(): PendingIntent {
val intent = Intent(this, MainActivity::class.java)
intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
intent.action = MainActivity.SHORTCUT_RECENTLY_UPDATED
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}

View File

@ -1,23 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync
import android.content.Context
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
class MangaSyncManager(private val context: Context) {
companion object {
const val MYANIMELIST = 1
const val ANILIST = 2
}
val myAnimeList = MyAnimeList(context, MYANIMELIST)
val aniList = Anilist(context, ANILIST)
// TODO enable anilist
val services = listOf(myAnimeList)
fun getService(id: Int) = services.find { it.id == id }
}

View File

@ -1,51 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync
import android.content.Context
import android.support.annotation.CallSuper
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import okhttp3.OkHttpClient
import rx.Completable
import rx.Observable
import uy.kohesive.injekt.injectLazy
abstract class MangaSyncService(private val context: Context, val id: Int) {
val preferences: PreferencesHelper by injectLazy()
val networkService: NetworkHelper by injectLazy()
open val client: OkHttpClient
get() = networkService.client
// Name of the manga sync service to display
abstract val name: String
abstract fun login(username: String, password: String): Completable
open val isLogged: Boolean
get() = !getUsername().isEmpty() &&
!getPassword().isEmpty()
abstract fun add(manga: MangaSync): Observable<MangaSync>
abstract fun update(manga: MangaSync): Observable<MangaSync>
abstract fun bind(manga: MangaSync): Observable<MangaSync>
abstract fun getStatus(status: Int): String
fun saveCredentials(username: String, password: String) {
preferences.setMangaSyncCredentials(this, username, password)
}
@CallSuper
open fun logout() {
preferences.setMangaSyncCredentials(this, "", "")
}
fun getUsername() = preferences.mangaSyncUsername(this)
fun getPassword() = preferences.mangaSyncPassword(this)
}

View File

@ -1,74 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.MangaSync
import rx.Observable
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subscriptions.CompositeSubscription
import uy.kohesive.injekt.injectLazy
class UpdateMangaSyncService : Service() {
val syncManager: MangaSyncManager by injectLazy()
val db: DatabaseHelper by injectLazy()
private lateinit var subscriptions: CompositeSubscription
override fun onCreate() {
super.onCreate()
subscriptions = CompositeSubscription()
}
override fun onDestroy() {
subscriptions.unsubscribe()
super.onDestroy()
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val manga = intent.getSerializableExtra(EXTRA_MANGASYNC)
if (manga != null) {
updateLastChapterRead(manga as MangaSync, startId)
return Service.START_REDELIVER_INTENT
} else {
stopSelf(startId)
return Service.START_NOT_STICKY
}
}
override fun onBind(intent: Intent): IBinder? {
return null
}
private fun updateLastChapterRead(mangaSync: MangaSync, startId: Int) {
val sync = syncManager.getService(mangaSync.sync_id)
if (sync == null) {
stopSelf(startId)
return
}
subscriptions.add(Observable.defer { sync.update(mangaSync) }
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ stopSelf(startId) },
{ stopSelf(startId) }))
}
companion object {
private val EXTRA_MANGASYNC = "extra_mangasync"
@JvmStatic
fun start(context: Context, mangaSync: MangaSync) {
val intent = Intent(context, UpdateMangaSyncService::class.java)
intent.putExtra(EXTRA_MANGASYNC, mangaSync)
context.startService(intent)
}
}
}

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