Compare commits

...

505 Commits

Author SHA1 Message Date
len
dcbd72e64d Release 0.3.2 2016-10-30 17:39:16 +01:00
len
52e1e93f9d Added another image decoder. It should be faster than Rapid and more reliable than Skia. 2016-10-28 19:26:47 +02:00
len
5b1f4f189b Reader fixes 2016-10-24 22:16:50 +02:00
len
d77a1e6925 Change webtoon image callback to onReady 2016-10-24 00:12:32 +02:00
len
19c713ebb2 Minor changes 2016-10-23 22:37:20 +02:00
len
90e0e0b72a Webtoon reader now shows download progress. Keep the progress bar until the image is decoded 2016-10-23 18:59:25 +02:00
len
22bbcaeed0 Remove builtin decoders from Rapid 2016-10-23 16:42:48 +02:00
len
d7b8015df7 Drop support for reencode images 2016-10-23 13:22:14 +02:00
len
c1ac47e1ce Revert support lib 25 (broken as usual), update subsampling lib 2016-10-22 21:43:37 +02:00
len
e375101132 Revert "Support API 25. Use new DividerItemDecoration."
This reverts commit 05b14bae7b.
2016-10-22 21:42:48 +02:00
len
05b14bae7b Support API 25. Use new DividerItemDecoration. 2016-10-22 20:21:25 +02:00
len
eb15fe3898 Remove 2048 bitmap size limit 2016-10-21 21:21:31 +02:00
4f5518bdd8 Fixed wrong chapter recognition for S0 - Chapter 00 (#499) 2016-10-20 16:28:25 +02:00
c9e1e6e020 Release 0.3.1 2016-10-17 08:43:19 +02:00
ade73e6892 Keep project classes 2016-10-17 08:43:19 +02:00
len
ee2aae7e3a Release 0.3.0 2016-10-16 21:00:40 +02:00
len
b6011d4cf5 Minor changes 2016-10-16 20:50:32 +02:00
len
a31c6ff875 Decode notification logo in background thread. Set max bitmap size to 2048 2016-10-16 15:02:55 +02:00
len
69baaac27e Another crash fixed in webtoon reader 2016-10-15 15:27:26 +02:00
b16a90e9d9 Fixed incorrect string for color filter (#493) 2016-10-15 11:50:07 +02:00
len
f31aa622c0 Fix tests 2016-10-15 11:37:28 +02:00
len
4578edf157 Use old refresh icon (but with the app's logo) 2016-10-15 11:31:24 +02:00
len
33df35db1b Multidex debug build 2016-10-15 11:12:16 +02:00
len
093ddd776b Update GCM 2016-10-14 18:17:02 +02:00
len
da10b27219 Dependency udpates, ABI filters 2016-10-14 17:33:58 +02:00
len
5b4ed6f926 Delete old alarm 2016-10-14 17:27:35 +02:00
len
8fc467652d Add app's notification icon 2016-10-13 19:45:10 +02:00
7971b64d57 Update Portuguese(pt_PT) translation. (#492)
-New strings required translation
-Correcting mistakes
2016-10-12 21:24:12 +02:00
len
9c1e2c3c45 Oops.. Fix #489 2016-10-09 14:42:27 +02:00
len
909917e133 Handle individual errors in metadata update 2016-10-09 12:22:21 +02:00
len
3b6c37a30b Increase minimum tile dpi 2016-10-09 11:51:07 +02:00
len
4a6e2a5d99 More crash fixes 2016-10-09 11:34:37 +02:00
len
6cf84256fe Crash fix 2016-10-09 11:10:47 +02:00
len
876831480a Remove unused context from sources 2016-10-08 19:48:55 +02:00
len
aebc9a3b9e Update metadata now ignores only completed manga setting 2016-10-08 15:52:02 +02:00
len
7b28614c37 Ignore chapters with duplicated name. Fixes #483 2016-10-06 20:02:22 +02:00
len
4524c705da Add simple method for preference bindings 2016-10-06 19:39:59 +02:00
len
1f70be688a Allow to refresh the entire library info (fixing empty covers after restoring backups). Closes #462 2016-10-06 19:23:59 +02:00
len
500eedaab7 Explicitly remove read phone permission 2016-10-03 21:15:59 +02:00
2d2ff0a29d Download queue will now be reset if negative. (#485) 2016-10-02 11:59:10 +02:00
len
6d0689fe6c Keep compatibility with YAML sources. Reorder methods 2016-09-30 21:29:03 +02:00
0b3dda18d3 Implement latest updates. (#472) 2016-09-30 21:11:51 +02:00
len
09a8a494a0 Remove unneeded call 2016-09-29 21:46:11 +02:00
len
11ac4df5d7 Bump dependencies, remove unused resources 2016-09-29 19:53:59 +02:00
d352405ba6 Open from homescreen/add shortcut to launcher (#435)
* Add very basic "Add to homescreen" action in manga info fragment.

* Fix open from homescreen opening current manga (if a manga is open).
Code cleanup.

* Improve fix for "Opening from homescreen opens currently open manga if a manga is currently open" and fix "Going back to the main app via a Manga opened through a shortcut repeats the launcher open animation".

* Implement custom icons, add star icon and optimize some things.

* Remove Tachiyomi and custom image icon types.

* Move icon creation task into an observable.
Added some extra error handling.
2016-09-29 18:38:29 +02:00
len
a81609fd2c Fix #480 ? 2016-09-25 23:04:43 +02:00
bf05952582 Gradle custom script 'app/custom.gradle' (#473) 2016-09-23 20:50:01 +02:00
596a24fce8 Added option to share your favorite manga (#477) 2016-09-22 21:36:40 +02:00
len
9f20e40257 Update kotlin and gradle build tools 2016-09-22 19:49:47 +02:00
8be67a4431 Custom color filter for reader (#434)
* [WIP] Custom color filter for reader

* Improvements

* temp image to prevent build error

* Shift all the bits

* Some improvements. Removed DiscreteSeekBar

* Improvements

* API 16 + fixes

* Reduced lag. Fixed brightness value being reset to 0

* Small fixes
2016-09-21 21:26:08 +02:00
58a2f7a874 Hide catalogues (#466)
Hide catalogues
2016-09-18 21:12:12 +02:00
len
cb92143613 Merge anilist backend 2016-09-18 11:50:52 +02:00
len
08e26aa30d Fix library update interval not being updated properly 2016-09-17 11:15:18 +02:00
8e3ffe87b8 Fix broken link (#470) 2016-09-16 11:31:34 +02:00
len
20e2bf9682 Place restrictions above category selection 2016-09-15 18:46:51 +02:00
len
8512f97386 Show default message when no categories selected 2016-09-15 18:39:16 +02:00
len
3ce880bc62 Ignore a random crash when closing the reader 2016-09-15 18:25:10 +02:00
len
72ae243fa2 Remove debug log 2016-09-15 18:04:36 +02:00
len
91829b0e7d Select categories for global update 2016-09-15 18:01:07 +02:00
len
7c3cd10696 Notify first page change 2016-09-11 16:00:06 +02:00
24bdee626f parse manga from the future (#458) 2016-09-09 19:20:24 +02:00
len
6a30a75e3e Upgrade dependencies, use new Timber's overloaded method for errors 2016-09-08 18:30:29 +02:00
ccdc336112 Complete auto updates checker (#449)
* Complete auto updates checker

* Use GcmTaskService for the periodical updates checker

* Persist task across reinstalls

* Hide setting instead of disabling

* Minor refactor
2016-09-07 19:44:55 +02:00
len
a4b71f4d11 Minor UI fixes 2016-09-06 21:22:56 +02:00
len
c3f61e86b7 Improve performance with big images. Feedback is appreciated. 2016-09-06 20:42:24 +02:00
d8d93ee344 Added read filter to chapter select. (#431)
* Added read filter to chapter select.
* Can now select how far back the chapter should be deleted after read.
2016-09-05 11:08:16 +02:00
8ffff44454 Merge pull request #441 from icewind1991/more-eng-filter
Add genre filter support for the remaining English sources
2016-09-03 12:54:20 +02:00
len
568b90d0b4 Fix #446 2016-09-03 11:05:32 +02:00
len
46e09d174b Travis fix. Update gradle 2016-09-01 20:05:11 +02:00
1698a85e99 Add filter support to readmangatoday 2016-08-31 23:19:03 +02:00
c9b62209c2 Add filter support to mangasee 2016-08-31 23:12:25 +02:00
b280d6a76b Add filter support to mangahere 2016-08-31 22:40:12 +02:00
29993e6412 Merge pull request #438 from Taumer/ru_parsers_genre_filter
Implement genre filter for Readmanga and Mintmanga
2016-08-31 19:16:08 +02:00
2a5edf4547 Implement genre filter for Readmanga 2016-08-30 14:23:47 +03:00
d58c517a6c Implement genre filter for Mintmanga 2016-08-30 14:22:55 +03:00
50136c319f MAL switched to SSL/HTTPS (#437)
Changed the URL for myanimelist.net to use HTTPS, as API endpoints are using HTTPS/TLS as of August 25.
2016-08-30 10:28:10 +02:00
2fb3b50535 Add genre filter for catalogue (#428)
* Add genre filter for catalogue

* Implement genre filter for batoto

* hardcode filters for sources

* swtich filter id to string

* reset filters when switching sources

* Add filter support to mangafox

* Catalogue changes

* Indefinite snackbar on error, use plain subscriptions in catalogue presenter
2016-08-28 22:59:00 +02:00
4171e87b4b update Mangasee chapter selector (#429) 2016-08-28 11:38:37 +02:00
len
60b3036037 Rename fragment to view 2016-08-22 12:55:31 +02:00
len
dfb2487640 Library views recycling 2016-08-22 12:54:16 +02:00
len
97454ca162 Disable shared holders for now 2016-08-01 00:09:34 +02:00
len
4200409f79 Fix crashes introduced yesterday 2016-07-31 14:07:12 +02:00
len
b6a06189fb Fix text overlapping, make icons a bit bigger 2016-07-31 01:01:25 +02:00
len
be521804c8 Fix inverted if condition 2016-07-31 00:05:05 +02:00
len
e95fcf6172 Dynamic recyclerview inflation for the library view and better swap handling 2016-07-30 23:54:32 +02:00
len
fbd2235a51 Recycle view holders in library. Format fixes 2016-07-30 20:21:01 +02:00
len
31b1b83606 Fix #408 2016-07-30 17:43:16 +02:00
len
a5d4f63281 Set jdk 8 in travis 2016-07-30 16:40:35 +02:00
len
328f9a70d3 Fix robolectric tests 2016-07-30 16:25:23 +02:00
len
df2b1dbeb1 Update travis 2016-07-30 16:18:51 +02:00
len
f768393a4b Bump dependencies, set target sdk 24 2016-07-30 16:04:43 +02:00
len
c0a0d60c87 Replace page fragments with views 2016-07-30 15:51:49 +02:00
9cf5a4cac0 Minor Improvements (#405) 2016-07-28 01:01:56 +02:00
f21a030cf8 Added the ability to view the library as a list (#394)
* Added in the ability to view the library as a list

* reverted LibraryAdapter and renamed libraryToggleViewEvent to LibraryToggleViewEvent for consistency

* removed LibraryToggleViewEvent and directly subscribed to option change

* fixed the toggleView subscription

* Made the library list item layout more compliant with material design

* Changed unread text style and removed background
2016-07-27 17:37:36 +02:00
len
74e3d387eb Release v0.2.3 2016-07-24 15:41:58 +02:00
len
8f83f497d5 Update history custom put resolver 2016-07-23 15:41:47 +02:00
len
6999fa858e Fix #400 2016-07-23 12:09:06 +02:00
len
8c1bedf796 Back button now returns to start screen. Also fix #356 2016-07-20 19:09:28 +02:00
len
1090c04fe3 Remove deprecated calls and fix a potential race condition 2016-07-18 21:01:51 +02:00
33b04427d5 Added a startup screen preference option (#395)
* Added a startup screen preference option

* changed string and keys to be consistent
2016-07-18 19:58:18 +02:00
len
f7bb356abd Fix exception thrown when Batoto search is empty 2016-07-16 17:25:22 +02:00
len
e16bf0698e Minor fix 2016-07-15 18:22:24 +02:00
len
e6190683dd Observable calls can now be retried, previously all retries were failing 2016-07-10 12:14:30 +02:00
len
e08e41ae0d Remove most unused settings from the reader (keep screen on and page transitions), they are still available in the app's settings. Also lower minimum brightness to -75% 2016-07-08 22:31:46 +02:00
len
5f1a89df63 Remove newThread usages, it probably fixes random crashes 2016-07-08 18:23:03 +02:00
len
f15df40a54 Add an overlay on top of the reader to simulate a lower brightness. Closes #362 2016-07-07 23:18:22 +02:00
len
a32e0e4ec5 Fix #361 2016-07-04 00:27:45 +02:00
len
3e8ac6b2d0 Fix for #361? 2016-07-03 21:48:55 +02:00
len
50a773f456 Fix YAML parser crashing the app on Kitkat and lower 2016-07-03 21:33:07 +02:00
len
42484d718a And a few more crashes fixed in preferences 2016-07-03 21:19:34 +02:00
len
81887000a8 Fix a few crashes 2016-07-03 21:04:09 +02:00
len
987473df44 Minor changes 2016-07-03 18:49:02 +02:00
len
3680eb0bf5 Recently read improvements: Open next chapter if read, local date formatting 2016-07-03 17:58:39 +02:00
len
3dbdc495e7 Minor changes 2016-07-03 14:25:51 +02:00
466515c801 Implement "Wie Manga!" (#379)
* Implement Wie Manga!

* Unnecessary import
2016-07-02 22:16:20 +02:00
len
e198f7e671 Add icons for settings 2016-07-02 22:14:04 +02:00
len
5fe1799dab Fix #333 2016-07-02 14:12:52 +02:00
len
ce7118084a Downloads view now uses a copy of the original queue. Fixes #351 and some crashes while scrolling and removing a download from the queue 2016-07-01 18:30:46 +02:00
len
06786322ca Bump dependencies 2016-07-01 01:52:05 +02:00
len
130b7501d1 Remove no predictive animations. Upgrade Kotlin to 1.0.3 2016-07-01 01:39:57 +02:00
len
864f001c3e Add portuguese translation by @MrAmnesiac 2016-06-30 16:10:30 +02:00
len
1553ce973f Ignore the first spinner selection 2016-06-30 13:02:14 +02:00
72811e59f5 Spanish UI translation (#365)
Added spanish translation
2016-06-29 15:32:05 +02:00
4c1da3575b Cleanup - squid:S1155 - Collection.isEmpty() should be used to test for emptiness (#371) 2016-06-29 15:31:41 +02:00
05c0516a57 New reader menu (#368) 2016-06-27 16:46:31 +02:00
fe6dff9086 Handle a missing page list in MangaHere (#366)
This typically happens when a manga is pulled from their catalog (I tested it on Nisekoi). Previous behavior led to a NullPointerError, now gives an empty page list.

Giving a reason to the user beyond "Empty Page list" would be a good idea in the future (this seems to be one holdup for #220), but there doesn't seem to be an obvious place to put it without touching the base classes.  In the meantime, this is far more informative than null errors.
2016-06-25 13:01:44 +02:00
len
b6df5e6ee6 Reader fixes (MAL not updating in certain scenarios) 2016-06-24 13:39:34 +02:00
3ee5774870 Use Cloudflare client for ReadManga.Today (#363) 2016-06-23 14:05:20 +02:00
c8fbb96f49 Mangasee as image source (#355)
* Mangasee as image source

* revert

* Mangasee source refactoring
2016-06-20 15:37:35 +02:00
len
143303f7df Parser improvements 2016-06-20 00:57:29 +02:00
len
585f7ec17d Remove getAbsoluteUrl method 2016-06-18 17:37:41 +02:00
len
9beeca652f Rewrite preferences with a modified support library v7 2016-06-16 20:52:51 +02:00
len
cd92569355 Restart inject module when the app is created 2016-06-15 17:58:28 +02:00
len
a82e1d0e45 Remove unneeded annotations and some cleanup 2016-06-15 17:47:44 +02:00
len
5ad06df4ac Fix chapters with 1 page not marked as read 2016-06-15 16:47:59 +02:00
len
5cfd5da338 Convert some classes to Kotlin 2016-06-15 16:37:48 +02:00
len
b1d7167112 Bump dependencies 2016-06-15 13:18:27 +02:00
len
a475ecec4d Test package in Kotlin 2016-06-15 12:53:12 +02:00
5c98e020f4 Merge pull request #350 from inorichi/dev
Rewrite DB models, tests and add a chapter loader.
2016-06-15 12:31:42 +02:00
len
eed295587d Fix tests 2016-06-14 15:17:44 +02:00
len
237af4b07d Fix dependency injection and use custom models extending DB ones 2016-06-14 15:17:37 +02:00
len
658860fdff Add chapter loader, drop non seamless mode 2016-06-14 15:15:31 +02:00
len
21ba371a32 Replace Dagger2 with Injekt, reorganize dependencies 2016-06-14 15:13:48 +02:00
len
589160242e Rewrite database models in Kotlin 2016-06-14 15:11:23 +02:00
4de8b6e9a8 Update Mangachan address and fix loading covers after update (#347) 2016-06-11 15:59:04 +02:00
len
e79d536f33 Update readme 2016-06-10 20:48:43 +02:00
len
9e90096328 Match release version 2016-06-10 20:31:33 +02:00
f0a382c21a Improve regex for pages from Readmanga and Mintmanga (#345) 2016-06-09 19:48:23 +02:00
len
682a2c7546 Delete file when exception is thrown 2016-06-09 15:46:08 +02:00
len
2d1e85f280 Fix scroll position with many categories. Closes #332 2016-06-09 14:27:11 +02:00
len
dbec4fc15e Cloudflare fix. Closes #344 2016-06-09 11:32:24 +02:00
95cd77e749 Multiple quality improvements - squid:S1213, squid:S1943, squid:S1066 (#342) 2016-06-08 08:44:12 +02:00
1f8126e2af Use cardBackgroundColor instead of android:Background (#339) 2016-06-07 20:59:43 +02:00
86db7497e9 Small card fixes (#338) 2016-06-07 20:36:40 +02:00
172305fc6a Wrong card background fix + bump gradle version (#337) 2016-06-07 20:14:36 +02:00
dad9dcd742 Improve getAbsolutUrl method (#336)
Also fix Mangachan most popular pages
2016-06-07 20:04:50 +02:00
len
59b90a94d0 Remove covers on error. #334 2016-06-06 20:45:22 +02:00
len
93fc5944f3 Remove unneeded casts 2016-06-06 16:53:58 +02:00
len
7039216eae Manual mappings. Code generation on java classes (better compilation times) 2016-06-06 16:27:24 +02:00
7ba898f701 Added recently read tab (#316) 2016-06-06 15:26:56 +02:00
len
11262f86f9 Release 0.2.2 2016-06-06 14:43:02 +02:00
len
d1db2d60ec Fix #329. Add confirmation before removing favorite manga on multiple selection. Add new proguard rules 2016-06-05 23:40:27 +02:00
len
156e43290e Remove unneeded swipe refresh 2016-06-05 11:54:35 +02:00
len
6687c80b2d Minor changes 2016-06-05 11:14:40 +02:00
1fbec7bf3d Added improvements for RecentChapters. Closes #320 (#324) 2016-06-04 17:50:44 +02:00
len
6196480d1d Minor improvements 2016-06-03 18:25:47 +02:00
len
2723aeeb5c Better error feedback. Closes #325 2016-06-03 12:37:07 +02:00
len
728ab18017 Make active page nullable. Fixes #326 2016-06-03 12:25:39 +02:00
len
0714fdc7e6 Fix #328 2016-06-03 12:09:38 +02:00
7bfdfee27b Merge pull request #323 from NoodleMage/season
Added chapter recognition for "season" case
2016-06-02 16:46:26 +02:00
10ec3a9b0c Added chapter recognition for "season" case 2016-06-02 16:39:19 +02:00
eec728f162 Rename "Only update incomplete manga" string to "ongoing" (#318) 2016-06-02 14:42:40 +02:00
len
1ac8ef5341 Reader fixes and minor changes 2016-06-02 14:11:10 +02:00
len
18cdddf433 Minor improvements for sync services 2016-05-31 16:14:32 +02:00
len
02a697031f Chapters FAB doesn't depend on the recognized chapter number. Cloudflare resolver fix 2016-05-28 19:10:09 +02:00
len
6beff242b0 LoginSource moved to an interface 2016-05-28 00:20:59 +02:00
len
46cc078e93 Downloading chapters now always add them from the beginning 2016-05-27 20:11:37 +02:00
len
9aa6da0642 Default headers are bad 2016-05-27 18:08:05 +02:00
len
8bd20c39aa All network calls are now done directly through the client 2016-05-27 17:17:30 +02:00
len
efd36388b0 Fix network unsubscription crashes, refactor network methods 2016-05-27 16:52:44 +02:00
len
79bb207a8d Use async method for network calls, trying to fix a crash 2016-05-27 13:56:18 +02:00
len
0fe350af9a Minor classes restructuration and optimize imports 2016-05-27 13:21:21 +02:00
len
4e784cd7c3 Minor fixes 2016-05-26 16:03:55 +02:00
len
12d6919421 Do not parse -2 chapter number 2016-05-26 15:52:12 +02:00
len
79ec4faddb Always close requests' response body 2016-05-26 15:16:42 +02:00
6603c0b990 Merge pull request #239 from inorichi/rewrite-source
Rewrite source
2016-05-26 15:14:36 +02:00
len
b5dbdd1774 Change default sorting method 2016-05-26 15:10:31 +02:00
len
a08cea9df8 Show changelog when new version is installed 2016-05-26 15:10:28 +02:00
5d9c817461 Implement ReadMangaToday with the new source 2016-05-26 10:22:43 +02:00
f95c9a12c9 Reimplement russian sources 2016-05-26 10:22:43 +02:00
len
015257fe75 Implement Mangafox and Mangahere with the new source 2016-05-26 10:22:42 +02:00
len
dd5692bb2d Rewrite sources. Implement Batoto and Kissmanga 2016-05-26 10:22:39 +02:00
len
bc3d5dc863 Add support's library custom tabs 2016-05-25 17:33:58 +02:00
a4b6003e58 Wrong versionName when not building latest version (#314) 2016-05-25 16:10:03 +02:00
8086d1db46 squid:SwitchLastCaseIsDefaultCheck - switch statements should end with a default clause (#306) 2016-05-25 00:09:38 +02:00
26f4f53ec2 Fixed spinner + improved ripple (#310) 2016-05-25 00:09:24 +02:00
len
6af78418a4 Fix an issue with seamless mode and chapters with less than 5 pages #291 2016-05-23 12:10:24 +02:00
len
f629db3c10 Exclude manga from unknown sources from the library 2016-05-22 23:40:44 +02:00
len
af0cf9e52d Load next/prev chapter depending on the sorting method 2016-05-21 22:04:07 +02:00
len
e885469504 Allow to change sorting mode from the chapters fragment 2016-05-21 22:04:06 +02:00
len
ca7e5260f0 Tables classes in Kotlin. Rename COLUMN -> COL 2016-05-21 22:04:06 +02:00
len
dba64f849b Database support for ordering chapters like the source 2016-05-21 22:04:05 +02:00
02e43bafd6 Merge pull request #303 from NoodleMage/myanimelist_cardview
UI tweaks
2016-05-21 17:45:49 +02:00
len
637dda2e22 Make status bar transparent on API >= 21 2016-05-21 15:43:48 +02:00
575eaee1d2 UI tweaks 2016-05-19 16:29:25 +02:00
48a1e8f74c pmd:UseIndexOfChar - Use Index Of Char (#300) 2016-05-17 21:49:13 +02:00
len
4d65038ad3 Optimize imports 2016-05-15 23:06:11 +02:00
len
6e8a41f898 Kissmanga loading through Cloudflare. A lot of refactoring was needed 2016-05-15 20:46:58 +02:00
len
8da11dbdb9 Fix MAL not binding a manga. Upgrade Kotlin to 1.0.2 2016-05-13 13:37:08 +02:00
len
70fabf6a6b Bump dependencies and support library 2016-05-12 20:15:15 +02:00
len
43fafbc747 Fix a crash when trying to change the downloads directory on some devices 2016-05-12 16:45:35 +02:00
len
88e64c878b Double the distance required to trigger category update and remove overflow menu setting 2016-05-12 15:30:57 +02:00
0ad9e4af0b Fix #267 (#299) 2016-05-12 14:28:44 +02:00
a6df745daa Rewrote ChapterRecognition to Kotlin. (#293) 2016-05-11 15:33:14 +02:00
len
c64bd81339 Build debug in travis 2016-05-10 16:13:28 +02:00
c31f0b4bb3 Change travis gradle task, small doc update (#295)
* Use gradle task build in travis

* Update CONTRIBUTING.md
2016-05-10 13:42:08 +02:00
len
136136d055 Trying workaround for #296 2016-05-09 13:29:46 +02:00
len
c20d86e5c0 Category update can only be triggered when the list is at the top 2016-05-09 13:23:57 +02:00
len
7ca99f749b Remember last active category. Closes #261 2016-05-08 18:11:09 +02:00
len
7cc4405c09 Swipe down updates active category. Closes #292. Not sure I'll keep this 2016-05-08 17:27:49 +02:00
len
62d5deaa6f Allow to open manga in the browser. Closes #157 (Doesn't work with kissmanga) 2016-05-08 15:20:55 +02:00
len
7f5879ed6f Add confirm dialog for multiple deletion. Closes #155 2016-05-08 14:58:21 +02:00
len
a0f7761a37 Minor changes 2016-05-07 23:50:05 +02:00
ed77c60283 Added download notifications, resolves #260 (#289) 2016-05-07 23:09:14 +02:00
len
8f144316a6 Fix a crash when updating active category and no categories available 2016-05-06 15:44:01 +02:00
len
e73eed4a9b Fix catalog covers' flickering when adding a page 2016-05-06 14:28:39 +02:00
len
9de3da33aa Fix recent chapters menu button not showing on high dpi screens 2016-05-05 20:58:19 +02:00
len
0de214c3b5 Delete from the download queue on the main thread. It could fix some crashes 2016-05-05 16:05:36 +02:00
len
1226023dc2 Allow to update one category 2016-05-05 00:37:03 +02:00
len
5e24054a0b Database queries are now separated by table. Improve how the app creates downloads 2016-05-02 22:36:10 +02:00
af2b886599 Fix F-Droid not recognizing update (#287) 2016-05-02 14:53:43 +02:00
len
1d1e5f1f99 Toggle reader menu with the menu button, closes #286. Fix incorrect drawer selected item when using back button 2016-05-01 21:53:00 +02:00
len
49628e9cf5 Fix recent crashes 2016-04-30 01:44:53 +02:00
len
47bc1f7a9f Remove RelativeLayout from covers for better performance 2016-04-29 20:00:03 +02:00
len
f9783407bd Downgrade RxJava. Closes #285 2016-04-29 14:26:30 +02:00
len
74ffa14304 Bump dependencies 2016-04-29 00:22:41 +02:00
len
e881488bcc Refactor and convert to Kotlin base classes. Fix FAB behavior 2016-04-28 21:54:54 +02:00
97ee7b81af Release version manual update hotfix. (#283) 2016-04-28 18:46:12 +02:00
ff6eefe1c4 pmd:ImmutableField - Immutable Field (#282) 2016-04-28 18:46:05 +02:00
9f546d13c2 squid:S1118 - Utility classes should not have public constructors (#281) 2016-04-28 18:45:39 +02:00
2e6fc70353 Auto number of latest release, shorter versionCode (#280) 2016-04-27 22:18:36 +02:00
270cacb1d7 squid:S2039 - Member variable visibility should be specified (#279) 2016-04-27 22:18:25 +02:00
len
e2ecf0ce5f Release 0.2.1 2016-04-27 14:06:45 +02:00
len
5d396bfb7c Make query non nullable, it fixes some bugs in the catalogue 2016-04-27 00:58:05 +02:00
len
de6cc8394e Fixed some crashes in the catalogue and the reader 2016-04-27 00:23:06 +02:00
4b7159648a Merge pull request #257 from NoodleMage/update_improv
Rewrote UpdateDownloader to Kotlin + AC fixes
2016-04-27 00:20:50 +02:00
eb9c5f95db Removed automatic update + duplicate fix 2016-04-26 22:56:49 +02:00
55e9d2880c Rewrote UpdateDownloader to Kotlin
Added auto update check (every 12 hour)
Warning message optional fix #256
Lots of bug fixes!
2016-04-26 20:57:05 +02:00
len
ec9c19ce7d Use a thread-safe list for downloads 2016-04-26 14:00:22 +02:00
len
31731e8f26 Fix a crash in older android versions 2016-04-25 23:43:32 +02:00
len
bfb12bc7c1 Minor changes to fix a possible crash in the downloads view 2016-04-24 23:32:49 +02:00
len
4befcf3819 Fix #277, library not updating 2016-04-23 15:39:41 +02:00
cb58145361 Allow setting versionCode in parameter (#276)
Allow easier debug versionCode change
2016-04-23 13:17:48 +02:00
len
b83efd90a8 Slightly increase library view performance by caching typefaces 2016-04-22 19:41:59 +02:00
len
9f0da3f1d6 Upgrade to nucleus 3 2016-04-22 14:28:06 +02:00
len
50ae08ed8d Back button now returns to library. Closes #252 2016-04-21 16:31:23 +02:00
len
5385642a5b Downloads now retry requests after some seconds. Closes #271 2016-04-21 15:57:47 +02:00
len
0a27d4e185 Add an option to reencode images under the advanced tab. #262 2016-04-21 15:31:07 +02:00
len
bd8b9febd2 Minor changes 2016-04-21 01:04:46 +02:00
len
a30705f197 Oops... nobody noticed being unlogged from batoto? 2016-04-20 17:31:31 +02:00
len
877032a757 Fix incomplete downloads. Closes #264 2016-04-20 17:10:10 +02:00
len
19bf47b6d2 Release resources before trying to delete an incomplete file #264 #211 2016-04-19 21:04:28 +02:00
len
a9bfeb058b Revert "Temporarily include nucleus in the project"
This reverts commit 447dfd1e3c.
2016-04-19 14:11:03 +02:00
len
9213fc6999 Always close response body 2016-04-19 14:08:35 +02:00
len
447dfd1e3c Temporarily include nucleus in the project 2016-04-19 12:58:33 +02:00
len
638d3a32cf Also use manga per row setting in catalogue 2016-04-18 20:33:09 +02:00
len
17c59657c3 Allow to unbind manga, closes #258. Fix some network calls leaking 2016-04-18 20:14:50 +02:00
len
81bce8ef76 Mark common categories when moving them. Closes #135 2016-04-18 19:20:14 +02:00
len
78314077bb Fix custom brightness turning off the screen. #106 2016-04-18 17:36:27 +02:00
len
a7840bc247 Rewrite PreferencesHelper. Allow to customize navigation with volume keys and tapping. Closes #251 and closes #129. 2016-04-18 17:29:46 +02:00
6d0254c5e5 Fixed backup/restore for 3rd party applications (#255)
* Same MIME for restore/backup, so 3rd party applications like Google Drive find the proper files.
MIME changed to proper type for json files.

* MIME type for restore temporaly on "application/*" so the cached file can also be chosen
2016-04-18 13:40:34 +02:00
06681a3db7 squid:S1854 - Dead stores should be removed (#253) 2016-04-18 13:40:26 +02:00
len
55de2b7d97 Remove gradle properties and fix travis build 2016-04-17 17:33:47 +02:00
len
065ada3d17 Update readme 2016-04-16 16:05:51 +02:00
len
0ee2bf5254 Release 0.2.0 2016-04-16 16:01:12 +02:00
len
0fe0088ff0 Also use no predictive animations for AutofitRecyclerView 2016-04-15 15:52:53 +02:00
len
492a24ec17 Use always 3 characters for downloaded pages. Fixes #181 2016-04-15 15:14:02 +02:00
len
17a6ea973e Some bugfixes 2016-04-15 14:58:36 +02:00
len
deaba48431 Fix a crash on older devices 2016-04-14 17:37:21 +02:00
len
eb662f1234 Fix some crashes when restoring backups 2016-04-14 17:24:34 +02:00
len
5ecdecea98 Skip memory cache for images in catalog 2016-04-13 17:59:05 +02:00
len
b4277faf90 Not yet #187 2016-04-13 17:42:53 +02:00
len
09902566ad Fix for #187? 2016-04-13 16:45:39 +02:00
len
dc80a5ffbd Minor XML refactoring 2016-04-13 16:42:59 +02:00
b1b97c19d4 Added option to check if connected to power before updating. closes #192 (#229) 2016-04-13 14:08:07 +02:00
len
46a0820e5c Fix scrolling issue 2016-04-13 02:08:43 +02:00
len
6cbdbb5be3 Fix #248 2016-04-12 22:41:45 +02:00
len
e753539c6d Bump subsampling version 2016-04-11 19:05:33 +02:00
len
b8d1a88623 Changes in cover cache. Store covers in external cache dir 2016-04-11 18:50:31 +02:00
len
b84635ffec Fix last commit 2016-04-10 20:14:35 +02:00
len
ed2fd00603 Style toolbar's spinner with light theme 2016-04-10 19:47:06 +02:00
len
af20c613a4 Hide upload date if not parsed 2016-04-10 17:53:02 +02:00
len
b27669ee32 Remove unused strings 2016-04-10 17:49:48 +02:00
len
81d39ea272 Remove unused attrs and colors. Theme preference dialogs on API >= 21 2016-04-10 17:34:10 +02:00
len
840437580f Fix for #144? 2016-04-10 16:19:05 +02:00
len
936ede9aba Light and dark theme are now using different accent colors 2016-04-10 15:48:57 +02:00
len
2fa5d0cbaf Add presenter subscriptions to the subscription list when using custom subscribe methods 2016-04-10 04:32:43 +02:00
len
e28f69cddf Dark theme now uses accent color for drawer items. #222 2016-04-10 01:37:42 +02:00
len
11f6c44442 Make status bar transparent on API > 21 properly. Snack function moved to an extension method in View 2016-04-10 00:59:12 +02:00
8c9db2db61 Merge pull request #245 from j2ghz/patch-1
Add caching to Travis
2016-04-09 17:57:55 +02:00
7e7d27505a Add caching to travis
https://docs.travis-ci.com/user/languages/android#Caching
2016-04-09 17:57:01 +02:00
len
b4211ddc0c Remove unneeded repository 2016-04-09 17:46:42 +02:00
len
ff906e8ee7 Move modified dependencies to another repository. Reorganize dependencies 2016-04-09 17:33:21 +02:00
c1ebccd0f4 Merge pull request #244 from j2ghz/master
Travis builds
2016-04-09 14:10:18 +02:00
9c223b48a3 rearrange badges on README 2016-04-09 13:37:11 +02:00
16037dd9bd try https://github.com/travis-ci/travis-ci/issues/4185 2016-04-09 11:52:36 +02:00
742e8c9f50 upgrade buildToolsVersion for SubsamplingScaleImageView 2016-04-09 11:45:07 +02:00
194cdf3b5f Travis 2016-04-09 11:38:59 +02:00
len
5fbeeade94 A few more crashes fixed 2016-04-08 22:57:31 +02:00
len
72f029b57f Fix #242. Minor changes 2016-04-08 18:01:33 +02:00
len
67c4781376 Upgrade support library. Switch gradle build tools and AS to 2.0. Adapt code to new support lib 2016-04-08 16:18:41 +02:00
len
fe49286d97 A few more crashes fixed 2016-04-07 20:39:37 +02:00
len
4196a0f585 Minor changes trying to fix a crash 2016-04-07 14:43:02 +02:00
len
a6a9b13545 Fix proguard rules 2016-04-06 21:31:35 +02:00
len
fa8d0946e9 Remove unneded dependency 2016-04-06 20:40:37 +02:00
len
1844b8c5a2 Add commit number to version name in debug version 2016-04-06 17:10:43 +02:00
len
7c503648ff Minor changes. Also fix #240 2016-04-06 16:45:13 +02:00
len
a598ebf72f More crash fixes 2016-04-06 02:18:04 +02:00
len
d8ac35d259 Crash fixes 2016-04-04 23:25:50 +02:00
len
5029e4a28c Fix a bug when opening a chapter from the recents tab and changing the viewer from the reader would not update chapters for that manga anymore 2016-04-03 18:47:56 +02:00
908e60dea4 Merge pull request #238 from j2ghz/patch-1
Fix formatting issues when people ignore instructions
2016-04-02 14:35:19 +02:00
d577bf300c Fix formatting issues when people ignore instructions 2016-04-02 14:34:27 +02:00
len
579a606f93 Upgrade dependencies. Downgrade material dialogs to avoid crash on older android versions 2016-04-02 14:30:21 +02:00
len
ac15c0c57e Fix an error when restoring backup 2016-03-31 12:17:40 +02:00
len
0c0372dc51 Fix #236 2016-03-30 18:17:00 +02:00
len
723c0e99a5 Remove unneeded class 2016-03-30 17:19:02 +02:00
len
e04596c668 Minor UI fixes 2016-03-30 10:30:13 +02:00
a809b05808 Merge pull request #212 from inorichi/backup
Support backups
2016-03-29 20:54:15 +02:00
da44dc3fb5 Support backups 2016-03-29 20:48:28 +02:00
len
06c63f1207 Fix a crash in the reader when restoring the instance. Removed capitalization on each word 2016-03-29 13:47:32 +02:00
c3425346b7 Merge pull request #234 from NoodleMage/sort
fix #99
2016-03-29 10:28:49 +02:00
fe8e4a4f54 fix #99 2016-03-28 14:07:24 +02:00
len
abab778e2e Try with a bigger heap to avoid OOM crashes 2016-03-26 13:28:22 +01:00
03ecb2fe13 Merge pull request #227 from NoodleMage/issue_42
Fix Issue #42
2016-03-26 13:14:16 +01:00
a78f89d4eb Can now choose to automatically remove chapter after reading (or previous). Fix #42 2016-03-25 23:05:50 +01:00
499c2213ee Implements delete chapter when set as read for issue #42 2016-03-25 23:05:46 +01:00
abbc7b572a Implements Download next 1/5/10/all chapters for issue #42 2016-03-25 23:05:45 +01:00
len
bf1fdda651 Fix crashes on settings 2016-03-25 15:50:25 +01:00
len
218ea7267d Remove lambdas 2016-03-23 20:28:44 +01:00
70cf085df9 Merge pull request #225 from NoodleMage/manga_chapters
Updated manga chapters UI
2016-03-23 19:51:20 +01:00
71783657af Merge pull request #201 from na-ji/master
Implement parser for readmanga.today
2016-03-23 19:45:29 +01:00
137c21e6c9 Added animation 2016-03-23 19:38:12 +01:00
bc473055b9 Changed fragment_manga_chapters.xml. Fix #221 2016-03-22 20:56:19 +01:00
3d67607768 Merge remote-tracking branch 'upstream/master' 2016-03-22 20:02:10 +01:00
len
ce271649ac Page number indicator now transparent 2016-03-21 15:58:59 +01:00
len
0078cb88c3 A few crashes fixed 2016-03-21 14:50:02 +01:00
len
19cb548e18 Fix last commit 2016-03-20 23:55:44 +01:00
len
b3cf7dbc14 Implement #226 2016-03-20 22:17:23 +01:00
len
bbfe0a0cd1 Fix directory picker 2016-03-20 21:50:48 +01:00
len
92b3f90380 Fix a query 2016-03-20 01:37:04 +01:00
len
b09345f2e1 Downgrade RxJava for a while 2016-03-19 23:08:47 +01:00
len
0d41c60a38 Fix tests 2016-03-19 21:09:51 +01:00
len
5132f4850f Minor changes 2016-03-19 19:30:55 +01:00
len
53fae2939a Remove apt, add manual EventBusIndex (not sure if it works) 2016-03-19 18:39:18 +01:00
len
14f248546a Use kapt, remove retrolambda, migrate database and source to Kotlin 2016-03-19 17:48:55 +01:00
len
0d519b3d16 Reader presenter in Kotlin + remove Icepick 2016-03-19 15:14:51 +01:00
len
8e0a9d6d66 Fix crashes 2016-03-19 01:03:28 +01:00
len
b8bc3476f4 Fix last commit 2016-03-19 00:22:13 +01:00
len
6b326cfb79 All events in Kotlin 2016-03-19 00:16:27 +01:00
len
aaef738dda Download manager in Kotlin and fix another crash in reader 2016-03-19 00:11:34 +01:00
len
35748fc1f3 Raw queries in Kotlin 2016-03-18 23:23:56 +01:00
len
a122d817e8 Fix ACRA not attaching BuildConfig 2016-03-18 20:40:14 +01:00
len
4ccce424de Reader view in Kotlin. Upgrade gradle wrapper. Remove ButterKnife from the project 2016-03-18 20:21:30 +01:00
len
396a79899e Bump dependencies. Fix crash in reader 2016-03-17 21:02:40 +01:00
len
de53681d2b Fix reader theme 2016-03-17 20:35:45 +01:00
aacd42b9f6 Merge pull request #218 from NoodleMage/theme_update
Rewrote Nav Drawer to Kotlin + Dark Theme
2016-03-17 20:34:29 +01:00
5ef5f9b45f Rewrote Theme 2016-03-17 20:01:52 +01:00
98d420d5aa Rewrote nav drawer to Kotlin + UI updates
Added launch screen + new Header

Removed MaterialDrawer library. Implemented Nav Draw from Support Library
2016-03-17 20:01:33 +01:00
len
5b75818fc5 Different approach for #214 2016-03-16 19:52:19 +01:00
len
f49577bc77 Manga in Kotlin. Expect some errors yet 2016-03-13 22:06:32 +01:00
len
a87c65872c Fix login dialogs not showing the correct title 2016-03-11 16:16:56 +01:00
ed636d5e2f Merge pull request #171 from Taumer/ru_parsers
Implement parsers for Readmanga, Mintmanga and Mangachan
2016-03-11 13:57:11 +01:00
2ced70652b Implement parsers for Readmanga, Mintmanga and Mangachan 2016-03-10 23:59:22 +03:00
dc742f4b8d Merge pull request #209 from inorichi/source-languages
Support for sources from different languages
2016-03-10 16:23:09 +01:00
len
689f2e7fbf Support for sources from different languages 2016-03-10 16:15:48 +01:00
len
ba1dca1826 Fix #206 2016-03-10 16:05:26 +01:00
len
a07e4c69b6 Fix crashes with vector drawables on older Android versions 2016-03-08 17:39:59 +01:00
len
05adde552d Kotlinize some widgets 2016-03-08 01:22:56 +01:00
0ddbfd1036 Merge pull request #204 from NoodleMage/svg_all_the_way
Converted all icon drawables to vector + Removed Android-Iconics library.
2016-03-08 00:47:26 +01:00
8b45df37d2 Converted all icon drawables to vector.
Removed Android-Iconics library.
2016-03-08 00:35:29 +01:00
len
70e557575f Preferences ported to support library 2016-03-07 23:48:43 +01:00
bcbd541d48 Merge pull request #198 from NoodleMage/issue_27
Added filter options. fix for Issue #27
2016-03-07 14:59:28 +01:00
6383a745ff Fixed wrong download filter from commit #33386e2
Fixed another tab not in TabLayout error.

Drawable to Vector
Removed Filter... Toast
2016-03-06 22:27:30 +01:00
len
b89d6644d8 Performance improvements for library filters 2016-03-06 22:27:26 +01:00
8fbef4b4bb Can now filter unread manga + Code opt 2016-03-06 22:27:24 +01:00
d9f5a97d56 Can now filter downloads only on library view. Fix #27 2016-03-06 22:27:22 +01:00
len
e4ee03cb61 Allow to cancel update. #192. Needs testing 2016-03-06 20:58:15 +01:00
c2a65c71e1 Merge pull request #202 from beschoenen/patch-1
Hide clear button
2016-03-06 18:20:42 +01:00
len
8c456a2da4 Replace some image drawables with vector drawables 2016-03-06 18:18:09 +01:00
f856386bf7 Hide clear button
Hide the clear butten when all downloads have finished.
2016-03-06 17:51:25 +01:00
e1448eaeda Merge pull request #199 from j2ghz/patch-1
Prevent issues with bad formatting like #194 #196
2016-03-05 20:52:21 +01:00
3cf61f2f93 Update ISSUE_TEMPLATE.md 2016-03-05 15:10:11 +01:00
len
ff61282104 Readers in Kotlin. Also fix #193 2016-03-04 14:10:41 +01:00
len
b2fe9d7d4d Fix #196 2016-03-03 20:16:28 +01:00
len
16a5e6c01a Some base classes and preferences in Kotlin 2016-03-02 16:17:56 +01:00
306b1d74bb Merge pull request #190 from NoodleMage/kotlin
Rewrote Recent to Kotlin
2016-03-02 15:30:59 +01:00
a7e652f1f7 Rewrote Recent to Kotlin 2016-03-02 14:54:51 +01:00
len
e1a3ab2726 Readded chapters do not notify. Fix #188 2016-03-02 14:18:50 +01:00
len
ae9c412b6d Fix possible crashes similar to #191 2016-03-02 14:07:54 +01:00
len
fad0027e17 Fix #191 2016-03-02 13:37:54 +01:00
len
1d7f012fd1 Fix builds 2016-03-02 03:15:36 +01:00
len
ee4bf163ef Catalogue in Kotlin. Support library upgraded to 23.2.0. Downloads directory now shows a list of folders, it should fix #141. 2016-03-01 23:29:07 +01:00
fabdba4452 Settings in Kotlin 2016-02-27 17:49:22 +01:00
1a14fc5c48 Merge pull request #183 from NoodleMage/master
Rewrote IOHandler to Kotlin
2016-02-27 14:13:21 +01:00
fa5b64ce2e Rewrote IOHandler to Kotlin 2016-02-27 10:35:14 +01:00
4397a44b80 Fix broken tests after last commit 2016-02-26 18:34:44 +01:00
d4bb092543 Allow custom parsing of chapter number on sources 2016-02-26 18:29:08 +01:00
f73f0cc341 Download queue's UI in Kotlin 2016-02-26 18:10:13 +01:00
b95d0e2848 Bump dependencies. Move ReactiveNetwork to app module. 2016-02-26 15:56:56 +01:00
50b97fa28f Log message error when a request from the catalogue fails. 2016-02-25 16:33:30 +01:00
61c7feca87 Refresh adapter after the cover is changed. Some minor changes on categories. 2016-02-25 16:13:48 +01:00
4dde6d1a31 Merge pull request #182 from NoodleMage/cover_edit_fix
Cover change fix (hopefully :-)
2016-02-25 16:11:17 +01:00
9ac2f02885 Cover change fix (hopefully :-) 2016-02-25 16:02:58 +01:00
19eb77f049 Merge pull request #180 from inorichi/library-kotlin
Migrate UI library to Kotlin.
2016-02-25 15:42:31 +01:00
59276b7160 Migrate library to Kotlin. 2016-02-25 15:37:52 +01:00
9062e40ec5 Merge pull request #178 from NoodleMage/kotlin
Category rewrite + FAB rewrite to Kotlin
2016-02-25 13:27:21 +01:00
9f78c8f6b4 - Rewrote Category to Kotlin
- Moved category to ui
- Reworked Animation (smoother)
- Updated TextDrawable
2016-02-25 09:58:01 +01:00
144d315e27 rewrote ScrollAwareFABBehavior.java to Kotlin. Can now implement FABAnimationBase to create different FAB animations 2016-02-24 11:57:57 +01:00
34dc85e605 Allow to retry image when decoding fails or open in the browser. Fixes #177 and fixes #120. Also fix a bug where the current page was not restored when changing settings. 2016-02-23 20:02:49 +01:00
4ba0f343e3 Fix #179 2016-02-23 17:32:47 +01:00
4876eaafcc Fix #168 and fix #81. 2016-02-22 16:53:33 +01:00
16f6be3613 Probable fix for #168. Maybe #81 also. Needs confirmation. 2016-02-22 14:59:57 +01:00
da0c7d7484 Merge pull request #175 from j2ghz/patch-1
Add link to wiki about debug F-Droid
2016-02-22 12:45:50 +01:00
fbd2f86f00 Add link to wiki about debug F-Droid 2016-02-22 09:29:51 +01:00
a309b7a066 Remove problematic test 2016-02-21 23:21:53 +01:00
9ad9c275a4 Delete an old file 2016-02-21 23:14:32 +01:00
7a467d43df Fix tests after Kotlin merge (probably) 2016-02-21 23:13:22 +01:00
db97250db8 Merge pull request #169 from inorichi/kotlin
Partial migration of data package to Kotlin
2016-02-21 22:42:54 +01:00
c0452038f7 Partial migration of data package to Kotlin 2016-02-21 18:55:37 +01:00
110df59197 Release 0.1.4 2016-02-21 15:59:07 +01:00
75ae4081d8 Merge pull request #167 from j2ghz/patch-1
Fix link broken by PR #164
2016-02-20 23:36:24 +01:00
2efca050b3 Fix link broken by PR #164 2016-02-20 23:33:55 +01:00
37e119c4f2 Merge pull request #164 from j2ghz/patch-1
Create ISSUE_TEMPLATE.md
2016-02-20 22:07:47 +01:00
9786074119 Move github files to .github/ 2016-02-20 12:06:53 +01:00
d4876b426f Create ISSUE_TEMPLATE.md 2016-02-20 12:02:57 +01:00
8581e4667a Merge pull request #160 from NoodleMage/issue_118
Implements Issue #118 download from recent tab
2016-02-19 20:36:31 +01:00
b94f86765d Code cleanup, implements #118 2016-02-18 19:01:40 +01:00
ba0f3778ce Can now mark as read / unread 2016-02-18 17:40:12 +01:00
aac6b242a0 Can now delete manga from recent + added missing res files #118 2016-02-18 17:29:31 +01:00
dec9442a65 Can now download from recent tab. #118 2016-02-18 17:29:29 +01:00
b33da641d9 Fix crash in chapters list #159 2016-02-18 14:25:35 +01:00
96d498e7e5 Merge pull request #152 from icewind1991/chapter-parsing
Chapter recognition improvements
2016-02-16 21:08:10 +01:00
eee137a084 prefer numbers at the start of the chapter title if otherwise unparsed 2016-02-16 21:03:52 +01:00
5e834ae3be improve colon handling 2016-02-16 21:03:21 +01:00
dcfda61aba Always create nomedia file 2016-02-16 20:47:23 +01:00
5ac7f7057a Merge pull request #150 from NoodleMage/comments
Improved comments
2016-02-16 20:41:45 +01:00
ff46c61f63 Merge pull request #151 from icewind1991/chapter-recognition-fallback
Fix infinite loop when no chapter number is parsed
2016-02-16 20:41:34 +01:00
57b64a412e Fix infinite loop when no chapter number is parsed 2016-02-16 20:37:57 +01:00
1e81f75377 Possible fix for #120 2016-02-16 18:19:54 +01:00
1dd49a2ab1 Improved comments 2016-02-16 15:30:15 +01:00
1cd77a97a7 Merge pull request #143 from NoodleMage/fab_improvement
Moved edit to cover select and update manga info view
2016-02-15 23:28:04 +01:00
f820522a69 Show keep screen on in reader settings. Closes #146 2016-02-15 21:25:01 +01:00
3da613dedb Moved edit cover to library | Updated manga info view | Updated catalogue
grid
2016-02-15 16:59:24 +01:00
5c329d2314 Incorrect mark as read with seamless mode. #142 2016-02-14 15:35:58 +01:00
4c073e713d Merge pull request #139 from j2ghz/patch-1
Make CONTRIBUTING.md more visible
2016-02-13 18:08:57 +01:00
2832f4ae5e Update README.md 2016-02-13 17:57:17 +01:00
7690e8a53f Merge pull request #137 from NoodleMage/fab_improvement
FAB animation update
2016-02-13 14:53:51 +01:00
3a19f8e40b FAB animation update 2016-02-13 12:08:15 +01:00
a33b525f9e Merge pull request #136 from icewind1991/search-sort
sort by views for mangafox and mangahere search results
2016-02-12 22:01:43 +01:00
7d6ce46829 sort by views for mangafox and mangahere search results 2016-02-12 21:49:38 +01:00
a90a4bf80c Remove old orientation lock. Add orientation types to preferences 2016-02-12 21:22:54 +01:00
140bf8caee Allow to force a rotation 2016-02-12 19:36:00 +01:00
56a45f263e Strip html tags from batoto notice and directly throw an exception 2016-02-12 15:38:16 +01:00
01d6ddfafb Merge pull request #132 from icewind1991/batato-staff-notice
Show batoto staff notice if updating chapters failed
2016-02-11 23:32:32 +01:00
393b4916f6 Show batoto staff notice if updating chapters failed 2016-02-11 22:59:24 +01:00
cb3c3af865 Include reactive network as library 2016-02-11 14:16:36 +01:00
5a83976fa5 Remove unneeded dependency. 2016-02-10 21:15:02 +01:00
a81f6c3ac4 Trying to give write permissions on SD card 2016-02-10 15:41:59 +01:00
6846ce5bfb Increase maximum allowed scale on pagers 2016-02-10 15:19:31 +01:00
0c0ebe06e5 Volume keys scroll pages. Closes #95 2016-02-10 15:06:18 +01:00
e50c683159 Fix tests failing after upgrading EventBus 2016-02-09 22:07:14 +01:00
872af276ea Merge pull request #130 from icewind1991/chapter-number-parsing
Improve chapter number parsing
2016-02-09 21:50:32 +01:00
e6faee9779 handle chapters with part numbers 2016-02-09 21:23:57 +01:00
bc1ddd4379 fallback to parsing parts to handle arc numbers 2016-02-09 21:20:17 +01:00
e348d6c1cf Upgrade to EventBus 3 2016-02-09 21:19:11 +01:00
7835921045 Merge pull request #126 from beschoenen/downloading
Download features
2016-02-09 21:15:36 +01:00
1611a274b9 differentiate subchapters denoted by an alpha prefix 2016-02-09 20:57:26 +01:00
fa4a8204a4 prefer numbers without anything appended when parsing chapter numbers 2016-02-09 20:43:10 +01:00
5977e9f47f handle chapter versions which are attached to the chapter number 2016-02-09 20:26:51 +01:00
63d0161da5 move clear queue to presenter 2016-02-09 17:42:39 +01:00
d8b46c1969 set display mode title 2016-02-09 16:57:11 +01:00
f84731c2df cleanup chapter action menu 2016-02-09 16:50:26 +01:00
50d71d1395 clear the download queue 2016-02-09 16:34:41 +01:00
4be0b2502e Change stop to pause in download queue view 2016-02-09 16:25:36 +01:00
6c069ad87b multiple chapter download from manga view 2016-02-09 16:01:11 +01:00
e69011ac5b Use a shorter description for seamless mode 2016-02-08 22:33:23 +01:00
ea130a0899 Merge pull request #112 from icewind1991/seamless-chapters
Seamless chapter transition
2016-02-08 22:25:56 +01:00
2566862e0f seamless chapter transitions 2016-02-08 22:24:47 +01:00
16081817c2 Upgrade dependencies 2016-02-08 22:14:48 +01:00
945625d3ad Cancel notification when no new chapters are found. Closes #121 2016-02-07 19:15:45 +01:00
050b9c9fce Remember last used source. Closes #30 2016-02-06 19:03:15 +01:00
c35184abdc Upgrade gradle. Other minor changes 2016-02-06 00:37:11 +01:00
34c5f0b7ba Try to mark readded chapters as read. #119 2016-02-05 22:08:54 +01:00
6435eeb251 Use network cache 2016-02-05 20:18:39 +01:00
eec2dcd981 Fix a crash 2016-02-05 17:30:58 +01:00
57ba368ae0 Add library search. Closes #64 2016-02-05 16:24:34 +01:00
ed06469885 Trying to fix a backpressure isue 2016-02-05 15:42:53 +01:00
79cd8c691e Minor changes 2016-02-05 14:53:07 +01:00
391550f49a Implement zoom start position. Closes #92. Rapid decoder properly throws an error when it fails to decode. 2016-02-04 17:16:47 +01:00
6aa07dd17e Download the first image of the next chapter 2016-02-03 22:21:15 +01:00
aada373a0c Replace onProcessRestart with the new startables. 2016-02-03 21:09:40 +01:00
3deac86bbe Merge pull request #98 from NoodleMage/download_updates
Download updates
2016-02-03 17:17:15 +01:00
d7aef2e97a Application can now check if update available 2016-02-03 17:12:26 +01:00
7953ba6e78 Display date in local format. Fix #108 2016-02-03 13:28:55 +01:00
8aa3c2a260 Update readme 2016-02-03 12:58:07 +01:00
854 changed files with 29587 additions and 21032 deletions

View File

@ -1,19 +1,20 @@
# Bugs # Bugs
* Include version (Setting > About > Version) * Include version (Setting > About > Version)
* If not latest, try updating, it may have already been solved * If not latest, try updating, it may have already been solved
* Debug version is equal to the number of commits as seen in the main page * 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 steps to reproduce (if not obvious from description)
* Include screenshot (if needed) * Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed) * 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).** * **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).**
* For large logs use http://pastebin.com/ (or similar) * For large logs use http://pastebin.com/ (or similar)
* For multipart issues use list like this: * For multipart issues **use list** like this:
* [x] Done * [x] Done
* [ ] Not done * [ ] Not done
``` ```
* [x] Done * [x] Done
* [ ] Not done * [ ] Not done
``` ```
* Don't put together too many unrelated requests into one issue
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71 DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71

7
.github/ISSUE_TEMPLATE.md vendored Normal file
View File

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

View File

@ -5,15 +5,27 @@ android:
- tools - tools
# The BuildTools version used by your project # The BuildTools version used by your project
- build-tools-23.0.1 - build-tools-24.0.2
- android-23 - android-24
- extra-android-m2repository - extra-android-m2repository
- extra-google-m2repository - extra-google-m2repository
- extra-android-support - extra-android-support
- extra-google-google_play_services - extra-google-google_play_services
jdk:
- oraclejdk8
before_script: before_script:
- chmod +x gradlew - chmod +x gradlew
#Build, and run tests #Build, and run tests
script: "./gradlew build testDebug" script: "./gradlew clean buildDebug"
sudo: false sudo: false
before_cache:
- rm -f $HOME/.gradle/caches/modules-2/modules-2.lock
cache:
directories:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
env:
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"

View File

@ -1,14 +1,16 @@
[![stable release](https://img.shields.io/badge/release-v0.1.2-blue.svg)](https://github.com/inorichi/tachiyomi/releases) | Build | Download | Auto Update |
[![fdroid release](https://img.shields.io/badge/release-F--Droid-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi) |-------|----------|-------------|
[![latest debug build](https://img.shields.io/badge/debug-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk) | [![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) |
## [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.**
Tachiyomi is a free and open source manga reader for Android. 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. Keep in mind it's still a beta, so expect it to crash sometimes.
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. # Features
## Features
* Online and offline reading * Online and offline reading
* Configurable reader with multiple viewers and settings * Configurable reader with multiple viewers and settings

2
app/.gitignore vendored
View File

@ -1,4 +1,4 @@
/build /build
*iml *iml
*.iml *.iml
.idea custom.gradle

View File

@ -1,19 +1,18 @@
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
apply plugin: 'android-sdk-manager'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' apply plugin: 'kotlin-android'
apply plugin: 'me.tatarka.retrolambda' apply plugin: 'kotlin-android-extensions'
retrolambda { if (file("custom.gradle").exists()) {
jvmArgs '-noverify' apply from: "custom.gradle"
} }
ext { ext {
// Git is needed in your system PATH for these commands to work. // Git is needed in your system PATH for these commands to work.
// If it's not installed, you can return a random value as a workaround // If it's not installed, you can return a random value as a workaround
getCommitCount = { getCommitCount = {
return 'git rev-list --count origin/master'.execute().text.trim() return 'git rev-list --count HEAD'.execute().text.trim()
// return "1" // return "1"
} }
@ -29,36 +28,45 @@ ext {
} }
} }
def includeUpdater() {
return hasProperty("include_updater")
}
android { android {
compileSdkVersion 23 compileSdkVersion 24
buildToolsVersion "23.0.2" buildToolsVersion "24.0.2"
publishNonDefault true publishNonDefault true
defaultConfig { defaultConfig {
applicationId "eu.kanade.tachiyomi" applicationId "eu.kanade.tachiyomi"
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 23 targetSdkVersion 24
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 4 versionCode 13
versionName "0.1.3" versionName "0.3.2"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\"" buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
} buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
compileOptions { vectorDrawables.useSupportLibrary = true
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8 ndk {
abiFilters "armeabi", "armeabi-v7a", "x86"
}
} }
buildTypes { buildTypes {
debug { debug {
versionNameSuffix "-${getCommitCount()}"
applicationIdSuffix ".debug" applicationIdSuffix ".debug"
multiDexEnabled true
} }
release { release {
minifyEnabled true minifyEnabled true
shrinkResources true shrinkResources true
multiDexEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
} }
} }
@ -76,76 +84,124 @@ android {
checkReleaseBuilds false checkReleaseBuilds false
} }
sourceSets {
main.java.srcDirs += 'src/main/kotlin'
}
} }
dependencies { dependencies {
final SUPPORT_LIBRARY_VERSION = '23.1.1'
final DAGGER_VERSION = '2.0.2'
final OKHTTP_VERSION = '3.0.1'
final MOCKITO_VERSION = '1.10.19'
final STORIO_VERSION = '1.8.0'
final ICEPICK_VERSION = '3.1.0'
compile fileTree(dir: 'libs', include: ['*.jar']) // Modified dependencies
compile project(":SubsamplingScaleImageView") compile 'com.github.inorichi:subsampling-scale-image-view:96d2c7f'
compile 'com.github.inorichi:ReactiveNetwork:69092ed'
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION" // Android support library
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION" final support_library_version = '24.2.1'
compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:support-v4:$support_library_version"
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:appcompat-v7:$support_library_version"
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:cardview-v7:$support_library_version"
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:design:$support_library_version"
compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:recyclerview-v7:$support_library_version"
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION" compile "com.android.support:support-annotations:$support_library_version"
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION" compile "com.android.support:customtabs:$support_library_version"
compile 'com.squareup.okio:okio:1.6.0'
compile 'com.google.code.gson:gson:2.5' compile 'com.android.support:multidex:1.0.1'
compile 'com.google.android.gms:play-services-gcm:9.8.0'
// ReactiveX
compile 'io.reactivex:rxandroid:1.2.1'
compile 'io.reactivex:rxjava:1.2.1'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.2'
// Network client
compile "com.squareup.okhttp3:okhttp:3.4.1"
// 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"
// IO
compile 'com.squareup.okio:okio:1.10.0'
// JSON
compile 'com.google.code.gson:gson:2.7'
compile 'com.github.salomonbrys.kotson:kotson:2.4.0'
// YAML
compile 'com.github.bmoliveira:snake-yaml:v1.18-android'
// JavaScript engine
compile 'com.squareup.duktape:duktape-android:1.0.0'
// Disk cache
compile 'com.jakewharton:disklrucache:2.0.2' compile 'com.jakewharton:disklrucache:2.0.2'
compile 'org.jsoup:jsoup:1.8.3'
compile 'io.reactivex:rxandroid:1.1.0' // Parse HTML
compile 'io.reactivex:rxjava:1.1.0' compile 'org.jsoup:jsoup:1.9.2'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION" // Changelog
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION" compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
compile 'info.android15.nucleus:nucleus:2.0.4'
compile 'de.greenrobot:eventbus:2.4.0' // Database
compile 'com.github.bumptech.glide:glide:3.6.1' compile "com.pushtorefresh.storio:sqlite:1.11.0"
compile 'com.jakewharton:butterknife:7.0.1'
compile 'com.jakewharton.timber:timber:4.1.0' // Model View Presenter
compile 'uk.co.ribot:easyadapter:1.5.0@aar' final nucleus_version = '3.0.0'
compile 'ch.acra:acra:4.7.0' compile "info.android15.nucleus:nucleus:$nucleus_version"
compile "frankiesardo:icepick:$ICEPICK_VERSION" compile "info.android15.nucleus:nucleus-support-v4:$nucleus_version"
provided "frankiesardo:icepick-processor:$ICEPICK_VERSION" compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
// Dependency injection
compile "uy.kohesive.injekt:injekt-core:1.16.1"
// Image library
compile 'com.github.bumptech.glide:glide:3.7.0'
compile 'com.github.bumptech.glide:okhttp3-integration:1.4.0@aar'
// Transformations
compile 'jp.wasabeef:glide-transformations:2.0.1'
// Logging
compile 'com.jakewharton.timber:timber:4.3.1'
// Crash reports
compile 'ch.acra:acra:4.9.1'
// UI
compile 'com.dmitrymalkovich.android:material-design-dimens:1.4'
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
compile 'eu.davidea:flexible-adapter:4.2.0' compile 'eu.davidea:flexible-adapter:4.2.0'
compile 'com.nononsenseapps:filepicker:2.5.1' compile 'com.nononsenseapps:filepicker:2.5.2'
compile 'com.github.amulyakhare:TextDrawable:558677e' compile 'com.github.amulyakhare:TextDrawable:558677e'
compile 'com.github.pwittchen:reactivenetwork:0.1.5' compile 'com.afollestad.material-dialogs:core:0.9.0.2'
compile 'net.xpece.android:support-preference:1.0.3'
compile "com.google.dagger:dagger:$DAGGER_VERSION" compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
apt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" compile 'de.hdodenhof:circleimageview:2.1.0'
apt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
provided 'org.glassfish:javax.annotation:10.0-b28'
compile('com.mikepenz:materialdrawer:4.6.4@aar') {
transitive = true
}
//Google material icons SVG.
compile 'com.mikepenz:google-material-typeface:2.1.0.1.original@aar'
compile('com.github.afollestad.material-dialogs:core:0.8.5.3@aar') {
transitive = true
}
// Tests
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:2.3.0' testCompile 'org.assertj:assertj-core:1.7.1'
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION" testCompile 'org.mockito:mockito-core:1.10.19'
testCompile('org.robolectric:robolectric:3.0') { testCompile 'org.robolectric:robolectric:3.1.2'
exclude group: 'commons-logging', module: 'commons-logging' testCompile 'org.robolectric:shadows-multidex:3.1.2'
exclude group: 'org.apache.httpcomponents', module: 'httpclient' testCompile 'org.robolectric:shadows-play-services:3.1.2'
}
androidTestApt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
buildscript {
ext.kotlin_version = '1.0.4'
repositories {
mavenCentral()
}
dependencies {
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
repositories {
mavenCentral()
} }

View File

@ -1,9 +1,9 @@
-dontobfuscate -dontobfuscate
-keep class eu.kanade.tachiyomi.injection.** { *; } -keep class eu.kanade.tachiyomi.**
# Retrolambda -keep class com.hippo.image.** { *; }
-dontwarn java.lang.invoke.* -keep interface com.hippo.image.** { *; }
# OkHttp # OkHttp
-keepattributes Signature -keepattributes Signature
@ -19,35 +19,6 @@
-dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement -dontwarn org.codehaus.mojo.animal_sniffer.IgnoreJRERequirement
-dontwarn okio.** -dontwarn okio.**
# ButterKnife 7
-keep class butterknife.** { *; }
-dontwarn butterknife.internal.**
-keep class **$$ViewBinder { *; }
-keepclasseswithmembernames class * {
@butterknife.* <fields>;
}
-keepclasseswithmembernames class * {
@butterknife.* <methods>;
}
#Easy-Adapter v1.5.0
-keepattributes *Annotation*
-keepclassmembers class * extends uk.co.ribot.easyadapter.ItemViewHolder {
public <init>(...);
}
## GreenRobot EventBus specific rules ##
# https://github.com/greenrobot/EventBus/blob/master/HOWTO.md#proguard-configuration
-keepclassmembers class ** {
public void onEvent*(***);
}
# Don't warn for missing support classes
-dontwarn de.greenrobot.event.util.*$Support
-dontwarn de.greenrobot.event.util.*$SupportManagerFragment
# Glide specific rules # # Glide specific rules #
# https://github.com/bumptech/glide # https://github.com/bumptech/glide
-keep public class * implements com.bumptech.glide.module.GlideModule -keep public class * implements com.bumptech.glide.module.GlideModule
@ -57,7 +28,6 @@
} }
# RxJava 1.1.0 # RxJava 1.1.0
-dontwarn sun.misc.** -dontwarn sun.misc.**
-keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* { -keepclassmembers class rx.internal.util.unsafe.*ArrayQueue*Field* {
@ -73,6 +43,18 @@
rx.internal.util.atomic.LinkedQueueNode consumerNode; rx.internal.util.atomic.LinkedQueueNode consumerNode;
} }
# Retrofit 2.X
## https://square.github.io/retrofit/ ##
-dontwarn retrofit2.**
-keep class retrofit2.** { *; }
-keepattributes Signature
-keepattributes Exceptions
-keepclasseswithmembers class * {
@retrofit2.http.* <methods>;
}
# AppCombat # AppCombat
-keep public class android.support.v7.widget.** { *; } -keep public class android.support.v7.widget.** { *; }
-keep public class android.support.v7.internal.widget.** { *; } -keep public class android.support.v7.internal.widget.** { *; }
@ -82,13 +64,6 @@
public <init>(android.content.Context); public <init>(android.content.Context);
} }
# Icepick
-dontwarn icepick.**
-keep class **$$Icepick { *; }
-keepclasseswithmembernames class * {
@icepick.* <fields>;
}
## GSON 2.2.4 specific rules ## ## GSON 2.2.4 specific rules ##
# Gson uses generic type information stored in a class file when working with fields. Proguard # Gson uses generic type information stored in a class file when working with fields. Proguard
@ -146,4 +121,12 @@
# Keep the support library # Keep the support library
-keep class org.acra.** { *; } -keep class org.acra.** { *; }
-keep interface org.acra.** { *; } -keep interface org.acra.** { *; }
# SnakeYaml
-keep class org.yaml.snakeyaml.** { public protected private *; }
-keep class org.yaml.snakeyaml.** { public protected private *; }
-dontwarn org.yaml.snakeyaml.**
# Duktape
-keep class com.squareup.duktape.** { *; }

View File

@ -1,20 +1,26 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android" <manifest
package="eu.kanade.tachiyomi" > xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
package="eu.kanade.tachiyomi">
<uses-permission android:name="android.permission.INTERNET" /> <uses-permission android:name="android.permission.INTERNET" />
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" /> <uses-permission android:name="android.permission.ACCESS_NETWORK_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.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" /> <uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/> <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="com.android.launcher.permission.INSTALL_SHORTCUT" />
<application <application
android:name=".App" android:name=".App"
android:allowBackup="true" android:allowBackup="true"
android:hardwareAccelerated="true"
android:icon="@mipmap/ic_launcher" android:icon="@mipmap/ic_launcher"
android:label="@string/app_name" android:label="@string/app_name"
android:hardwareAccelerated="true" android:largeHeap="true"
android:theme="@style/AppTheme" > android:theme="@style/Theme.Tachiyomi" >
<activity <activity
android:name=".ui.main.MainActivity"> android:name=".ui.main.MainActivity">
<intent-filter> <intent-filter>
@ -25,14 +31,12 @@
</activity> </activity>
<activity <activity
android:name=".ui.manga.MangaActivity" android:name=".ui.manga.MangaActivity"
android:parentActivityName=".ui.main.MainActivity" > android:parentActivityName=".ui.main.MainActivity"
android:exported="true">
</activity> </activity>
<activity <activity
android:name=".ui.reader.ReaderActivity" android:name=".ui.reader.ReaderActivity"
android:parentActivityName=".ui.manga.MangaActivity"> android:theme="@style/Theme.Reader">
<meta-data
android:name="android.support.PARENT_ACTIVITY"
android:value=".ui.manga.MangaActivity" />
</activity> </activity>
<activity <activity
android:name=".ui.setting.SettingsActivity" android:name=".ui.setting.SettingsActivity"
@ -40,44 +44,54 @@
android:parentActivityName=".ui.main.MainActivity" > android:parentActivityName=".ui.main.MainActivity" >
</activity> </activity>
<activity <activity
android:name=".ui.library.category.CategoryActivity" android:name=".ui.category.CategoryActivity"
android:label="@string/label_categories" android:label="@string/label_categories"
android:parentActivityName=".ui.main.MainActivity"> android:parentActivityName=".ui.main.MainActivity">
</activity> </activity>
<activity <activity
android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity" android:name=".ui.setting.SettingsDownloadsFragment$CustomLayoutPickerActivity"
android:label="@string/app_name" android:label="@string/app_name"
android:theme="@style/FilePickerTheme"> android:theme="@style/FilePickerTheme">
</activity> </activity>
<service android:name=".data.sync.LibraryUpdateService" <service android:name=".data.library.LibraryUpdateService"
android:exported="false"/> android:exported="false"/>
<service android:name=".data.download.DownloadService" <service android:name=".data.download.DownloadService"
android:exported="false"/> android:exported="false"/>
<service android:name=".data.sync.UpdateMangaSyncService" <service android:name=".data.mangasync.UpdateMangaSyncService"
android:exported="false"/> android:exported="false"/>
<receiver <service
android:name=".data.sync.LibraryUpdateService$SyncOnConnectionAvailable" android:name=".data.library.LibraryUpdateTrigger"
android:enabled="false"> android:exported="true"
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
<intent-filter> <intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" /> <action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
</intent-filter> </intent-filter>
</receiver> </service>
<service
android:name=".data.updater.UpdateCheckerService"
android:exported="true"
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
<intent-filter>
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
</intent-filter>
</service>
<service android:name=".data.updater.UpdateDownloaderService"
android:exported="false"/>
<receiver android:name=".data.updater.UpdateNotificationReceiver"/>
<receiver <receiver
android:name=".data.sync.LibraryUpdateAlarm"> android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="eu.kanade.UPDATE_LIBRARY" />
</intent-filter>
</receiver> </receiver>
<meta-data <meta-data
android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule" android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
android:value="GlideModule" /> android:value="GlideModule" />
</application> </application>

View File

@ -1,57 +0,0 @@
package eu.kanade.tachiyomi;
import android.app.Application;
import android.content.Context;
import org.acra.ACRA;
import org.acra.annotation.ReportsCrashes;
import eu.kanade.tachiyomi.injection.ComponentReflectionInjector;
import eu.kanade.tachiyomi.injection.component.AppComponent;
import eu.kanade.tachiyomi.injection.component.DaggerAppComponent;
import eu.kanade.tachiyomi.injection.module.AppModule;
import timber.log.Timber;
@ReportsCrashes(
formUri = "http://tachiyomi.kanade.eu/crash_report",
reportType = org.acra.sender.HttpSender.Type.JSON,
httpMethod = org.acra.sender.HttpSender.Method.PUT,
excludeMatchingSharedPreferencesKeys={".*username.*",".*password.*"}
)
public class App extends Application {
AppComponent applicationComponent;
ComponentReflectionInjector<AppComponent> componentInjector;
@Override
public void onCreate() {
super.onCreate();
if (BuildConfig.DEBUG) Timber.plant(new Timber.DebugTree());
applicationComponent = DaggerAppComponent.builder()
.appModule(new AppModule(this))
.build();
componentInjector =
new ComponentReflectionInjector<>(AppComponent.class, applicationComponent);
ACRA.init(this);
}
public static App get(Context context) {
return (App) context.getApplicationContext();
}
public AppComponent getComponent() {
return applicationComponent;
}
public ComponentReflectionInjector<AppComponent> getComponentReflection() {
return componentInjector;
}
// Needed to replace the component with a test specific one
public void setComponent(AppComponent applicationComponent) {
this.applicationComponent = applicationComponent;
}
}

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi
import android.app.Application
import android.content.Context
import android.support.multidex.MultiDex
import org.acra.ACRA
import org.acra.annotation.ReportsCrashes
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.InjektScope
import uy.kohesive.injekt.registry.default.DefaultRegistrar
@ReportsCrashes(
formUri = "http://tachiyomi.kanade.eu/crash_report",
reportType = org.acra.sender.HttpSender.Type.JSON,
httpMethod = org.acra.sender.HttpSender.Method.PUT,
buildConfigClass = BuildConfig::class,
excludeMatchingSharedPreferencesKeys = arrayOf(".*username.*", ".*password.*")
)
open class App : Application() {
override fun onCreate() {
super.onCreate()
Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this))
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
setupAcra()
}
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
if (BuildConfig.DEBUG) {
MultiDex.install(this)
}
}
protected open fun setupAcra() {
ACRA.init(this)
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi
import android.app.Application
import com.google.gson.Gson
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
class AppModule(val app: Application) : InjektModule {
override fun InjektRegistrar.registerInjectables() {
addSingletonFactory { PreferencesHelper(app) }
addSingletonFactory { DatabaseHelper(app) }
addSingletonFactory { ChapterCache(app) }
addSingletonFactory { CoverCache(app) }
addSingletonFactory { NetworkHelper(app) }
addSingletonFactory { SourceManager(app) }
addSingletonFactory { DownloadManager(app) }
addSingletonFactory { MangaSyncManager(app) }
addSingletonFactory { Gson() }
}
}

View File

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

View File

@ -0,0 +1,364 @@
package eu.kanade.tachiyomi.data.backup
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.*
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.data.backup.serializer.IdExclusion
import eu.kanade.tachiyomi.data.backup.serializer.IntegerSerializer
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.*
import java.io.*
import java.util.*
/**
* 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(Integer::class.java, IntegerSerializer())
.setExclusionStrategies(IdExclusion())
.create()
/**
* 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.
*/
@Throws(IOException::class)
fun backupToFile(file: File) {
val root = backupToJson()
FileWriter(file).use {
gson.toJson(root, it)
}
}
/**
* Creates a JSON object containing the backup of the app's data.
*
* @return the backup as a JSON object.
*/
fun backupToJson(): JsonObject {
val root = JsonObject()
// Backup library mangas and its dependencies
val mangaEntries = JsonArray()
root.add(MANGAS, mangaEntries)
for (manga in db.getFavoriteMangas().executeAsBlocking()) {
mangaEntries.add(backupManga(manga))
}
// Backup categories
val categoryEntries = JsonArray()
root.add(CATEGORIES, categoryEntries)
for (category in db.getCategories().executeAsBlocking()) {
categoryEntries.add(backupCategory(category))
}
return root
}
/**
* Backups a manga and its related data (chapters, categories this manga is in, sync...).
*
* @param manga the manga to backup.
* @return a JSON object containing all the data of the manga.
*/
private fun backupManga(manga: Manga): JsonObject {
// Entry for this manga
val entry = JsonObject()
// Backup manga fields
entry.add(MANGA, gson.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)
}
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)
}
/**
* Restores a backup from a file.
*
* @param file the file containing the backup.
* @throws IOException if there's any IO error.
*/
@Throws(IOException::class)
fun restoreFromFile(file: File) {
JsonReader(FileReader(file)).use {
val root = JsonParser().parse(it).asJsonObject
restoreFromJson(root)
}
}
/**
* Restores a backup from an input stream.
*
* @param stream the stream containing the backup.
* @throws IOException if there's any IO error.
*/
@Throws(IOException::class)
fun restoreFromStream(stream: InputStream) {
JsonReader(InputStreamReader(stream)).use {
val root = JsonParser().parse(it).asJsonObject
restoreFromJson(root)
}
}
/**
* Restores a backup from a JSON object. Everything executes in a single transaction so that
* nothing is modified if there's an error.
*
* @param root the root of the JSON.
*/
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) {
// Get categories from file and from db
val dbCategories = db.getCategories().executeAsBlocking()
val backupCategories = gson.fromJson<List<CategoryImpl>>(jsonCategories)
// Iterate over them
for (category in backupCategories) {
// Used to know if the category is already in the db
var found = false
for (dbCategory in dbCategories) {
// If the category is already in the db, assign the id to the file's category
// and do nothing
if (category.nameLower == dbCategory.nameLower) {
category.id = dbCategory.id
found = true
break
}
}
// If the category isn't in the db, remove the id and insert a new category
// Store the inserted id in the category
if (!found) {
// Let the db assign the id
category.id = null
val result = db.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()
val mangaCategoriesToUpdate = ArrayList<MangaCategory>()
for (backupCategoryStr in categories) {
for (dbCategory in dbCategories) {
if (backupCategoryStr.toLowerCase() == dbCategory.nameLower) {
mangaCategoriesToUpdate.add(MangaCategory.create(manga, dbCategory))
break
}
}
}
// Update database
if (!mangaCategoriesToUpdate.isEmpty()) {
val mangaAsList = ArrayList<Manga>()
mangaAsList.add(manga)
db.deleteOldMangasCategories(mangaAsList).executeAsBlocking()
db.insertMangasCategories(mangaCategoriesToUpdate).executeAsBlocking()
}
}
/**
* Restores the sync of a manga.
*
* @param manga the manga whose sync have to be restored.
* @param sync the sync to restore.
*/
private fun restoreSyncForManga(manga: Manga, sync: List<MangaSync>) {
// Fix foreign keys with the current manga id
for (mangaSync in sync) {
mangaSync.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)
}
}
// Update database
if (!syncToUpdate.isEmpty()) {
db.insertMangasSync(syncToUpdate).executeAsBlocking()
}
}
}

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.data.backup.serializer
import com.google.gson.ExclusionStrategy
import com.google.gson.FieldAttributes
import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaSync
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) {
Manga::class.java -> mangaExclusions.contains(f.name)
Chapter::class.java -> chapterExclusions.contains(f.name)
MangaSync::class.java -> syncExclusions.contains(f.name)
Category::class.java -> categoryExclusions.contains(f.name)
else -> false
}
override fun shouldSkipClass(clazz: Class<*>) = false
}

View File

@ -0,0 +1,17 @@
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,268 +0,0 @@
package eu.kanade.tachiyomi.data.cache;
import android.content.Context;
import android.text.format.Formatter;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.jakewharton.disklrucache.DiskLruCache;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.OutputStream;
import java.lang.reflect.Type;
import java.util.List;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.DiskUtils;
import okhttp3.Response;
import okio.BufferedSink;
import okio.Okio;
import rx.Observable;
/**
* Class used to create chapter cache
* For each image in a chapter a file is created
* For each chapter a Json list is created and converted to a file.
* The files are in format *md5key*.0
*/
public class ChapterCache {
/** Name of cache directory. */
private static final String PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache";
/** Application cache version. */
private static final int PARAMETER_APP_VERSION = 1;
/** The number of values per cache entry. Must be positive. */
private static final int PARAMETER_VALUE_COUNT = 1;
/** The maximum number of bytes this cache should use to store. */
private static final int PARAMETER_CACHE_SIZE = 75 * 1024 * 1024;
/** Interface to global information about an application environment. */
private final Context context;
/** Google Json class used for parsing JSON files. */
private final Gson gson;
/** Cache class used for cache management. */
private DiskLruCache diskCache;
/** Page list collection used for deserializing from JSON. */
private final Type pageListCollection;
/**
* Constructor of ChapterCache.
* @param context application environment interface.
*/
public ChapterCache(Context context) {
this.context = context;
// Initialize Json handler.
gson = new Gson();
// Try to open cache in default cache directory.
try {
diskCache = DiskLruCache.open(
new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE
);
} catch (IOException e) {
// Do Nothing.
}
pageListCollection = new TypeToken<List<Page>>() {}.getType();
}
/**
* Returns directory of cache.
* @return directory of cache.
*/
public File getCacheDir() {
return diskCache.getDirectory();
}
/**
* Returns real size of directory.
* @return real size of directory.
*/
private long getRealSize() {
return DiskUtils.getDirectorySize(getCacheDir());
}
/**
* Returns real size of directory in human readable format.
* @return real size of directory.
*/
public String getReadableSize() {
return Formatter.formatFileSize(context, getRealSize());
}
/**
* Remove file from cache.
* @param file name of file "md5.0".
* @return status of deletion for the file.
*/
public boolean removeFileFromCache(String file) {
// Make sure we don't delete the journal file (keeps track of cache).
if (file.equals("journal") || file.startsWith("journal."))
return false;
try {
// Remove the extension from the file to get the key of the cache
String key = file.substring(0, file.lastIndexOf("."));
// Remove file from cache.
return diskCache.remove(key);
} catch (IOException e) {
return false;
}
}
/**
* Get page list from cache.
* @param chapterUrl the url of the chapter.
* @return an observable of the list of pages.
*/
public Observable<List<Page>> getPageListFromCache(final String chapterUrl) {
return Observable.fromCallable(() -> {
// Initialize snapshot (a snapshot of the values for an entry).
DiskLruCache.Snapshot snapshot = null;
try {
// Create md5 key and retrieve snapshot.
String key = DiskUtils.hashKeyForDisk(chapterUrl);
snapshot = diskCache.get(key);
// Convert JSON string to list of objects.
return gson.fromJson(snapshot.getString(0), pageListCollection);
} finally {
if (snapshot != null) {
snapshot.close();
}
}
});
}
/**
* Add page list to disk cache.
* @param chapterUrl the url of the chapter.
* @param pages list of pages.
*/
public void putPageListToCache(final String chapterUrl, final List<Page> pages) {
// Convert list of pages to json string.
String cachedValue = gson.toJson(pages);
// Initialize the editor (edits the values for an entry).
DiskLruCache.Editor editor = null;
// Initialize OutputStream.
OutputStream outputStream = null;
try {
// Get editor from md5 key.
String key = DiskUtils.hashKeyForDisk(chapterUrl);
editor = diskCache.edit(key);
if (editor == null) {
return;
}
// Write chapter urls to cache.
outputStream = new BufferedOutputStream(editor.newOutputStream(0));
outputStream.write(cachedValue.getBytes());
outputStream.flush();
diskCache.flush();
editor.commit();
} catch (Exception e) {
// Do Nothing.
} finally {
if (editor != null) {
editor.abortUnlessCommitted();
}
if (outputStream != null) {
try {
outputStream.close();
} catch (IOException ignore) {
// Do Nothing.
}
}
}
}
/**
* Check if image is in cache.
* @param imageUrl url of image.
* @return true if in cache otherwise false.
*/
public boolean isImageInCache(final String imageUrl) {
try {
return diskCache.get(DiskUtils.hashKeyForDisk(imageUrl)) != null;
} catch (IOException e) {
return false;
}
}
/**
* Get image path from url.
* @param imageUrl url of image.
* @return path of image.
*/
public String getImagePath(final String imageUrl) {
try {
// Get file from md5 key.
String imageName = DiskUtils.hashKeyForDisk(imageUrl) + ".0";
File file = new File(diskCache.getDirectory(), imageName);
return file.getCanonicalPath();
} catch (IOException e) {
return null;
}
}
/**
* Add image to cache.
* @param imageUrl url of image.
* @param response http response from page.
* @throws IOException image error.
*/
public void putImageToCache(final String imageUrl, final Response response) throws IOException {
// Initialize editor (edits the values for an entry).
DiskLruCache.Editor editor = null;
// Initialize BufferedSink (used for small writes).
BufferedSink sink = null;
try {
// Get editor from md5 key.
String key = DiskUtils.hashKeyForDisk(imageUrl);
editor = diskCache.edit(key);
if (editor == null) {
throw new IOException("Unable to edit key");
}
// Initialize OutputStream and write image.
OutputStream outputStream = new BufferedOutputStream(editor.newOutputStream(0));
sink = Okio.buffer(Okio.sink(outputStream));
sink.writeAll(response.body().source());
diskCache.flush();
editor.commit();
} catch (Exception e) {
response.body().close();
throw new IOException("Unable to save image");
} finally {
if (editor != null) {
editor.abortUnlessCommitted();
}
if (sink != null) {
sink.close();
}
}
}
}

View File

@ -0,0 +1,209 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import android.text.format.Formatter
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.jakewharton.disklrucache.DiskLruCache
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.util.DiskUtils
import eu.kanade.tachiyomi.util.saveTo
import okhttp3.Response
import okio.Okio
import rx.Observable
import java.io.File
import java.io.IOException
import java.lang.reflect.Type
/**
* Class used to create chapter cache
* For each image in a chapter a file is created
* For each chapter a Json list is created and converted to a file.
* The files are in format *md5key*.0
*
* @param context the application context.
* @constructor creates an instance of the chapter cache.
*/
class ChapterCache(private val context: Context) {
/** Google Json class used for parsing JSON files. */
private val gson: Gson = Gson()
/** Cache class used for cache management. */
private val diskCache: DiskLruCache
/** Page list collection used for deserializing from JSON. */
private val pageListCollection: Type = object : TypeToken<List<Page>>() {}.type
companion object {
/** Name of cache directory. */
const val PARAMETER_CACHE_DIRECTORY = "chapter_disk_cache"
/** Application cache version. */
const val PARAMETER_APP_VERSION = 1
/** The number of values per cache entry. Must be positive. */
const val PARAMETER_VALUE_COUNT = 1
/** The maximum number of bytes this cache should use to store. */
const val PARAMETER_CACHE_SIZE = 75L * 1024 * 1024
}
init {
// Open cache in default cache directory.
diskCache = DiskLruCache.open(
File(context.cacheDir, PARAMETER_CACHE_DIRECTORY),
PARAMETER_APP_VERSION,
PARAMETER_VALUE_COUNT,
PARAMETER_CACHE_SIZE)
}
/**
* Returns directory of cache.
* @return directory of cache.
*/
val cacheDir: File
get() = diskCache.directory
/**
* Returns real size of directory.
* @return real size of directory.
*/
private val realSize: Long
get() = DiskUtils.getDirectorySize(cacheDir)
/**
* Returns real size of directory in human readable format.
* @return real size of directory.
*/
val readableSize: String
get() = Formatter.formatFileSize(context, realSize)
/**
* Remove file from cache.
* @param file name of file "md5.0".
* @return status of deletion for the file.
*/
fun removeFileFromCache(file: String): Boolean {
// Make sure we don't delete the journal file (keeps track of cache).
if (file == "journal" || file.startsWith("journal."))
return false
try {
// Remove the extension from the file to get the key of the cache
val key = file.substring(0, file.lastIndexOf("."))
// Remove file from cache.
return diskCache.remove(key)
} catch (e: IOException) {
return false
}
}
/**
* Get page list from cache.
* @param chapterUrl the url of the chapter.
* @return an observable of the list of pages.
*/
fun getPageListFromCache(chapterUrl: String): Observable<List<Page>> {
return Observable.fromCallable<List<Page>> {
// Get the key for the chapter.
val key = DiskUtils.hashKeyForDisk(chapterUrl)
// Convert JSON string to list of objects. Throws an exception if snapshot is null
diskCache.get(key).use {
gson.fromJson(it.getString(0), pageListCollection)
}
}
}
/**
* Add page list to disk cache.
* @param chapterUrl the url of the chapter.
* @param pages list of pages.
*/
fun putPageListToCache(chapterUrl: String, pages: List<Page>) {
// Convert list of pages to json string.
val cachedValue = gson.toJson(pages)
// Initialize the editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null
try {
// Get editor from md5 key.
val key = DiskUtils.hashKeyForDisk(chapterUrl)
editor = diskCache.edit(key) ?: return
// Write chapter urls to cache.
Okio.buffer(Okio.sink(editor.newOutputStream(0))).use {
it.write(cachedValue.toByteArray())
it.flush()
}
diskCache.flush()
editor.commit()
editor.abortUnlessCommitted()
} catch (e: Exception) {
// Ignore.
} finally {
editor?.abortUnlessCommitted()
}
}
/**
* Check if image is in cache.
* @param imageUrl url of image.
* @return true if in cache otherwise false.
*/
fun isImageInCache(imageUrl: String): Boolean {
try {
return diskCache.get(DiskUtils.hashKeyForDisk(imageUrl)) != null
} catch (e: IOException) {
return false
}
}
/**
* Get image path from url.
* @param imageUrl url of image.
* @return path of image.
*/
fun getImagePath(imageUrl: String): String? {
try {
// Get file from md5 key.
val imageName = DiskUtils.hashKeyForDisk(imageUrl) + ".0"
return File(diskCache.directory, imageName).canonicalPath
} catch (e: IOException) {
return null
}
}
/**
* Add image to cache.
* @param imageUrl url of image.
* @param response http response from page.
* @throws IOException image error.
*/
@Throws(IOException::class)
fun putImageToCache(imageUrl: String, response: Response) {
// Initialize editor (edits the values for an entry).
var editor: DiskLruCache.Editor? = null
try {
// Get editor from md5 key.
val key = DiskUtils.hashKeyForDisk(imageUrl)
editor = diskCache.edit(key) ?: throw IOException("Unable to edit key")
// Get OutputStream and write image with Okio.
response.body().source().saveTo(editor.newOutputStream(0))
diskCache.flush()
editor.commit()
} finally {
response.body().close()
editor?.abortUnlessCommitted()
}
}
}

View File

@ -1,235 +0,0 @@
package eu.kanade.tachiyomi.data.cache;
import android.content.Context;
import android.support.annotation.Nullable;
import android.text.TextUtils;
import android.widget.ImageView;
import com.bumptech.glide.Glide;
import com.bumptech.glide.load.engine.DiskCacheStrategy;
import com.bumptech.glide.load.model.GlideUrl;
import com.bumptech.glide.load.model.LazyHeaders;
import com.bumptech.glide.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.signature.StringSignature;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import eu.kanade.tachiyomi.util.DiskUtils;
/**
* Class used to create cover cache
* It is used to store the covers of the library.
* Makes use of Glide (which can avoid repeating requests) to download covers.
* Names of files are created with the md5 of the thumbnail URL
*/
public class CoverCache {
/**
* Name of cache directory.
*/
private static final String PARAMETER_CACHE_DIRECTORY = "cover_disk_cache";
/**
* Interface to global information about an application environment.
*/
private final Context context;
/**
* Cache directory used for cache management.
*/
private final File cacheDir;
/**
* Constructor of CoverCache.
*
* @param context application environment interface.
*/
public CoverCache(Context context) {
this.context = context;
// Get cache directory from parameter.
cacheDir = new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY);
// Create cache directory.
createCacheDir();
}
/**
* Create cache directory if it doesn't exist
*
* @return true if cache dir is created otherwise false.
*/
private boolean createCacheDir() {
return !cacheDir.exists() && cacheDir.mkdirs();
}
/**
* Download the cover with Glide and save the file in this cache.
*
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
*/
public void save(String thumbnailUrl, LazyHeaders headers) {
save(thumbnailUrl, headers, null);
}
/**
* Download the cover with Glide and save the file.
*
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
* @param imageView imageView where picture should be displayed.
*/
private void save(String thumbnailUrl, LazyHeaders headers, @Nullable ImageView imageView) {
// Check if url is empty.
if (TextUtils.isEmpty(thumbnailUrl))
return;
// Download the cover with Glide and save the file.
GlideUrl url = new GlideUrl(thumbnailUrl, headers);
Glide.with(context)
.load(url)
.downloadOnly(new SimpleTarget<File>() {
@Override
public void onResourceReady(File resource, GlideAnimation<? super File> anim) {
try {
// Copy the cover from Glide's cache to local cache.
copyToLocalCache(thumbnailUrl, resource);
// Check if imageView isn't null and show picture in imageView.
if (imageView != null) {
loadFromCache(imageView, resource);
}
} catch (IOException e) {
// Do nothing.
}
}
});
}
/**
* Copy the cover from Glide's cache to this cache.
*
* @param thumbnailUrl url of thumbnail.
* @param source the cover image.
* @throws IOException exception returned
*/
public void copyToLocalCache(String thumbnailUrl, File source) throws IOException {
// Create cache directory if needed.
createCacheDir();
// Get destination file.
File dest = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
// Delete the current file if it exists.
if (dest.exists())
dest.delete();
// Write thumbnail image to file.
InputStream in = new FileInputStream(source);
try {
OutputStream out = new FileOutputStream(dest);
try {
// Transfer bytes from in to out.
byte[] buf = new byte[1024];
int len;
while ((len = in.read(buf)) > 0) {
out.write(buf, 0, len);
}
} finally {
out.close();
}
} finally {
in.close();
}
}
/**
* Returns the cover from cache.
*
* @param thumbnailUrl the thumbnail url.
* @return cover image.
*/
private File getCoverFromCache(String thumbnailUrl) {
return new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
}
/**
* Delete the cover file from the cache.
*
* @param thumbnailUrl the thumbnail url.
* @return status of deletion.
*/
public boolean deleteCoverFromCache(String thumbnailUrl) {
// Check if url is empty.
if (TextUtils.isEmpty(thumbnailUrl))
return false;
// Remove file.
File file = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
return file.exists() && file.delete();
}
/**
* Save or load the image from cache
*
* @param imageView imageView where picture should be displayed.
* @param thumbnailUrl the thumbnail url.
* @param headers headers included in Glide request.
*/
public void saveOrLoadFromCache(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
// If file exist load it otherwise save it.
File localCover = getCoverFromCache(thumbnailUrl);
if (localCover.exists()) {
loadFromCache(imageView, localCover);
} else {
save(thumbnailUrl, headers, imageView);
}
}
/**
* Helper method to load the cover from the cache directory into the specified image view.
* Glide stores the resized image in its cache to improve performance.
*
* @param imageView imageView where picture should be displayed.
* @param file file to load. Must exist!.
*/
private void loadFromCache(ImageView imageView, File file) {
Glide.with(context)
.load(file)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.signature(new StringSignature(String.valueOf(file.lastModified())))
.into(imageView);
}
/**
* Helper method to load the cover from network into the specified image view.
* The source image is stored in Glide's cache so that it can be easily copied to this cache
* if the manga is added to the library.
*
* @param imageView imageView where picture should be displayed.
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
*/
public void loadFromNetwork(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
// Check if url is empty.
if (TextUtils.isEmpty(thumbnailUrl))
return;
GlideUrl url = new GlideUrl(thumbnailUrl, headers);
Glide.with(context)
.load(url)
.diskCacheStrategy(DiskCacheStrategy.SOURCE)
.centerCrop()
.into(imageView);
}
}

View File

@ -0,0 +1,66 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import eu.kanade.tachiyomi.util.DiskUtils
import java.io.File
import java.io.IOException
import java.io.InputStream
/**
* Class used to create cover cache.
* It is used to store the covers of the library.
* Makes use of Glide (which can avoid repeating requests) to download covers.
* Names of files are created with the md5 of the thumbnail URL.
*
* @param context the application context.
* @constructor creates an instance of the cover cache.
*/
class CoverCache(private val context: Context) {
/**
* Cache directory used for cache management.
*/
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
/**
* Returns the cover from cache.
*
* @param thumbnailUrl the thumbnail url.
* @return cover image.
*/
fun getCoverFile(thumbnailUrl: String): File {
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
}
/**
* Copy the given stream to this cache.
*
* @param thumbnailUrl url of the thumbnail.
* @param inputStream the stream to copy.
* @throws IOException if there's any error.
*/
@Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
// Get destination file.
val destFile = getCoverFile(thumbnailUrl)
destFile.outputStream().use { inputStream.copyTo(it) }
}
/**
* Delete the cover file from the cache.
*
* @param thumbnailUrl the thumbnail url.
* @return status of deletion.
*/
fun deleteFromCache(thumbnailUrl: String?): Boolean {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
return false
// Remove file.
val file = getCoverFile(thumbnailUrl!!)
return file.exists() && file.delete()
}
}

View File

@ -1,30 +0,0 @@
package eu.kanade.tachiyomi.data.cache;
import android.content.Context;
import com.bumptech.glide.Glide;
import com.bumptech.glide.GlideBuilder;
import com.bumptech.glide.load.DecodeFormat;
import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory;
import com.bumptech.glide.module.GlideModule;
/**
* Class used to update Glide module settings
*/
public class CoverGlideModule implements GlideModule {
@Override
public void applyOptions(Context context, GlideBuilder builder) {
// Bitmaps decoded from most image formats (other than GIFs with hidden configs)
// will be decoded with the ARGB_8888 config.
builder.setDecodeFormat(DecodeFormat.PREFER_ARGB_8888);
// Set the cache size of Glide to 15 MiB
builder.setDiskCache(new InternalCacheDiskCacheFactory(context, 15 * 1024 * 1024));
}
@Override
public void registerComponents(Context context, Glide glide) {
// Nothing to see here!
}
}

View File

@ -1,408 +0,0 @@
package eu.kanade.tachiyomi.data.database;
import android.content.Context;
import android.util.Pair;
import com.pushtorefresh.storio.Queries;
import com.pushtorefresh.storio.sqlite.StorIOSQLite;
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite;
import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteByQuery;
import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteCollectionOfObjects;
import com.pushtorefresh.storio.sqlite.operations.delete.PreparedDeleteObject;
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetListOfObjects;
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject;
import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutCollectionOfObjects;
import com.pushtorefresh.storio.sqlite.operations.put.PreparedPutObject;
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery;
import com.pushtorefresh.storio.sqlite.queries.Query;
import com.pushtorefresh.storio.sqlite.queries.RawQuery;
import java.util.Date;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.data.database.models.CategorySQLiteTypeMapping;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.ChapterSQLiteTypeMapping;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.database.models.MangaCategory;
import eu.kanade.tachiyomi.data.database.models.MangaCategorySQLiteTypeMapping;
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
import eu.kanade.tachiyomi.data.database.models.MangaSQLiteTypeMapping;
import eu.kanade.tachiyomi.data.database.models.MangaSync;
import eu.kanade.tachiyomi.data.database.models.MangaSyncSQLiteTypeMapping;
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver;
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver;
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.MangaSyncTable;
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import eu.kanade.tachiyomi.util.ChapterRecognition;
import rx.Observable;
public class DatabaseHelper {
private StorIOSQLite db;
public DatabaseHelper(Context context) {
db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(new DbOpenHelper(context))
.addTypeMapping(Manga.class, new MangaSQLiteTypeMapping())
.addTypeMapping(Chapter.class, new ChapterSQLiteTypeMapping())
.addTypeMapping(MangaSync.class, new MangaSyncSQLiteTypeMapping())
.addTypeMapping(Category.class, new CategorySQLiteTypeMapping())
.addTypeMapping(MangaCategory.class, new MangaCategorySQLiteTypeMapping())
.build();
}
// Mangas related queries
public PreparedGetListOfObjects<Manga> getMangas() {
return db.get()
.listOfObjects(Manga.class)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.build())
.prepare();
}
public PreparedGetListOfObjects<Manga> getLibraryMangas() {
return db.get()
.listOfObjects(Manga.class)
.withQuery(RawQuery.builder()
.query(LibraryMangaGetResolver.QUERY)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
.build())
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare();
}
public PreparedGetListOfObjects<Manga> getFavoriteMangas() {
return db.get()
.listOfObjects(Manga.class)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where(MangaTable.COLUMN_FAVORITE + "=?")
.whereArgs(1)
.orderBy(MangaTable.COLUMN_TITLE)
.build())
.prepare();
}
public PreparedGetObject<Manga> getManga(String url, int sourceId) {
return db.get()
.object(Manga.class)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where(MangaTable.COLUMN_URL + "=? AND " + MangaTable.COLUMN_SOURCE + "=?")
.whereArgs(url, sourceId)
.build())
.prepare();
}
public PreparedGetObject<Manga> getManga(long id) {
return db.get()
.object(Manga.class)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where(MangaTable.COLUMN_ID + "=?")
.whereArgs(id)
.build())
.prepare();
}
public PreparedPutObject<Manga> insertManga(Manga manga) {
return db.put()
.object(manga)
.prepare();
}
public PreparedPutCollectionOfObjects<Manga> insertMangas(List<Manga> mangas) {
return db.put()
.objects(mangas)
.prepare();
}
public PreparedDeleteObject<Manga> deleteManga(Manga manga) {
return db.delete()
.object(manga)
.prepare();
}
public PreparedDeleteCollectionOfObjects<Manga> deleteMangas(List<Manga> mangas) {
return db.delete()
.objects(mangas)
.prepare();
}
public PreparedDeleteByQuery deleteMangasNotInLibrary() {
return db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaTable.TABLE)
.where(MangaTable.COLUMN_FAVORITE + "=?")
.whereArgs(0)
.build())
.prepare();
}
// Chapters related queries
public PreparedGetListOfObjects<Chapter> getChapters(Manga manga) {
return db.get()
.listOfObjects(Chapter.class)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where(ChapterTable.COLUMN_MANGA_ID + "=?")
.whereArgs(manga.id)
.build())
.prepare();
}
public PreparedGetListOfObjects<MangaChapter> getRecentChapters(Date date) {
return db.get()
.listOfObjects(MangaChapter.class)
.withQuery(RawQuery.builder()
.query(MangaChapterGetResolver.getRecentChaptersQuery(date))
.observesTables(ChapterTable.TABLE)
.build())
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare();
}
public PreparedGetObject<Chapter> getNextChapter(Chapter chapter) {
// Add a delta to the chapter number, because binary decimal representation
// can retrieve the same chapter again
double chapterNumber = chapter.chapter_number + 0.00001;
return db.get()
.object(Chapter.class)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
ChapterTable.COLUMN_CHAPTER_NUMBER + ">? AND " +
ChapterTable.COLUMN_CHAPTER_NUMBER + "<=?")
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber + 1)
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
.limit(1)
.build())
.prepare();
}
public PreparedGetObject<Chapter> getPreviousChapter(Chapter chapter) {
// Add a delta to the chapter number, because binary decimal representation
// can retrieve the same chapter again
double chapterNumber = chapter.chapter_number - 0.00001;
return db.get()
.object(Chapter.class)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
ChapterTable.COLUMN_CHAPTER_NUMBER + "<? AND " +
ChapterTable.COLUMN_CHAPTER_NUMBER + ">=?")
.whereArgs(chapter.manga_id, chapterNumber, chapterNumber - 1)
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + " DESC")
.limit(1)
.build())
.prepare();
}
public PreparedGetObject<Chapter> getNextUnreadChapter(Manga manga) {
return db.get()
.object(Chapter.class)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where(ChapterTable.COLUMN_MANGA_ID + "=? AND " +
ChapterTable.COLUMN_READ + "=? AND " +
ChapterTable.COLUMN_CHAPTER_NUMBER + ">=?")
.whereArgs(manga.id, 0, 0)
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER)
.limit(1)
.build())
.prepare();
}
public PreparedPutObject<Chapter> insertChapter(Chapter chapter) {
return db.put()
.object(chapter)
.prepare();
}
public PreparedPutCollectionOfObjects<Chapter> insertChapters(List<Chapter> chapters) {
return db.put()
.objects(chapters)
.prepare();
}
// Add new chapters or delete if the source deletes them
public Observable<Pair<Integer, Integer>> insertOrRemoveChapters(Manga manga, List<Chapter> sourceChapters) {
List<Chapter> dbChapters = getChapters(manga).executeAsBlocking();
Observable<List<Chapter>> newChapters = Observable.from(sourceChapters)
.filter(c -> !dbChapters.contains(c))
.doOnNext(c -> {
c.manga_id = manga.id;
ChapterRecognition.parseChapterNumber(c, manga);
})
.toList();
Observable<List<Chapter>> deletedChapters = Observable.from(dbChapters)
.filter(c -> !sourceChapters.contains(c))
.toList();
return Observable.zip(newChapters, deletedChapters, (toAdd, toDelete) -> {
int added = 0;
int deleted = 0;
db.internal().beginTransaction();
try {
if (!toAdd.isEmpty()) {
// Set the date fetch for new items in reverse order to allow another sorting method.
// Sources MUST return the chapters from most to less recent, which is common.
long now = new Date().getTime();
for (int i = toAdd.size() - 1; i >= 0; i--) {
toAdd.get(i).date_fetch = now++;
}
added = insertChapters(toAdd).executeAsBlocking().numberOfInserts();
}
if (!toDelete.isEmpty()) {
deleted = deleteChapters(toDelete).executeAsBlocking().results().size();
}
db.internal().setTransactionSuccessful();
} finally {
db.internal().endTransaction();
}
return Pair.create(added, deleted);
});
}
public PreparedDeleteObject<Chapter> deleteChapter(Chapter chapter) {
return db.delete()
.object(chapter)
.prepare();
}
public PreparedDeleteCollectionOfObjects<Chapter> deleteChapters(List<Chapter> chapters) {
return db.delete()
.objects(chapters)
.prepare();
}
// Manga sync related queries
public PreparedGetObject<MangaSync> getMangaSync(Manga manga, MangaSyncService sync) {
return db.get()
.object(MangaSync.class)
.withQuery(Query.builder()
.table(MangaSyncTable.TABLE)
.where(MangaSyncTable.COLUMN_MANGA_ID + "=? AND " +
MangaSyncTable.COLUMN_SYNC_ID + "=?")
.whereArgs(manga.id, sync.getId())
.build())
.prepare();
}
public PreparedGetListOfObjects<MangaSync> getMangasSync(Manga manga) {
return db.get()
.listOfObjects(MangaSync.class)
.withQuery(Query.builder()
.table(MangaSyncTable.TABLE)
.where(MangaSyncTable.COLUMN_MANGA_ID + "=?")
.whereArgs(manga.id)
.build())
.prepare();
}
public PreparedPutObject<MangaSync> insertMangaSync(MangaSync manga) {
return db.put()
.object(manga)
.prepare();
}
public PreparedDeleteObject<MangaSync> deleteMangaSync(MangaSync manga) {
return db.delete()
.object(manga)
.prepare();
}
// Categories related queries
public PreparedGetListOfObjects<Category> getCategories() {
return db.get()
.listOfObjects(Category.class)
.withQuery(Query.builder()
.table(CategoryTable.TABLE)
.orderBy(CategoryTable.COLUMN_ORDER)
.build())
.prepare();
}
public PreparedPutObject<Category> insertCategory(Category category) {
return db.put()
.object(category)
.prepare();
}
public PreparedPutCollectionOfObjects<Category> insertCategories(List<Category> categories) {
return db.put()
.objects(categories)
.prepare();
}
public PreparedDeleteObject<Category> deleteCategory(Category category) {
return db.delete()
.object(category)
.prepare();
}
public PreparedDeleteCollectionOfObjects<Category> deleteCategories(List<Category> categories) {
return db.delete()
.objects(categories)
.prepare();
}
public PreparedPutObject<MangaCategory> insertMangaCategory(MangaCategory mangaCategory) {
return db.put()
.object(mangaCategory)
.prepare();
}
public PreparedPutCollectionOfObjects<MangaCategory> insertMangasCategories(List<MangaCategory> mangasCategories) {
return db.put()
.objects(mangasCategories)
.prepare();
}
public PreparedDeleteByQuery deleteOldMangasCategories(List<Manga> mangas) {
List<Long> mangaIds = Observable.from(mangas)
.map(manga -> manga.id)
.toList().toBlocking().single();
return db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaCategoryTable.TABLE)
.where(MangaCategoryTable.COLUMN_MANGA_ID + " IN ("
+ Queries.placeholders(mangas.size()) + ")")
.whereArgs(mangaIds.toArray())
.build())
.prepare();
}
public void setMangaCategories(List<MangaCategory> mangasCategories, List<Manga> mangas) {
db.internal().beginTransaction();
try {
deleteOldMangasCategories(mangas).executeAsBlocking();
insertMangasCategories(mangasCategories).executeAsBlocking();
db.internal().setTransactionSuccessful();
} finally {
db.internal().endTransaction();
}
}
}

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.data.database
import android.content.Context
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
import eu.kanade.tachiyomi.data.database.mappers.*
import eu.kanade.tachiyomi.data.database.models.*
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 {
override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(DbOpenHelper(context))
.addTypeMapping(Manga::class.java, MangaTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterTypeMapping())
.addTypeMapping(MangaSync::class.java, MangaSyncTypeMapping())
.addTypeMapping(Category::class.java, CategoryTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategoryTypeMapping())
.addTypeMapping(History::class.java, HistoryTypeMapping())
.build()
inline fun inTransaction(block: () -> Unit) = db.inTransaction(block)
}

View File

@ -0,0 +1,25 @@
package eu.kanade.tachiyomi.data.database
import com.pushtorefresh.storio.sqlite.StorIOSQLite
inline fun StorIOSQLite.inTransaction(block: () -> Unit) {
lowLevel().beginTransaction()
try {
block()
lowLevel().setTransactionSuccessful()
} finally {
lowLevel().endTransaction()
}
}
inline fun <T> StorIOSQLite.inTransactionReturn(block: () -> T): T {
lowLevel().beginTransaction()
try {
val result = block()
lowLevel().setTransactionSuccessful()
return result
} finally {
lowLevel().endTransaction()
}
}

View File

@ -1,47 +0,0 @@
package eu.kanade.tachiyomi.data.database;
import android.content.Context;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
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.MangaSyncTable;
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
public class DbOpenHelper extends SQLiteOpenHelper {
public static final String DATABASE_NAME = "tachiyomi.db";
public static final int DATABASE_VERSION = 1;
public DbOpenHelper(@NonNull Context context) {
super(context, DATABASE_NAME, null, DATABASE_VERSION);
}
@Override
public void onCreate(@NonNull SQLiteDatabase db) {
db.execSQL(MangaTable.getCreateTableQuery());
db.execSQL(ChapterTable.getCreateTableQuery());
db.execSQL(MangaSyncTable.getCreateTableQuery());
db.execSQL(CategoryTable.getCreateTableQuery());
db.execSQL(MangaCategoryTable.getCreateTableQuery());
// DB indexes
db.execSQL(MangaTable.getCreateUrlIndexQuery());
db.execSQL(MangaTable.getCreateFavoriteIndexQuery());
db.execSQL(ChapterTable.getCreateMangaIdIndexQuery());
}
@Override
public void onUpgrade(@NonNull SQLiteDatabase db, int oldVersion, int newVersion) {
}
@Override
public void onConfigure(@NonNull SQLiteDatabase db) {
db.setForeignKeyConstraintsEnabled(true);
}
}

View File

@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.data.database
import android.content.Context
import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import eu.kanade.tachiyomi.data.database.tables.*
class DbOpenHelper(context: Context)
: SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
/**
* Name of the database file.
*/
const val DATABASE_NAME = "tachiyomi.db"
/**
* Version of the database.
*/
const val DATABASE_VERSION = 3
}
override fun onCreate(db: SQLiteDatabase) = with(db) {
execSQL(MangaTable.createTableQuery)
execSQL(ChapterTable.createTableQuery)
execSQL(MangaSyncTable.createTableQuery)
execSQL(CategoryTable.createTableQuery)
execSQL(MangaCategoryTable.createTableQuery)
execSQL(HistoryTable.createTableQuery)
// DB indexes
execSQL(MangaTable.createUrlIndexQuery)
execSQL(MangaTable.createFavoriteIndexQuery)
execSQL(ChapterTable.createMangaIdIndexQuery)
execSQL(HistoryTable.createChapterIdIndexQuery)
}
override fun onUpgrade(db: SQLiteDatabase, oldVersion: Int, newVersion: Int) {
if (oldVersion < 2) {
db.execSQL(ChapterTable.sourceOrderUpdateQuery)
// Fix kissmanga covers after supporting cloudflare
db.execSQL("""UPDATE mangas SET thumbnail_url =
REPLACE(thumbnail_url, '93.174.95.110', 'kissmanga.com') WHERE source = 4""")
}
if (oldVersion < 3) {
// Initialize history tables
db.execSQL(HistoryTable.createTableQuery)
db.execSQL(HistoryTable.createChapterIdIndexQuery)
}
}
override fun onConfigure(db: SQLiteDatabase) {
db.setForeignKeyConstraintsEnabled(true)
}
}

View File

@ -0,0 +1,9 @@
package eu.kanade.tachiyomi.data.database
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
interface DbProvider {
val db: DefaultStorIOSQLite
}

View File

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
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.Category
import eu.kanade.tachiyomi.data.database.models.CategoryImpl
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_FLAGS
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_NAME
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.COL_ORDER
import eu.kanade.tachiyomi.data.database.tables.CategoryTable.TABLE
class CategoryTypeMapping : SQLiteTypeMapping<Category>(
CategoryPutResolver(),
CategoryGetResolver(),
CategoryDeleteResolver()
)
class CategoryPutResolver : DefaultPutResolver<Category>() {
override fun mapToInsertQuery(obj: Category) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: Category) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Category) = ContentValues(4).apply {
put(COL_ID, obj.id)
put(COL_NAME, obj.name)
put(COL_ORDER, obj.order)
put(COL_FLAGS, obj.flags)
}
}
class CategoryGetResolver : DefaultGetResolver<Category>() {
override fun mapFromCursor(cursor: Cursor): Category = CategoryImpl().apply {
id = cursor.getInt(cursor.getColumnIndex(COL_ID))
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
order = cursor.getInt(cursor.getColumnIndex(COL_ORDER))
flags = cursor.getInt(cursor.getColumnIndex(COL_FLAGS))
}
}
class CategoryDeleteResolver : DefaultDeleteResolver<Category>() {
override fun mapToDeleteQuery(obj: Category) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -0,0 +1,82 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
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.Chapter
import eu.kanade.tachiyomi.data.database.models.ChapterImpl
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_CHAPTER_NUMBER
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_FETCH
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_DATE_UPLOAD
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_ID
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_SOURCE_ORDER
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.COL_URL
import eu.kanade.tachiyomi.data.database.tables.ChapterTable.TABLE
class ChapterTypeMapping : SQLiteTypeMapping<Chapter>(
ChapterPutResolver(),
ChapterGetResolver(),
ChapterDeleteResolver()
)
class ChapterPutResolver : DefaultPutResolver<Chapter>() {
override fun mapToInsertQuery(obj: Chapter) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: Chapter) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Chapter) = ContentValues(10).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_URL, obj.url)
put(COL_NAME, obj.name)
put(COL_READ, obj.read)
put(COL_DATE_FETCH, obj.date_fetch)
put(COL_DATE_UPLOAD, obj.date_upload)
put(COL_LAST_PAGE_READ, obj.last_page_read)
put(COL_CHAPTER_NUMBER, obj.chapter_number)
put(COL_SOURCE_ORDER, obj.source_order)
}
}
class ChapterGetResolver : DefaultGetResolver<Chapter>() {
override fun mapFromCursor(cursor: Cursor): Chapter = ChapterImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
url = cursor.getString(cursor.getColumnIndex(COL_URL))
name = cursor.getString(cursor.getColumnIndex(COL_NAME))
read = cursor.getInt(cursor.getColumnIndex(COL_READ)) == 1
date_fetch = cursor.getLong(cursor.getColumnIndex(COL_DATE_FETCH))
date_upload = cursor.getLong(cursor.getColumnIndex(COL_DATE_UPLOAD))
last_page_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_PAGE_READ))
chapter_number = cursor.getFloat(cursor.getColumnIndex(COL_CHAPTER_NUMBER))
source_order = cursor.getInt(cursor.getColumnIndex(COL_SOURCE_ORDER))
}
}
class ChapterDeleteResolver : DefaultDeleteResolver<Chapter>() {
override fun mapToDeleteQuery(obj: Chapter) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -0,0 +1,62 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
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.History
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
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.COL_TIME_READ
import eu.kanade.tachiyomi.data.database.tables.HistoryTable.TABLE
class HistoryTypeMapping : SQLiteTypeMapping<History>(
HistoryPutResolver(),
HistoryGetResolver(),
HistoryDeleteResolver()
)
open class HistoryPutResolver : DefaultPutResolver<History>() {
override fun mapToInsertQuery(obj: History) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: History) = ContentValues(4).apply {
put(COL_ID, obj.id)
put(COL_CHAPTER_ID, obj.chapter_id)
put(COL_LAST_READ, obj.last_read)
put(COL_TIME_READ, obj.time_read)
}
}
class HistoryGetResolver : DefaultGetResolver<History>() {
override fun mapFromCursor(cursor: Cursor): History = History().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))
time_read = cursor.getLong(cursor.getColumnIndex(COL_TIME_READ))
}
}
class HistoryDeleteResolver : DefaultDeleteResolver<History>() {
override fun mapToDeleteQuery(obj: History) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -0,0 +1,59 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
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.MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_CATEGORY_ID
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.COL_MANGA_ID
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable.TABLE
class MangaCategoryTypeMapping : SQLiteTypeMapping<MangaCategory>(
MangaCategoryPutResolver(),
MangaCategoryGetResolver(),
MangaCategoryDeleteResolver()
)
class MangaCategoryPutResolver : DefaultPutResolver<MangaCategory>() {
override fun mapToInsertQuery(obj: MangaCategory) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: MangaCategory) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: MangaCategory) = ContentValues(3).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_CATEGORY_ID, obj.category_id)
}
}
class MangaCategoryGetResolver : DefaultGetResolver<MangaCategory>() {
override fun mapFromCursor(cursor: Cursor): MangaCategory = MangaCategory().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
manga_id = cursor.getLong(cursor.getColumnIndex(COL_MANGA_ID))
category_id = cursor.getInt(cursor.getColumnIndex(COL_CATEGORY_ID))
}
}
class MangaCategoryDeleteResolver : DefaultDeleteResolver<MangaCategory>() {
override fun mapToDeleteQuery(obj: MangaCategory) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -0,0 +1,78 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
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
class MangaSyncTypeMapping : SQLiteTypeMapping<MangaSync>(
MangaSyncPutResolver(),
MangaSyncGetResolver(),
MangaSyncDeleteResolver()
)
class MangaSyncPutResolver : DefaultPutResolver<MangaSync>() {
override fun mapToInsertQuery(obj: MangaSync) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: MangaSync) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: MangaSync) = ContentValues(9).apply {
put(COL_ID, obj.id)
put(COL_MANGA_ID, obj.manga_id)
put(COL_SYNC_ID, obj.sync_id)
put(COL_REMOTE_ID, obj.remote_id)
put(COL_TITLE, obj.title)
put(COL_LAST_CHAPTER_READ, obj.last_chapter_read)
put(COL_TOTAL_CHAPTERS, obj.total_chapters)
put(COL_STATUS, obj.status)
put(COL_SCORE, obj.score)
}
}
class MangaSyncGetResolver : DefaultGetResolver<MangaSync>() {
override fun mapFromCursor(cursor: Cursor): MangaSync = MangaSyncImpl().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))
remote_id = cursor.getInt(cursor.getColumnIndex(COL_REMOTE_ID))
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
last_chapter_read = cursor.getInt(cursor.getColumnIndex(COL_LAST_CHAPTER_READ))
total_chapters = cursor.getInt(cursor.getColumnIndex(COL_TOTAL_CHAPTERS))
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
score = cursor.getFloat(cursor.getColumnIndex(COL_SCORE))
}
}
class MangaSyncDeleteResolver : DefaultDeleteResolver<MangaSync>() {
override fun mapToDeleteQuery(obj: MangaSync) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -0,0 +1,96 @@
package eu.kanade.tachiyomi.data.database.mappers
import android.content.ContentValues
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.SQLiteTypeMapping
import com.pushtorefresh.storio.sqlite.operations.delete.DefaultDeleteResolver
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
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.Manga
import eu.kanade.tachiyomi.data.database.models.MangaImpl
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ARTIST
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_AUTHOR
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_CHAPTER_FLAGS
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_DESCRIPTION
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_FAVORITE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_GENRE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_ID
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_INITIALIZED
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_LAST_UPDATE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_SOURCE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_STATUS
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_THUMBNAIL_URL
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_TITLE
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_URL
import eu.kanade.tachiyomi.data.database.tables.MangaTable.COL_VIEWER
import eu.kanade.tachiyomi.data.database.tables.MangaTable.TABLE
class MangaTypeMapping : SQLiteTypeMapping<Manga>(
MangaPutResolver(),
MangaGetResolver(),
MangaDeleteResolver()
)
class MangaPutResolver : DefaultPutResolver<Manga>() {
override fun mapToInsertQuery(obj: Manga) = InsertQuery.builder()
.table(TABLE)
.build()
override fun mapToUpdateQuery(obj: Manga) = UpdateQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
override fun mapToContentValues(obj: Manga) = ContentValues(15).apply {
put(COL_ID, obj.id)
put(COL_SOURCE, obj.source)
put(COL_URL, obj.url)
put(COL_ARTIST, obj.artist)
put(COL_AUTHOR, obj.author)
put(COL_DESCRIPTION, obj.description)
put(COL_GENRE, obj.genre)
put(COL_TITLE, obj.title)
put(COL_STATUS, obj.status)
put(COL_THUMBNAIL_URL, obj.thumbnail_url)
put(COL_FAVORITE, obj.favorite)
put(COL_LAST_UPDATE, obj.last_update)
put(COL_INITIALIZED, obj.initialized)
put(COL_VIEWER, obj.viewer)
put(COL_CHAPTER_FLAGS, obj.chapter_flags)
}
}
open class MangaGetResolver : DefaultGetResolver<Manga>() {
override fun mapFromCursor(cursor: Cursor): Manga = MangaImpl().apply {
id = cursor.getLong(cursor.getColumnIndex(COL_ID))
source = cursor.getInt(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))
description = cursor.getString(cursor.getColumnIndex(COL_DESCRIPTION))
genre = cursor.getString(cursor.getColumnIndex(COL_GENRE))
title = cursor.getString(cursor.getColumnIndex(COL_TITLE))
status = cursor.getInt(cursor.getColumnIndex(COL_STATUS))
thumbnail_url = cursor.getString(cursor.getColumnIndex(COL_THUMBNAIL_URL))
favorite = cursor.getInt(cursor.getColumnIndex(COL_FAVORITE)) == 1
last_update = cursor.getLong(cursor.getColumnIndex(COL_LAST_UPDATE))
initialized = cursor.getInt(cursor.getColumnIndex(COL_INITIALIZED)) == 1
viewer = cursor.getInt(cursor.getColumnIndex(COL_VIEWER))
chapter_flags = cursor.getInt(cursor.getColumnIndex(COL_CHAPTER_FLAGS))
}
}
class MangaDeleteResolver : DefaultDeleteResolver<Manga>() {
override fun mapToDeleteQuery(obj: Manga) = DeleteQuery.builder()
.table(TABLE)
.where("$COL_ID = ?")
.whereArgs(obj.id)
.build()
}

View File

@ -1,38 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import java.io.Serializable;
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
@StorIOSQLiteType(table = CategoryTable.TABLE)
public class Category implements Serializable {
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ID, key = true)
public Integer id;
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_NAME)
public String name;
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_ORDER)
public int order;
@StorIOSQLiteColumn(name = CategoryTable.COLUMN_FLAGS)
public int flags;
public Category() {}
public static Category create(String name) {
Category c = new Category();
c.name = name;
return c;
}
public static Category createDefault() {
Category c = create("Default");
c.id = 0;
return c;
}
}

View File

@ -0,0 +1,27 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
interface Category : Serializable {
var id: Int?
var name: String
var order: Int
var flags: Int
val nameLower: String
get() = name.toLowerCase()
companion object {
fun create(name: String): Category = CategoryImpl().apply {
this.name = name
}
fun createDefault(): Category = create("Default").apply { id = 0 }
}
}

View File

@ -0,0 +1,26 @@
package eu.kanade.tachiyomi.data.database.models
class CategoryImpl : Category {
override var id: Int? = null
override lateinit var name: String
override var order: Int = 0
override var flags: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val category = other as Category
return name == category.name
}
override fun hashCode(): Int {
return name.hashCode()
}
}

View File

@ -1,71 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import java.io.Serializable;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
import eu.kanade.tachiyomi.util.UrlUtil;
@StorIOSQLiteType(table = ChapterTable.TABLE)
public class Chapter implements Serializable {
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_MANGA_ID)
public Long manga_id;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_URL)
public String url;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_NAME)
public String name;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_READ)
public boolean read;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_LAST_PAGE_READ)
public int last_page_read;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_FETCH)
public long date_fetch;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_DATE_UPLOAD)
public long date_upload;
@StorIOSQLiteColumn(name = ChapterTable.COLUMN_CHAPTER_NUMBER)
public float chapter_number;
public int status;
public Chapter() {}
public void setUrl(String url) {
this.url = UrlUtil.getPath(url);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Chapter chapter = (Chapter) o;
return url.equals(chapter.url);
}
@Override
public int hashCode() {
return url.hashCode();
}
public static Chapter create() {
Chapter chapter = new Chapter();
chapter.chapter_number = -1;
return chapter;
}
}

View File

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
interface Chapter : Serializable {
var id: Long?
var manga_id: Long?
var url: String
var name: String
var read: Boolean
var last_page_read: Int
var date_fetch: Long
var date_upload: Long
var chapter_number: Float
var source_order: Int
val isRecognizedNumber: Boolean
get() = chapter_number >= 0f
companion object {
fun create(): Chapter = ChapterImpl().apply {
chapter_number = -1f
}
}
}

View File

@ -0,0 +1,39 @@
package eu.kanade.tachiyomi.data.database.models
class ChapterImpl : Chapter {
override var id: Long? = null
override var manga_id: Long? = null
override lateinit var url: String
override lateinit var name: String
override var read: Boolean = false
override var last_page_read: Int = 0
override var date_fetch: Long = 0
override var date_upload: Long = 0
override var chapter_number: Float = 0f
override var source_order: Int = 0
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val chapter = other as Chapter
return url == chapter.url
}
override fun hashCode(): Int {
return url.hashCode()
}
}

View File

@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
/**
* Object containing the history statistics of a chapter
*/
class History : Serializable {
/**
* Id of history object.
*/
var id: Long? = null
/**
* Chapter id of history object.
*/
var chapter_id: Long = 0
/**
* Last time chapter was read in time long format
*/
var last_read: Long = 0
/**
* Total time chapter was read - todo not yet implemented
*/
var time_read: Long = 0
companion object {
/**
* History constructor
*
* @param chapter chapter object
* @return history object
*/
fun create(chapter: Chapter): History {
val history = History()
history.chapter_id = chapter.id!!
return history
}
}
}

View File

@ -1,194 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
import android.content.Context;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import java.io.Serializable;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
import eu.kanade.tachiyomi.util.UrlUtil;
@StorIOSQLiteType(table = MangaTable.TABLE)
public class Manga implements Serializable {
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_SOURCE)
public int source;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_URL)
public String url;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_ARTIST)
public String artist;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_AUTHOR)
public String author;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_DESCRIPTION)
public String description;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_GENRE)
public String genre;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_TITLE)
public String title;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_STATUS)
public int status;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_THUMBNAIL_URL)
public String thumbnail_url;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_FAVORITE)
public boolean favorite;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_LAST_UPDATE)
public long last_update;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_INITIALIZED)
public boolean initialized;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_VIEWER)
public int viewer;
@StorIOSQLiteColumn(name = MangaTable.COLUMN_CHAPTER_FLAGS)
public int chapter_flags;
public int unread;
public int category;
public static final int UNKNOWN = 0;
public static final int ONGOING = 1;
public static final int COMPLETED = 2;
public static final int LICENSED = 3;
public static final int SORT_AZ = 0x00000000;
public static final int SORT_ZA = 0x00000001;
public static final int SORT_MASK = 0x00000001;
public static final int SHOW_UNREAD = 0x00000002;
public static final int SHOW_READ = 0x00000004;
public static final int READ_MASK = 0x00000006;
public static final int SHOW_DOWNLOADED = 0x00000008;
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
public static final int DOWNLOADED_MASK = 0x00000018;
// Generic filter that does not filter anything
public static final int SHOW_ALL = 0x00000000;
public static final int DISPLAY_NAME = 0x00000000;
public static final int DISPLAY_NUMBER = 0x00100000;
public static final int DISPLAY_MASK = 0x00100000;
public Manga() {}
public static Manga create(String pathUrl) {
Manga m = new Manga();
m.url = pathUrl;
return m;
}
public void setUrl(String url) {
this.url = UrlUtil.getPath(url);
}
public void copyFrom(Manga other) {
if (other.title != null)
title = other.title;
if (other.author != null)
author = other.author;
if (other.artist != null)
artist = other.artist;
if (other.url != null)
url = other.url;
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;
}
public String getStatus(Context context) {
switch (status) {
case ONGOING:
return context.getString(R.string.ongoing);
case COMPLETED:
return context.getString(R.string.completed);
case LICENSED:
return context.getString(R.string.licensed);
default:
return context.getString(R.string.unknown);
}
}
public void setChapterOrder(int order) {
setFlags(order, SORT_MASK);
}
public void setDisplayMode(int mode) {
setFlags(mode, DISPLAY_MASK);
}
public void setReadFilter(int filter) {
setFlags(filter, READ_MASK);
}
public void setDownloadedFilter(int filter) {
setFlags(filter, DOWNLOADED_MASK);
}
private void setFlags(int flag, int mask) {
chapter_flags = (chapter_flags & ~mask) | (flag & mask);
}
public boolean sortChaptersAZ() {
return (chapter_flags & SORT_MASK) == SORT_AZ;
}
// Used to display the chapter's title one way or another
public int getDisplayMode() {
return chapter_flags & DISPLAY_MASK;
}
public int getReadFilter() {
return chapter_flags & READ_MASK;
}
public int getDownloadedFilter() {
return chapter_flags & DOWNLOADED_MASK;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Manga manga = (Manga) o;
return url.equals(manga.url);
}
@Override
public int hashCode() {
return url.hashCode();
}
}

View File

@ -0,0 +1,131 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
interface Manga : Serializable {
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 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)
}
private fun setFlags(flag: Int, mask: Int) {
chapter_flags = chapter_flags and mask.inv() or (flag and mask)
}
fun sortDescending(): Boolean {
return chapter_flags and SORT_MASK == SORT_DESC
}
// Used to display the chapter's title one way or another
var displayMode: Int
get() = chapter_flags and DISPLAY_MASK
set(mode) = setFlags(mode, DISPLAY_MASK)
var readFilter: Int
get() = chapter_flags and READ_MASK
set(filter) = setFlags(filter, READ_MASK)
var downloadedFilter: Int
get() = chapter_flags and DOWNLOADED_MASK
set(filter) = setFlags(filter, DOWNLOADED_MASK)
var sorting: Int
get() = chapter_flags and SORTING_MASK
set(sort) = setFlags(sort, SORTING_MASK)
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
// Generic filter that does not filter anything
const val SHOW_ALL = 0x00000000
const val SHOW_UNREAD = 0x00000002
const val SHOW_READ = 0x00000004
const val READ_MASK = 0x00000006
const val SHOW_DOWNLOADED = 0x00000008
const val SHOW_NOT_DOWNLOADED = 0x00000010
const val DOWNLOADED_MASK = 0x00000018
const val SORTING_SOURCE = 0x00000000
const val SORTING_NUMBER = 0x00000100
const val SORTING_MASK = 0x00000100
const val DISPLAY_NAME = 0x00000000
const val DISPLAY_NUMBER = 0x00100000
const val DISPLAY_MASK = 0x00100000
fun create(source: Int): Manga = MangaImpl().apply {
this.source = source
}
fun create(pathUrl: String, source: Int = 0): Manga = MangaImpl().apply {
url = pathUrl
this.source = source
}
}
}

View File

@ -1,29 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
@StorIOSQLiteType(table = MangaCategoryTable.TABLE)
public class MangaCategory {
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_MANGA_ID)
public long manga_id;
@StorIOSQLiteColumn(name = MangaCategoryTable.COLUMN_CATEGORY_ID)
public int category_id;
public MangaCategory() {}
public static MangaCategory create(Manga manga, Category category) {
MangaCategory mc = new MangaCategory();
mc.manga_id = manga.id;
mc.category_id = category.id;
return mc;
}
}

View File

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.data.database.models
class MangaCategory {
var id: Long? = null
var manga_id: Long = 0
var category_id: Int = 0
companion object {
fun create(manga: Manga, category: Category): MangaCategory {
val mc = MangaCategory()
mc.manga_id = manga.id!!
mc.category_id = category.id!!
return mc
}
}
}

View File

@ -1,12 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
public class MangaChapter {
public Manga manga;
public Chapter chapter;
public MangaChapter(Manga manga, Chapter chapter) {
this.manga = manga;
this.chapter = chapter;
}
}

View File

@ -0,0 +1,3 @@
package eu.kanade.tachiyomi.data.database.models
class MangaChapter(val manga: Manga, val chapter: Chapter)

View File

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

View File

@ -0,0 +1,53 @@
package eu.kanade.tachiyomi.data.database.models
class MangaImpl : Manga {
override var id: Long? = null
override var source: Int = 0
override lateinit var url: String
override lateinit var title: String
override var artist: String? = null
override var author: String? = null
override var description: String? = null
override var genre: String? = null
override var status: Int = 0
override var thumbnail_url: String? = null
override var favorite: Boolean = false
override var last_update: Long = 0
override var initialized: Boolean = false
override var viewer: Int = 0
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
val manga = other as Manga
return url == manga.url
}
override fun hashCode(): Int {
return url.hashCode()
}
}

View File

@ -1,55 +0,0 @@
package eu.kanade.tachiyomi.data.database.models;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import java.io.Serializable;
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
public class MangaSync implements Serializable {
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_ID, key = true)
public Long id;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_MANGA_ID)
public long manga_id;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_SYNC_ID)
public int sync_id;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_REMOTE_ID)
public int remote_id;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_TITLE)
public String title;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_LAST_CHAPTER_READ)
public int last_chapter_read;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_TOTAL_CHAPTERS)
public int total_chapters;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_SCORE)
public float score;
@StorIOSQLiteColumn(name = MangaSyncTable.COLUMN_STATUS)
public int status;
public boolean update;
public static MangaSync create(MangaSyncService service) {
MangaSync mangasync = new MangaSync();
mangasync.sync_id = service.getId();
return mangasync;
}
public void copyPersonalFrom(MangaSync other) {
last_chapter_read = other.last_chapter_read;
score = other.score;
status = other.status;
}
}

View File

@ -0,0 +1,40 @@
package eu.kanade.tachiyomi.data.database.models
import java.io.Serializable
interface MangaSync : Serializable {
var id: Long?
var manga_id: Long
var sync_id: Int
var remote_id: Int
var title: String
var last_chapter_read: Int
var total_chapters: Int
var score: Float
var status: Int
var update: Boolean
fun copyPersonalFrom(other: MangaSync) {
last_chapter_read = other.last_chapter_read
score = other.score
status = other.status
}
companion object {
fun create(serviceId: Int): MangaSync = MangaSyncImpl().apply {
sync_id = serviceId
}
}
}

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.data.database.models
class MangaSyncImpl : MangaSync {
override var id: Long? = null
override var manga_id: Long = 0
override var sync_id: Int = 0
override var remote_id: Int = 0
override lateinit var title: String
override var last_chapter_read: Int = 0
override var total_chapters: Int = 0
override var score: Float = 0f
override var status: Int = 0
override var update: Boolean = false
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (other == null || javaClass != other.javaClass) return false
val mangaSync = other as MangaSync
if (manga_id != mangaSync.manga_id) return false
if (sync_id != mangaSync.sync_id) return false
return remote_id == mangaSync.remote_id
}
override fun hashCode(): Int {
var result = (manga_id xor manga_id.ushr(32)).toInt()
result = 31 * result + sync_id
result = 31 * result + remote_id
return result
}
}

View File

@ -0,0 +1,36 @@
package eu.kanade.tachiyomi.data.database.queries
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.Category
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.tables.CategoryTable
interface CategoryQueries : DbProvider {
fun getCategories() = db.get()
.listOfObjects(Category::class.java)
.withQuery(Query.builder()
.table(CategoryTable.TABLE)
.orderBy(CategoryTable.COL_ORDER)
.build())
.prepare()
fun getCategoriesForManga(manga: Manga) = db.get()
.listOfObjects(Category::class.java)
.withQuery(RawQuery.builder()
.query(getCategoriesForMangaQuery())
.args(manga.id)
.build())
.prepare()
fun insertCategory(category: Category) = db.put().`object`(category).prepare()
fun insertCategories(categories: List<Category>) = db.put().objects(categories).prepare()
fun deleteCategory(category: Category) = db.delete().`object`(category).prepare()
fun deleteCategories(categories: List<Category>) = db.delete().objects(categories).prepare()
}

View File

@ -0,0 +1,59 @@
package eu.kanade.tachiyomi.data.database.queries
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.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.database.resolvers.ChapterProgressPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.ChapterSourceOrderPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import java.util.*
interface ChapterQueries : DbProvider {
fun getChapters(manga: Manga) = db.get()
.listOfObjects(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
fun getRecentChapters(date: Date) = db.get()
.listOfObjects(MangaChapter::class.java)
.withQuery(RawQuery.builder()
.query(getRecentsQuery())
.args(date.time)
.observesTables(ChapterTable.TABLE)
.build())
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare()
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
fun updateChapterProgress(chapter: Chapter) = db.put()
.`object`(chapter)
.withPutResolver(ChapterProgressPutResolver())
.prepare()
fun updateChaptersProgress(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterProgressPutResolver())
.prepare()
fun fixChaptersSourceOrder(chapters: List<Chapter>) = db.put()
.objects(chapters)
.withPutResolver(ChapterSourceOrderPutResolver())
.prepare()
}

View File

@ -0,0 +1,63 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.sqlite.queries.RawQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
import eu.kanade.tachiyomi.data.database.resolvers.HistoryLastReadPutResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterHistoryGetResolver
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
import java.util.*
interface HistoryQueries : DbProvider {
/**
* Insert history into database
* @param history object containing history information
*/
fun insertHistory(history: History) = db.put().`object`(history).prepare()
/**
* Returns history of recent manga containing last read chapter
* @param date recent date range
*/
fun getRecentManga(date: Date) = db.get()
.listOfObjects(MangaChapterHistory::class.java)
.withQuery(RawQuery.builder()
.query(getRecentMangasQuery())
.args(date.time)
.observesTables(HistoryTable.TABLE)
.build())
.withGetResolver(MangaChapterHistoryGetResolver.INSTANCE)
.prepare()
fun getHistoryByMangaId(mangaId: Long) = db.get()
.listOfObjects(History::class.java)
.withQuery(RawQuery.builder()
.query(getHistoryByMangaId())
.args(mangaId)
.observesTables(HistoryTable.TABLE)
.build())
.prepare()
/**
* Updates the history last read.
* Inserts history object if not yet in database
* @param history history object
*/
fun updateHistoryLastRead(history: History) = db.put()
.`object`(history)
.withPutResolver(HistoryLastReadPutResolver())
.prepare()
/**
* Updates the history last read.
* Inserts history object if not yet in database
* @param historyList history object list
*/
fun updateHistoryLastRead(historyList: List<History>) = db.put()
.objects(historyList)
.withPutResolver(HistoryLastReadPutResolver())
.prepare()
}

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.data.database.queries
import com.pushtorefresh.storio.Queries
import com.pushtorefresh.storio.sqlite.queries.DeleteQuery
import eu.kanade.tachiyomi.data.database.DbProvider
import eu.kanade.tachiyomi.data.database.inTransaction
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
interface MangaCategoryQueries : DbProvider {
fun insertMangaCategory(mangaCategory: MangaCategory) = db.put().`object`(mangaCategory).prepare()
fun insertMangasCategories(mangasCategories: List<MangaCategory>) = db.put().objects(mangasCategories).prepare()
fun deleteOldMangasCategories(mangas: List<Manga>) = db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaCategoryTable.TABLE)
.where("${MangaCategoryTable.COL_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.whereArgs(*mangas.map { it.id }.toTypedArray())
.build())
.prepare()
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
db.inTransaction {
deleteOldMangasCategories(mangas).executeAsBlocking()
insertMangasCategories(mangasCategories).executeAsBlocking()
}
}
}

View File

@ -0,0 +1,81 @@
package eu.kanade.tachiyomi.data.database.queries
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.Manga
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaFlagsPutResolver
import eu.kanade.tachiyomi.data.database.tables.ChapterTable
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable
import eu.kanade.tachiyomi.data.database.tables.MangaTable
interface MangaQueries : DbProvider {
fun getMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.build())
.prepare()
fun getLibraryMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(RawQuery.builder()
.query(libraryQuery)
.observesTables(MangaTable.TABLE, ChapterTable.TABLE, MangaCategoryTable.TABLE)
.build())
.withGetResolver(LibraryMangaGetResolver.INSTANCE)
.prepare()
open fun getFavoriteMangas() = db.get()
.listOfObjects(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs(1)
.orderBy(MangaTable.COL_TITLE)
.build())
.prepare()
fun getManga(url: String, sourceId: Int) = db.get()
.`object`(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_URL} = ? AND ${MangaTable.COL_SOURCE} = ?")
.whereArgs(url, sourceId)
.build())
.prepare()
fun getManga(id: Long) = db.get()
.`object`(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_ID} = ?")
.whereArgs(id)
.build())
.prepare()
fun insertManga(manga: Manga) = db.put().`object`(manga).prepare()
fun insertMangas(mangas: List<Manga>) = db.put().objects(mangas).prepare()
fun updateFlags(manga: Manga) = db.put()
.`object`(manga)
.withPutResolver(MangaFlagsPutResolver())
.prepare()
fun deleteManga(manga: Manga) = db.delete().`object`(manga).prepare()
fun deleteMangas(mangas: List<Manga>) = db.delete().objects(mangas).prepare()
fun deleteMangasNotInLibrary() = db.delete()
.byQuery(DeleteQuery.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COL_FAVORITE} = ?")
.whereArgs(0)
.build())
.prepare()
}

View File

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

@ -0,0 +1,84 @@
package eu.kanade.tachiyomi.data.database.queries
import eu.kanade.tachiyomi.data.database.tables.CategoryTable as Category
import eu.kanade.tachiyomi.data.database.tables.ChapterTable as Chapter
import eu.kanade.tachiyomi.data.database.tables.HistoryTable as History
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable as MangaCategory
import eu.kanade.tachiyomi.data.database.tables.MangaTable as Manga
/**
* Query to get the manga from the library, with their categories and unread count.
*/
val libraryQuery = """
SELECT M.*, COALESCE(MC.${MangaCategory.COL_CATEGORY_ID}, 0) AS ${Manga.COL_CATEGORY}
FROM (
SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COL_UNREAD}
FROM ${Manga.TABLE}
LEFT JOIN (
SELECT ${Chapter.COL_MANGA_ID}, COUNT(*) AS unread
FROM ${Chapter.TABLE}
WHERE ${Chapter.COL_READ} = 0
GROUP BY ${Chapter.COL_MANGA_ID}
) AS C
ON ${Manga.COL_ID} = C.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1
GROUP BY ${Manga.COL_ID}
ORDER BY ${Manga.COL_TITLE}
) AS M
LEFT JOIN (
SELECT * FROM ${MangaCategory.TABLE}) AS MC
ON MC.${MangaCategory.COL_MANGA_ID} = M.${Manga.COL_ID}
"""
/**
* Query to get the recent chapters of manga from the library up to a date.
*/
fun getRecentsQuery() = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE}
ON ${Manga.TABLE}.${Manga.COL_ID} = ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}
WHERE ${Manga.COL_FAVORITE} = 1 AND ${Chapter.COL_DATE_UPLOAD} > ?
ORDER BY ${Chapter.COL_DATE_UPLOAD} DESC
"""
/**
* Query to get the recently read chapters of manga from the library up to a date.
* The max_last_read table contains the most recent chapters grouped by manga
* The select statement returns all information of chapters that have the same id as the chapter in max_last_read
* and are read after the given time period
* @return return limit is 25
*/
fun getRecentMangasQuery() = """
SELECT ${Manga.TABLE}.${Manga.COL_URL} as mangaUrl, ${Manga.TABLE}.*, ${Chapter.TABLE}.*, ${History.TABLE}.*
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}
JOIN (
SELECT ${Chapter.TABLE}.${Chapter.COL_MANGA_ID},${Chapter.TABLE}.${Chapter.COL_ID} as ${History.COL_CHAPTER_ID}, MAX(${History.TABLE}.${History.COL_LAST_READ}) as ${History.COL_LAST_READ}
FROM ${Chapter.TABLE} JOIN ${History.TABLE}
ON ${Chapter.TABLE}.${Chapter.COL_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
GROUP BY ${Chapter.TABLE}.${Chapter.COL_MANGA_ID}) AS max_last_read
ON ${Chapter.TABLE}.${Chapter.COL_MANGA_ID} = max_last_read.${Chapter.COL_MANGA_ID}
WHERE ${History.TABLE}.${History.COL_LAST_READ} > ? AND max_last_read.${History.COL_CHAPTER_ID} = ${History.TABLE}.${History.COL_CHAPTER_ID}
ORDER BY max_last_read.${History.COL_LAST_READ} DESC
LIMIT 25
"""
fun getHistoryByMangaId() = """
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_MANGA_ID} = ? AND ${History.TABLE}.${History.COL_CHAPTER_ID} = ${Chapter.TABLE}.${Chapter.COL_ID}
"""
/**
* Query to get the categories for a manga.
*/
fun getCategoriesForMangaQuery() = """
SELECT ${Category.TABLE}.* FROM ${Category.TABLE}
JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COL_ID} =
${MangaCategory.TABLE}.${MangaCategory.COL_CATEGORY_ID}
WHERE ${MangaCategory.COL_MANGA_ID} = ?
"""

View File

@ -0,0 +1,34 @@
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 ChapterProgressPutResolver : PutResolver<Chapter>() {
override fun performPut(db: StorIOSQLite, chapter: Chapter) = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(chapter)
val contentValues = mapToContentValues(chapter)
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, contentValues)
PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
fun mapToUpdateQuery(chapter: Chapter) = UpdateQuery.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COL_ID} = ?")
.whereArgs(chapter.id)
.build()
fun mapToContentValues(chapter: Chapter) = ContentValues(2).apply {
put(ChapterTable.COL_READ, chapter.read)
put(ChapterTable.COL_LAST_PAGE_READ, chapter.last_page_read)
}
}

View File

@ -0,0 +1,32 @@
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 ChapterSourceOrderPutResolver : 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} = ? AND ${ChapterTable.COL_MANGA_ID} = ?")
.whereArgs(chapter.url, chapter.manga_id)
.build()
fun mapToContentValues(chapter: Chapter) = ContentValues(1).apply {
put(ChapterTable.COL_SOURCE_ORDER, chapter.source_order)
}
}

View File

@ -0,0 +1,64 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.content.ContentValues
import android.support.annotation.NonNull
import com.pushtorefresh.storio.sqlite.StorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.put.PutResult
import com.pushtorefresh.storio.sqlite.queries.Query
import com.pushtorefresh.storio.sqlite.queries.UpdateQuery
import eu.kanade.tachiyomi.data.database.inTransactionReturn
import eu.kanade.tachiyomi.data.database.mappers.HistoryPutResolver
import eu.kanade.tachiyomi.data.database.models.History
import eu.kanade.tachiyomi.data.database.tables.HistoryTable
class HistoryLastReadPutResolver : HistoryPutResolver() {
/**
* Updates last_read time of chapter
*/
override fun performPut(@NonNull db: StorIOSQLite, @NonNull history: History): PutResult = db.inTransactionReturn {
val updateQuery = mapToUpdateQuery(history)
val cursor = db.lowLevel().query(Query.builder()
.table(updateQuery.table())
.where(updateQuery.where())
.whereArgs(updateQuery.whereArgs())
.build())
val putResult: PutResult
try {
if (cursor.count == 0) {
val insertQuery = mapToInsertQuery(history)
val insertedId = db.lowLevel().insert(insertQuery, mapToContentValues(history))
putResult = PutResult.newInsertResult(insertedId, insertQuery.table())
} else {
val numberOfRowsUpdated = db.lowLevel().update(updateQuery, mapToUpdateContentValues(history))
putResult = PutResult.newUpdateResult(numberOfRowsUpdated, updateQuery.table())
}
} finally {
cursor.close()
}
putResult
}
/**
* Creates update query
* @param obj history object
*/
override fun mapToUpdateQuery(obj: History) = UpdateQuery.builder()
.table(HistoryTable.TABLE)
.where("${HistoryTable.COL_CHAPTER_ID} = ?")
.whereArgs(obj.chapter_id)
.build()
/**
* Create content query
* @param history object
*/
fun mapToUpdateContentValues(history: History) = ContentValues(1).apply {
put(HistoryTable.COL_LAST_READ, history.last_read)
}
}

View File

@ -1,61 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers;
import android.database.Cursor;
import android.support.annotation.NonNull;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
public class LibraryMangaGetResolver extends MangaStorIOSQLiteGetResolver {
public static final LibraryMangaGetResolver INSTANCE = new LibraryMangaGetResolver();
public static final String QUERY = String.format(
"SELECT M.*, COALESCE(MC.%10$s, 0) AS %12$s " +
"FROM (" +
"SELECT %1$s.*, COALESCE(C.unread, 0) AS %6$s " +
"FROM %1$s " +
"LEFT JOIN (" +
"SELECT %5$s, COUNT(*) AS unread " +
"FROM %2$s " +
"WHERE %7$s = 0 " +
"GROUP BY %5$s" +
") AS C " +
"ON %4$s = C.%5$s " +
"WHERE %8$s = 1 " +
"GROUP BY %4$s " +
"ORDER BY %9$s" +
") AS M " +
"LEFT JOIN (SELECT * FROM %3$s) AS MC ON MC.%11$s = M.%4$s",
MangaTable.TABLE,
ChapterTable.TABLE,
MangaCategoryTable.TABLE,
MangaTable.COLUMN_ID,
ChapterTable.COLUMN_MANGA_ID,
MangaTable.COLUMN_UNREAD,
ChapterTable.COLUMN_READ,
MangaTable.COLUMN_FAVORITE,
MangaTable.COLUMN_TITLE,
MangaCategoryTable.COLUMN_CATEGORY_ID,
MangaCategoryTable.COLUMN_MANGA_ID,
MangaTable.COLUMN_CATEGORY
);
@Override
@NonNull
public Manga mapFromCursor(@NonNull Cursor cursor) {
Manga manga = super.mapFromCursor(cursor);
int unreadColumn = cursor.getColumnIndex(MangaTable.COLUMN_UNREAD);
manga.unread = cursor.getInt(unreadColumn);
int categoryColumn = cursor.getColumnIndex(MangaTable.COLUMN_CATEGORY);
manga.category = cursor.getInt(categoryColumn);
return manga;
}
}

View File

@ -0,0 +1,26 @@
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 eu.kanade.tachiyomi.data.database.tables.MangaTable
class LibraryMangaGetResolver : MangaGetResolver() {
companion object {
val INSTANCE = LibraryMangaGetResolver()
}
override fun mapFromCursor(cursor: Cursor): Manga {
val manga = super.mapFromCursor(cursor)
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD)
manga.unread = cursor.getInt(unreadColumn)
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
manga.category = cursor.getInt(categoryColumn)
return manga
}
}

View File

@ -1,56 +0,0 @@
package eu.kanade.tachiyomi.data.database.resolvers;
import android.database.Cursor;
import android.support.annotation.NonNull;
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver;
import java.util.Date;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.ChapterStorIOSQLiteGetResolver;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
public class MangaChapterGetResolver extends DefaultGetResolver<MangaChapter> {
public static final MangaChapterGetResolver INSTANCE = new MangaChapterGetResolver();
public static final String QUERY = String.format(
"SELECT * FROM %1$s JOIN %2$s on %1$s.%3$s = %2$s.%4$s",
MangaTable.TABLE,
ChapterTable.TABLE,
MangaTable.COLUMN_ID,
ChapterTable.COLUMN_MANGA_ID);
public static String getRecentChaptersQuery(Date date) {
return QUERY + String.format(" WHERE %1$s = 1 AND %2$s > %3$d ORDER BY %2$s DESC",
MangaTable.COLUMN_FAVORITE,
ChapterTable.COLUMN_DATE_UPLOAD,
date.getTime());
}
@NonNull
private final MangaStorIOSQLiteGetResolver mangaGetResolver;
@NonNull
private final ChapterStorIOSQLiteGetResolver chapterGetResolver;
public MangaChapterGetResolver() {
this.mangaGetResolver = new MangaStorIOSQLiteGetResolver();
this.chapterGetResolver = new ChapterStorIOSQLiteGetResolver();
}
@NonNull
@Override
public MangaChapter mapFromCursor(@NonNull Cursor cursor) {
final Manga manga = mangaGetResolver.mapFromCursor(cursor);
final Chapter chapter = chapterGetResolver.mapFromCursor(cursor);
manga.id = chapter.manga_id;
return new MangaChapter(manga, chapter);
}
}

View File

@ -0,0 +1,28 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
import eu.kanade.tachiyomi.data.database.models.MangaChapter
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
companion object {
val INSTANCE = MangaChapterGetResolver()
}
private val mangaGetResolver = MangaGetResolver()
private val chapterGetResolver = ChapterGetResolver()
override fun mapFromCursor(cursor: Cursor): MangaChapter {
val manga = mangaGetResolver.mapFromCursor(cursor)
val chapter = chapterGetResolver.mapFromCursor(cursor)
manga.id = chapter.manga_id
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
return MangaChapter(manga, chapter)
}
}

View File

@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.data.database.resolvers
import android.database.Cursor
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
import eu.kanade.tachiyomi.data.database.mappers.HistoryGetResolver
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
import eu.kanade.tachiyomi.data.database.models.MangaChapterHistory
class MangaChapterHistoryGetResolver : DefaultGetResolver<MangaChapterHistory>() {
companion object {
val INSTANCE = MangaChapterHistoryGetResolver()
}
/**
* Manga get resolver
*/
private val mangaGetResolver = MangaGetResolver()
/**
* Chapter get resolver
*/
private val chapterResolver = ChapterGetResolver()
/**
* History get resolver
*/
private val historyGetResolver = HistoryGetResolver()
/**
* Map correct objects from cursor result
*/
override fun mapFromCursor(cursor: Cursor): MangaChapterHistory {
// Get manga object
val manga = mangaGetResolver.mapFromCursor(cursor)
// Get chapter object
val chapter = chapterResolver.mapFromCursor(cursor)
// Get history object
val history = historyGetResolver.mapFromCursor(cursor)
// Make certain column conflicts are dealt with
manga.id = chapter.manga_id
manga.url = cursor.getString(cursor.getColumnIndex("mangaUrl"))
chapter.id = history.chapter_id
// Return result
return MangaChapterHistory(manga, chapter, history)
}
}

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 MangaFlagsPutResolver : 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_CHAPTER_FLAGS, manga.chapter_flags)
}
}

View File

@ -1,39 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables;
import android.support.annotation.NonNull;
public class CategoryTable {
@NonNull
public static final String TABLE = "categories";
@NonNull
public static final String COLUMN_ID = "_id";
@NonNull
public static final String COLUMN_NAME = "name";
@NonNull
public static final String COLUMN_ORDER = "sort";
@NonNull
public static final String COLUMN_FLAGS = "flags";
// This is just class with Meta Data, we don't need instances
private CategoryTable() {
throw new IllegalStateException("No instances please");
}
// Better than static final field -> allows VM to unload useless String
// Because you need this string only once per application life on the device
@NonNull
public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_NAME + " TEXT NOT NULL, "
+ COLUMN_ORDER + " INTEGER NOT NULL, "
+ COLUMN_FLAGS + " INTEGER NOT NULL"
+ ");";
}
}

View File

@ -0,0 +1,23 @@
package eu.kanade.tachiyomi.data.database.tables
object CategoryTable {
const val TABLE = "categories"
const val COL_ID = "_id"
const val COL_NAME = "name"
const val COL_ORDER = "sort"
const val COL_FLAGS = "flags"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_NAME TEXT NOT NULL,
$COL_ORDER INTEGER NOT NULL,
$COL_FLAGS INTEGER NOT NULL
)"""
}

View File

@ -1,59 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables;
import android.support.annotation.NonNull;
public class ChapterTable {
@NonNull
public static final String TABLE = "chapters";
@NonNull
public static final String COLUMN_ID = "_id";
@NonNull
public static final String COLUMN_MANGA_ID = "manga_id";
@NonNull
public static final String COLUMN_URL = "url";
@NonNull
public static final String COLUMN_NAME = "name";
@NonNull
public static final String COLUMN_READ = "read";
@NonNull
public static final String COLUMN_DATE_FETCH = "date_fetch";
@NonNull
public static final String COLUMN_DATE_UPLOAD = "date_upload";
@NonNull
public static final String COLUMN_LAST_PAGE_READ = "last_page_read";
@NonNull
public static final String COLUMN_CHAPTER_NUMBER = "chapter_number";
@NonNull
public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
+ COLUMN_URL + " TEXT NOT NULL, "
+ COLUMN_NAME + " TEXT NOT NULL, "
+ COLUMN_READ + " BOOLEAN NOT NULL, "
+ COLUMN_LAST_PAGE_READ + " INT NOT NULL, "
+ COLUMN_CHAPTER_NUMBER + " FLOAT NOT NULL, "
+ COLUMN_DATE_FETCH + " LONG NOT NULL, "
+ COLUMN_DATE_UPLOAD + " LONG NOT NULL, "
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
+ "ON DELETE CASCADE"
+ ");";
}
public static String getCreateMangaIdIndexQuery() {
return "CREATE INDEX " + TABLE + "_" + COLUMN_MANGA_ID + "_index ON " + TABLE + "(" + COLUMN_MANGA_ID + ");";
}
}

View File

@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.data.database.tables
object ChapterTable {
const val TABLE = "chapters"
const val COL_ID = "_id"
const val COL_MANGA_ID = "manga_id"
const val COL_URL = "url"
const val COL_NAME = "name"
const val COL_READ = "read"
const val COL_DATE_FETCH = "date_fetch"
const val COL_DATE_UPLOAD = "date_upload"
const val COL_LAST_PAGE_READ = "last_page_read"
const val COL_CHAPTER_NUMBER = "chapter_number"
const val COL_SOURCE_ORDER = "source_order"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_URL TEXT NOT NULL,
$COL_NAME TEXT NOT NULL,
$COL_READ BOOLEAN NOT NULL,
$COL_LAST_PAGE_READ INT NOT NULL,
$COL_CHAPTER_NUMBER FLOAT NOT NULL,
$COL_SOURCE_ORDER INTEGER NOT NULL,
$COL_DATE_FETCH LONG NOT NULL,
$COL_DATE_UPLOAD LONG NOT NULL,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE
)"""
val createMangaIdIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_MANGA_ID}_index ON $TABLE($COL_MANGA_ID)"
val sourceOrderUpdateQuery: String
get() = "ALTER TABLE $TABLE ADD COLUMN $COL_SOURCE_ORDER INTEGER DEFAULT 0"
}

View File

@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.data.database.tables
object HistoryTable {
/**
* Table name
*/
const val TABLE = "history"
/**
* Id column name
*/
const val COL_ID = "${TABLE}_id"
/**
* Chapter id column name
*/
const val COL_CHAPTER_ID = "${TABLE}_chapter_id"
/**
* Last read column name
*/
const val COL_LAST_READ = "${TABLE}_last_read"
/**
* Time read column name
*/
const val COL_TIME_READ = "${TABLE}_time_read"
/**
* query to create history table
*/
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_CHAPTER_ID INTEGER NOT NULL UNIQUE,
$COL_LAST_READ LONG,
$COL_TIME_READ LONG,
FOREIGN KEY($COL_CHAPTER_ID) REFERENCES ${ChapterTable.TABLE} (${ChapterTable.COL_ID})
ON DELETE CASCADE
)"""
/**
* query to index history chapter id
*/
val createChapterIdIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_CHAPTER_ID}_index ON $TABLE($COL_CHAPTER_ID)"
}

View File

@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables;
import android.support.annotation.NonNull;
public class MangaCategoryTable {
@NonNull
public static final String TABLE = "mangas_categories";
@NonNull
public static final String COLUMN_ID = "_id";
@NonNull
public static final String COLUMN_MANGA_ID = "manga_id";
@NonNull
public static final String COLUMN_CATEGORY_ID = "category_id";
// This is just class with Meta Data, we don't need instances
private MangaCategoryTable() {
throw new IllegalStateException("No instances please");
}
// Better than static final field -> allows VM to unload useless String
// Because you need this string only once per application life on the device
@NonNull
public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
+ COLUMN_CATEGORY_ID + " INTEGER NOT NULL, "
+ "FOREIGN KEY(" + COLUMN_CATEGORY_ID + ") REFERENCES " + CategoryTable.TABLE + "(" + CategoryTable.COLUMN_ID + ") "
+ "ON DELETE CASCADE, "
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
+ "ON DELETE CASCADE"
+ ");";
}
}

View File

@ -0,0 +1,24 @@
package eu.kanade.tachiyomi.data.database.tables
object MangaCategoryTable {
const val TABLE = "mangas_categories"
const val COL_ID = "_id"
const val COL_MANGA_ID = "manga_id"
const val COL_CATEGORY_ID = "category_id"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_CATEGORY_ID INTEGER NOT NULL,
FOREIGN KEY($COL_CATEGORY_ID) REFERENCES ${CategoryTable.TABLE} (${CategoryTable.COL_ID})
ON DELETE CASCADE,
FOREIGN KEY($COL_MANGA_ID) REFERENCES ${MangaTable.TABLE} (${MangaTable.COL_ID})
ON DELETE CASCADE
)"""
}

View File

@ -1,45 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables;
import android.support.annotation.NonNull;
public class MangaSyncTable {
public static final String TABLE = "manga_sync";
public static final String COLUMN_ID = "_id";
public static final String COLUMN_MANGA_ID = "manga_id";
public static final String COLUMN_SYNC_ID = "sync_id";
public static final String COLUMN_REMOTE_ID = "remote_id";
public static final String COLUMN_TITLE = "title";
public static final String COLUMN_LAST_CHAPTER_READ = "last_chapter_read";
public static final String COLUMN_STATUS = "status";
public static final String COLUMN_SCORE = "score";
public static final String COLUMN_TOTAL_CHAPTERS = "total_chapters";
@NonNull
public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_MANGA_ID + " INTEGER NOT NULL, "
+ COLUMN_SYNC_ID + " INTEGER NOT NULL, "
+ COLUMN_REMOTE_ID + " INTEGER NOT NULL, "
+ COLUMN_TITLE + " TEXT NOT NULL, "
+ COLUMN_LAST_CHAPTER_READ + " INTEGER NOT NULL, "
+ COLUMN_TOTAL_CHAPTERS + " INTEGER NOT NULL, "
+ COLUMN_STATUS + " INTEGER NOT NULL, "
+ COLUMN_SCORE + " FLOAT NOT NULL, "
+ "UNIQUE (" + COLUMN_MANGA_ID + ", " + COLUMN_SYNC_ID + ") ON CONFLICT REPLACE, "
+ "FOREIGN KEY(" + COLUMN_MANGA_ID + ") REFERENCES " + MangaTable.TABLE + "(" + MangaTable.COLUMN_ID + ") "
+ "ON DELETE CASCADE"
+ ");";
}
}

View File

@ -0,0 +1,41 @@
package eu.kanade.tachiyomi.data.database.tables
object MangaSyncTable {
const val TABLE = "manga_sync"
const val COL_ID = "_id"
const val COL_MANGA_ID = "manga_id"
const val COL_SYNC_ID = "sync_id"
const val COL_REMOTE_ID = "remote_id"
const val COL_TITLE = "title"
const val COL_LAST_CHAPTER_READ = "last_chapter_read"
const val COL_STATUS = "status"
const val COL_SCORE = "score"
const val COL_TOTAL_CHAPTERS = "total_chapters"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_MANGA_ID INTEGER NOT NULL,
$COL_SYNC_ID INTEGER NOT NULL,
$COL_REMOTE_ID INTEGER NOT NULL,
$COL_TITLE TEXT NOT NULL,
$COL_LAST_CHAPTER_READ INTEGER NOT NULL,
$COL_TOTAL_CHAPTERS INTEGER NOT NULL,
$COL_STATUS INTEGER NOT NULL,
$COL_SCORE FLOAT 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
)"""
}

View File

@ -1,98 +0,0 @@
package eu.kanade.tachiyomi.data.database.tables;
import android.support.annotation.NonNull;
public class MangaTable {
@NonNull
public static final String TABLE = "mangas";
@NonNull
public static final String COLUMN_ID = "_id";
@NonNull
public static final String COLUMN_SOURCE = "source";
@NonNull
public static final String COLUMN_URL = "url";
@NonNull
public static final String COLUMN_ARTIST = "artist";
@NonNull
public static final String COLUMN_AUTHOR = "author" ;
@NonNull
public static final String COLUMN_DESCRIPTION = "description";
@NonNull
public static final String COLUMN_GENRE = "genre";
@NonNull
public static final String COLUMN_TITLE = "title";
@NonNull
public static final String COLUMN_STATUS = "status";
@NonNull
public static final String COLUMN_THUMBNAIL_URL = "thumbnail_url";
@NonNull
public static final String COLUMN_FAVORITE = "favorite";
@NonNull
public static final String COLUMN_LAST_UPDATE = "last_update";
@NonNull
public static final String COLUMN_INITIALIZED = "initialized";
@NonNull
public static final String COLUMN_VIEWER = "viewer";
@NonNull
public static final String COLUMN_CHAPTER_FLAGS = "chapter_flags";
@NonNull
public static final String COLUMN_UNREAD = "unread";
@NonNull
public static final String COLUMN_CATEGORY = "category";
// This is just class with Meta Data, we don't need instances
private MangaTable() {
throw new IllegalStateException("No instances please");
}
// Better than static final field -> allows VM to unload useless String
// Because you need this string only once per application life on the device
@NonNull
public static String getCreateTableQuery() {
return "CREATE TABLE " + TABLE + "("
+ COLUMN_ID + " INTEGER NOT NULL PRIMARY KEY, "
+ COLUMN_SOURCE + " INTEGER NOT NULL, "
+ COLUMN_URL + " TEXT NOT NULL, "
+ COLUMN_ARTIST + " TEXT, "
+ COLUMN_AUTHOR + " TEXT, "
+ COLUMN_DESCRIPTION + " TEXT, "
+ COLUMN_GENRE + " TEXT, "
+ COLUMN_TITLE + " TEXT NOT NULL, "
+ COLUMN_STATUS + " INTEGER NOT NULL, "
+ COLUMN_THUMBNAIL_URL + " TEXT, "
+ COLUMN_FAVORITE + " INTEGER NOT NULL, "
+ COLUMN_LAST_UPDATE + " LONG, "
+ COLUMN_INITIALIZED + " BOOLEAN NOT NULL, "
+ COLUMN_VIEWER + " INTEGER NOT NULL, "
+ COLUMN_CHAPTER_FLAGS + " INTEGER NOT NULL"
+ ");";
}
public static String getCreateUrlIndexQuery() {
return "CREATE INDEX " + TABLE + "_" + COLUMN_URL + "_index ON " + TABLE + "(" + COLUMN_URL + ");";
}
public static String getCreateFavoriteIndexQuery() {
return "CREATE INDEX " + TABLE + "_" + COLUMN_FAVORITE + "_index ON " + TABLE + "(" + COLUMN_FAVORITE + ");";
}
}

View File

@ -0,0 +1,65 @@
package eu.kanade.tachiyomi.data.database.tables
object MangaTable {
const val TABLE = "mangas"
const val COL_ID = "_id"
const val COL_SOURCE = "source"
const val COL_URL = "url"
const val COL_ARTIST = "artist"
const val COL_AUTHOR = "author"
const val COL_DESCRIPTION = "description"
const val COL_GENRE = "genre"
const val COL_TITLE = "title"
const val COL_STATUS = "status"
const val COL_THUMBNAIL_URL = "thumbnail_url"
const val COL_FAVORITE = "favorite"
const val COL_LAST_UPDATE = "last_update"
const val COL_INITIALIZED = "initialized"
const val COL_VIEWER = "viewer"
const val COL_CHAPTER_FLAGS = "chapter_flags"
const val COL_UNREAD = "unread"
const val COL_CATEGORY = "category"
val createTableQuery: String
get() = """CREATE TABLE $TABLE(
$COL_ID INTEGER NOT NULL PRIMARY KEY,
$COL_SOURCE INTEGER NOT NULL,
$COL_URL TEXT NOT NULL,
$COL_ARTIST TEXT,
$COL_AUTHOR TEXT,
$COL_DESCRIPTION TEXT,
$COL_GENRE TEXT,
$COL_TITLE TEXT NOT NULL,
$COL_STATUS INTEGER NOT NULL,
$COL_THUMBNAIL_URL TEXT,
$COL_FAVORITE INTEGER NOT NULL,
$COL_LAST_UPDATE LONG,
$COL_INITIALIZED BOOLEAN NOT NULL,
$COL_VIEWER INTEGER NOT NULL,
$COL_CHAPTER_FLAGS INTEGER NOT NULL
)"""
val createUrlIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_URL}_index ON $TABLE($COL_URL)"
val createFavoriteIndexQuery: String
get() = "CREATE INDEX ${TABLE}_${COL_FAVORITE}_index ON $TABLE($COL_FAVORITE)"
}

View File

@ -1,441 +0,0 @@
package eu.kanade.tachiyomi.data.download;
import android.content.Context;
import android.net.Uri;
import com.google.gson.Gson;
import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
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.base.Source;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
import eu.kanade.tachiyomi.util.DiskUtils;
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator;
import eu.kanade.tachiyomi.util.UrlUtil;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subjects.BehaviorSubject;
import rx.subjects.PublishSubject;
import timber.log.Timber;
public class DownloadManager {
private Context context;
private SourceManager sourceManager;
private PreferencesHelper preferences;
private Gson gson;
private PublishSubject<List<Download>> downloadsQueueSubject;
private BehaviorSubject<Boolean> runningSubject;
private Subscription downloadsSubscription;
private BehaviorSubject<Integer> threadsSubject;
private Subscription threadsSubscription;
private DownloadQueue queue;
private volatile boolean isRunning;
public static final String PAGE_LIST_FILE = "index.json";
public DownloadManager(Context context, SourceManager sourceManager, PreferencesHelper preferences) {
this.context = context;
this.sourceManager = sourceManager;
this.preferences = preferences;
gson = new Gson();
queue = new DownloadQueue();
downloadsQueueSubject = PublishSubject.create();
runningSubject = BehaviorSubject.create();
threadsSubject = BehaviorSubject.create();
}
private void initializeSubscriptions() {
if (downloadsSubscription != null && !downloadsSubscription.isUnsubscribed())
downloadsSubscription.unsubscribe();
threadsSubscription = preferences.downloadThreads().asObservable()
.subscribe(threadsSubject::onNext);
downloadsSubscription = downloadsQueueSubject
.flatMap(Observable::from)
.lift(new DynamicConcurrentMergeOperator<>(this::downloadChapter, threadsSubject))
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.map(download -> areAllDownloadsFinished())
.subscribe(finished -> {
if (finished) {
DownloadService.stop(context);
}
}, e -> Timber.e(e.getCause(), e.getMessage()));
if (!isRunning) {
isRunning = true;
runningSubject.onNext(true);
}
}
public void destroySubscriptions() {
if (isRunning) {
isRunning = false;
runningSubject.onNext(false);
}
if (downloadsSubscription != null && !downloadsSubscription.isUnsubscribed()) {
downloadsSubscription.unsubscribe();
downloadsSubscription = null;
}
if (threadsSubscription != null && !threadsSubscription.isUnsubscribed()) {
threadsSubscription.unsubscribe();
}
}
// Create a download object for every chapter in the event and add them to the downloads queue
public void onDownloadChaptersEvent(DownloadChaptersEvent event) {
final Manga manga = event.getManga();
final Source source = sourceManager.get(manga.source);
// Used to avoid downloading chapters with the same name
final List<String> addedChapters = new ArrayList<>();
final List<Download> pending = new ArrayList<>();
for (Chapter chapter : event.getChapters()) {
if (addedChapters.contains(chapter.name))
continue;
addedChapters.add(chapter.name);
Download download = new Download(source, manga, chapter);
if (!prepareDownload(download)) {
queue.add(download);
pending.add(download);
}
}
if (isRunning) downloadsQueueSubject.onNext(pending);
}
// Public method to check if a chapter is downloaded
public boolean isChapterDownloaded(Source source, Manga manga, Chapter chapter) {
File directory = getAbsoluteChapterDirectory(source, manga, chapter);
if (!directory.exists())
return false;
List<Page> pages = getSavedPageList(source, manga, chapter);
return isChapterDownloaded(directory, pages);
}
// Prepare the download. Returns true if the chapter is already downloaded
private boolean prepareDownload(Download download) {
// If the chapter is already queued, don't add it again
for (Download queuedDownload : queue) {
if (download.chapter.id.equals(queuedDownload.chapter.id))
return true;
}
// Add the directory to the download object for future access
download.directory = getAbsoluteChapterDirectory(download);
// If the directory doesn't exist, the chapter isn't downloaded.
if (!download.directory.exists()) {
return false;
}
// If the page list doesn't exist, the chapter isn't downloaded
List<Page> savedPages = getSavedPageList(download);
if (savedPages == null)
return false;
// Add the page list to the download object for future access
download.pages = savedPages;
// If the number of files matches the number of pages, the chapter is downloaded.
// We have the index file, so we check one file more
return isChapterDownloaded(download.directory, download.pages);
}
// Check that all the images are downloaded
private boolean isChapterDownloaded(File directory, List<Page> pages) {
return pages != null && !pages.isEmpty() && pages.size() + 1 == directory.listFiles().length;
}
// Download the entire chapter
private Observable<Download> downloadChapter(Download download) {
try {
DiskUtils.createDirectory(download.directory);
} catch (IOException e) {
return Observable.error(e);
}
Observable<List<Page>> pageListObservable = download.pages == null ?
// Pull page list from network and add them to download object
download.source
.pullPageListFromNetwork(download.chapter.url)
.doOnNext(pages -> download.pages = pages)
.doOnNext(pages -> savePageList(download)) :
// Or if the page list already exists, start from the file
Observable.just(download.pages);
return Observable.defer(() -> pageListObservable
.doOnNext(pages -> {
download.downloadedImages = 0;
download.setStatus(Download.DOWNLOADING);
})
// Get all the URLs to the source images, fetch pages if necessary
.flatMap(download.source::getAllImageUrlsFromPageList)
// Start downloading images, consider we can have downloaded images already
.concatMap(page -> getOrDownloadImage(page, download))
// Do after download completes
.doOnCompleted(() -> onDownloadCompleted(download))
.toList()
.map(pages -> download)
// If the page list threw, it will resume here
.onErrorResumeNext(error -> {
download.setStatus(Download.ERROR);
return Observable.just(download);
}))
.subscribeOn(Schedulers.io());
}
// Get the image from the filesystem if it exists or download from network
private Observable<Page> getOrDownloadImage(final Page page, Download download) {
// If the image URL is empty, do nothing
if (page.getImageUrl() == null)
return Observable.just(page);
String filename = getImageFilename(page);
File imagePath = new File(download.directory, filename);
// If the image is already downloaded, do nothing. Otherwise download from network
Observable<Page> pageObservable = isImageDownloaded(imagePath) ?
Observable.just(page) :
downloadImage(page, download.source, download.directory, filename);
return pageObservable
// When the image is ready, set image path, progress (just in case) and status
.doOnNext(p -> {
page.setImagePath(imagePath.getAbsolutePath());
page.setProgress(100);
download.downloadedImages++;
page.setStatus(Page.READY);
})
// Mark this page as error and allow to download the remaining
.onErrorResumeNext(e -> {
page.setProgress(0);
page.setStatus(Page.ERROR);
return Observable.just(page);
});
}
// Save image on disk
private Observable<Page> downloadImage(Page page, Source source, File directory, String filename) {
page.setStatus(Page.DOWNLOAD_IMAGE);
return source.getImageProgressResponse(page)
.flatMap(resp -> {
try {
DiskUtils.saveBufferedSourceToDirectory(resp.body().source(), directory, filename);
} catch (Exception e) {
Timber.e(e.getCause(), e.getMessage());
return Observable.error(e);
}
return Observable.just(page);
})
.retry(2);
}
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
public Observable<Page> getDownloadedImage(final Page page, File chapterDir) {
if (page.getImageUrl() == null) {
page.setStatus(Page.ERROR);
return Observable.just(page);
}
File imagePath = new File(chapterDir, getImageFilename(page));
// When the image is ready, set image path, progress (just in case) and status
if (isImageDownloaded(imagePath)) {
page.setImagePath(imagePath.getAbsolutePath());
page.setProgress(100);
page.setStatus(Page.READY);
} else {
page.setStatus(Page.ERROR);
}
return Observable.just(page);
}
// Get the filename for an image given the page
private String getImageFilename(Page page) {
String url = page.getImageUrl();
int number = page.getPageNumber() + 1;
// Try to preserve file extension
if (UrlUtil.isJpg(url)) {
return number + ".jpg";
} else if (UrlUtil.isPng(url)) {
return number + ".png";
} else if (UrlUtil.isGif(url)) {
return number + ".gif";
}
return Uri.parse(url).getLastPathSegment().replaceAll("[^\\sa-zA-Z0-9.-]", "_");
}
private boolean isImageDownloaded(File imagePath) {
return imagePath.exists();
}
// Called when a download finishes. This doesn't mean the download was successful, so we check it
private void onDownloadCompleted(final Download download) {
checkDownloadIsSuccessful(download);
savePageList(download);
}
private void checkDownloadIsSuccessful(final Download download) {
int actualProgress = 0;
int status = Download.DOWNLOADED;
// If any page has an error, the download result will be error
for (Page page : download.pages) {
actualProgress += page.getProgress();
if (page.getStatus() != Page.READY) status = Download.ERROR;
}
// Ensure that the chapter folder has all the images
if (!isChapterDownloaded(download.directory, download.pages)) {
status = Download.ERROR;
}
download.totalProgress = actualProgress;
download.setStatus(status);
// Delete successful downloads from queue after notifying
if (status == Download.DOWNLOADED) {
queue.remove(download);
}
}
// Return the page list from the chapter's directory if it exists, null otherwise
public List<Page> getSavedPageList(Source source, Manga manga, Chapter chapter) {
File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter);
File pagesFile = new File(chapterDir, PAGE_LIST_FILE);
JsonReader reader = null;
try {
if (pagesFile.exists()) {
reader = new JsonReader(new FileReader(pagesFile.getAbsolutePath()));
Type collectionType = new TypeToken<List<Page>>() {}.getType();
return gson.fromJson(reader, collectionType);
}
} catch (Exception e) {
Timber.e(e.getCause(), e.getMessage());
} finally {
if (reader != null) try { reader.close(); } catch (IOException e) { /* Do nothing */ }
}
return null;
}
// Shortcut for the method above
private List<Page> getSavedPageList(Download download) {
return getSavedPageList(download.source, download.manga, download.chapter);
}
// Save the page list to the chapter's directory
public void savePageList(Source source, Manga manga, Chapter chapter, List<Page> pages) {
File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter);
File pagesFile = new File(chapterDir, PAGE_LIST_FILE);
FileOutputStream out = null;
try {
out = new FileOutputStream(pagesFile);
out.write(gson.toJson(pages).getBytes());
out.flush();
} catch (IOException e) {
Timber.e(e.getCause(), e.getMessage());
} finally {
if (out != null) try { out.close(); } catch (IOException e) { /* Do nothing */ }
}
}
// Shortcut for the method above
private void savePageList(Download download) {
savePageList(download.source, download.manga, download.chapter, download.pages);
}
// Get the absolute path to the chapter directory
public File getAbsoluteChapterDirectory(Source source, Manga manga, Chapter chapter) {
String chapterRelativePath = source.getName() +
File.separator +
manga.title.replaceAll("[^\\sa-zA-Z0-9.-]", "_") +
File.separator +
chapter.name.replaceAll("[^\\sa-zA-Z0-9.-]", "_");
return new File(preferences.getDownloadsDirectory(), chapterRelativePath);
}
// Shortcut for the method above
private File getAbsoluteChapterDirectory(Download download) {
return getAbsoluteChapterDirectory(download.source, download.manga, download.chapter);
}
public void deleteChapter(Source source, Manga manga, Chapter chapter) {
File path = getAbsoluteChapterDirectory(source, manga, chapter);
DiskUtils.deleteFiles(path);
}
public DownloadQueue getQueue() {
return queue;
}
public boolean areAllDownloadsFinished() {
for (Download download : queue) {
if (download.getStatus() <= Download.DOWNLOADING)
return false;
}
return true;
}
public boolean startDownloads() {
if (queue.isEmpty())
return false;
if (downloadsSubscription == null)
initializeSubscriptions();
final List<Download> pending = new ArrayList<>();
for (Download download : queue) {
if (download.getStatus() != Download.DOWNLOADED) {
if (download.getStatus() != Download.QUEUE) download.setStatus(Download.QUEUE);
pending.add(download);
}
}
downloadsQueueSubject.onNext(pending);
return !pending.isEmpty();
}
public void stopDownloads() {
destroySubscriptions();
for (Download download : queue) {
if (download.getStatus() == Download.DOWNLOADING) {
download.setStatus(Download.ERROR);
}
}
}
public BehaviorSubject<Boolean> getRunningSubject() {
return runningSubject;
}
}

View File

@ -0,0 +1,450 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
import android.net.Uri
import com.google.gson.Gson
import com.google.gson.reflect.TypeToken
import com.google.gson.stream.JsonReader
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.download.model.DownloadQueue
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.preference.getOrDefault
import eu.kanade.tachiyomi.data.source.Source
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.*
import rx.Observable
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import rx.subjects.BehaviorSubject
import rx.subjects.PublishSubject
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
import java.io.File
import java.io.FileReader
import java.util.*
class DownloadManager(
private val context: Context,
private val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) {
private val gson = Gson()
private val downloadsQueueSubject = PublishSubject.create<List<Download>>()
val runningSubject = BehaviorSubject.create<Boolean>()
private var downloadsSubscription: Subscription? = null
val downloadNotifier by lazy { DownloadNotifier(context) }
private val threadsSubject = BehaviorSubject.create<Int>()
private var threadsSubscription: Subscription? = null
val queue = DownloadQueue()
val imageFilenameRegex = "[^\\sa-zA-Z0-9.-]".toRegex()
val PAGE_LIST_FILE = "index.json"
@Volatile var isRunning: Boolean = false
private set
private fun initializeSubscriptions() {
downloadsSubscription?.unsubscribe()
threadsSubscription = preferences.downloadThreads().asObservable()
.subscribe {
threadsSubject.onNext(it)
downloadNotifier.multipleDownloadThreads = it > 1
}
downloadsSubscription = downloadsQueueSubject.flatMap { Observable.from(it) }
.lift(DynamicConcurrentMergeOperator<Download, Download>({ downloadChapter(it) }, threadsSubject))
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.subscribe({
// Delete successful downloads from queue
if (it.status == Download.DOWNLOADED) {
// remove downloaded chapter from queue
queue.del(it)
downloadNotifier.onProgressChange(queue)
}
if (areAllDownloadsFinished()) {
DownloadService.stop(context)
}
}, { error ->
DownloadService.stop(context)
Timber.e(error)
downloadNotifier.onError(error.message)
})
if (!isRunning) {
isRunning = true
runningSubject.onNext(true)
}
}
fun destroySubscriptions() {
if (isRunning) {
isRunning = false
runningSubject.onNext(false)
}
if (downloadsSubscription != null) {
downloadsSubscription?.unsubscribe()
downloadsSubscription = null
}
if (threadsSubscription != null) {
threadsSubscription?.unsubscribe()
}
}
// Create a download object for every chapter and add them to the downloads queue
fun downloadChapters(manga: Manga, chapters: List<Chapter>) {
val source = sourceManager.get(manga.source) as? OnlineSource ?: return
// Add chapters to queue from the start
val sortedChapters = chapters.sortedByDescending { it.source_order }
// Used to avoid downloading chapters with the same name
val addedChapters = ArrayList<String>()
val pending = ArrayList<Download>()
for (chapter in sortedChapters) {
if (addedChapters.contains(chapter.name))
continue
addedChapters.add(chapter.name)
val download = Download(source, manga, chapter)
if (!prepareDownload(download)) {
queue.add(download)
pending.add(download)
}
}
// Initialize queue size
downloadNotifier.initialQueueSize = queue.size
// Show notification
downloadNotifier.onProgressChange(queue)
if (isRunning) downloadsQueueSubject.onNext(pending)
}
// Public method to check if a chapter is downloaded
fun isChapterDownloaded(source: Source, manga: Manga, chapter: Chapter): Boolean {
val directory = getAbsoluteChapterDirectory(source, manga, chapter)
if (!directory.exists())
return false
val pages = getSavedPageList(source, manga, chapter)
return isChapterDownloaded(directory, pages)
}
// Prepare the download. Returns true if the chapter is already downloaded
private fun prepareDownload(download: Download): Boolean {
// If the chapter is already queued, don't add it again
for (queuedDownload in queue) {
if (download.chapter.id == queuedDownload.chapter.id)
return true
}
// Add the directory to the download object for future access
download.directory = getAbsoluteChapterDirectory(download)
// If the directory doesn't exist, the chapter isn't downloaded.
if (!download.directory.exists()) {
return false
}
// If the page list doesn't exist, the chapter isn't downloaded
val savedPages = getSavedPageList(download) ?: return false
// Add the page list to the download object for future access
download.pages = savedPages
// If the number of files matches the number of pages, the chapter is downloaded.
// We have the index file, so we check one file more
return isChapterDownloaded(download.directory, download.pages)
}
// Check that all the images are downloaded
private fun isChapterDownloaded(directory: File, pages: List<Page>?): Boolean {
return pages != null && !pages.isEmpty() && pages.size + 1 == directory.listFiles().size
}
// Download the entire chapter
private fun downloadChapter(download: Download): Observable<Download> {
DiskUtils.createDirectory(download.directory)
val pageListObservable: Observable<List<Page>> = if (download.pages == null)
// Pull page list from network and add them to download object
download.source.fetchPageListFromNetwork(download.chapter)
.doOnNext { pages ->
download.pages = pages
savePageList(download)
}
else
// Or if the page list already exists, start from the file
Observable.just(download.pages)
return Observable.defer {
pageListObservable
.doOnNext { pages ->
download.downloadedImages = 0
download.status = Download.DOWNLOADING
}
// Get all the URLs to the source images, fetch pages if necessary
.flatMap { download.source.fetchAllImageUrlsFromPageList(it) }
// Start downloading images, consider we can have downloaded images already
.concatMap { page -> getOrDownloadImage(page, download) }
// Do when page is downloaded.
.doOnNext {
downloadNotifier.onProgressChange(download, queue)
}
// Do after download completes
.doOnCompleted { onDownloadCompleted(download) }
.toList()
.map { pages -> download }
// If the page list threw, it will resume here
.onErrorResumeNext { error ->
download.status = Download.ERROR
downloadNotifier.onError(error.message, download.chapter.name)
Observable.just(download)
}
}.subscribeOn(Schedulers.io())
}
// Get the image from the filesystem if it exists or download from network
private fun getOrDownloadImage(page: Page, download: Download): Observable<Page> {
// If the image URL is empty, do nothing
if (page.imageUrl == null)
return Observable.just(page)
val filename = getImageFilename(page)
val imagePath = File(download.directory, filename)
// If the image is already downloaded, do nothing. Otherwise download from network
val pageObservable = if (isImageDownloaded(imagePath))
Observable.just(page)
else
downloadImage(page, download.source, download.directory, filename)
return pageObservable
// When the image is ready, set image path, progress (just in case) and status
.doOnNext {
page.imagePath = imagePath.absolutePath
page.progress = 100
download.downloadedImages++
page.status = Page.READY
}
// Mark this page as error and allow to download the remaining
.onErrorResumeNext {
page.progress = 0
page.status = Page.ERROR
Observable.just(page)
}
}
// Save image on disk
private fun downloadImage(page: Page, source: OnlineSource, directory: File, filename: String): Observable<Page> {
page.status = Page.DOWNLOAD_IMAGE
return source.imageResponse(page)
.map {
val file = File(directory, filename)
try {
file.parentFile.mkdirs()
it.body().source().saveTo(file.outputStream())
} catch (e: Exception) {
it.close()
file.delete()
throw e
}
page
}
// Retry 3 times, waiting 2, 4 and 8 seconds between attempts.
.retryWhen(RetryWithDelay(3, { (2 shl it - 1) * 1000 }, Schedulers.trampoline()))
}
// Public method to get the image from the filesystem. It does NOT provide any way to download the image
fun getDownloadedImage(page: Page, chapterDir: File): Observable<Page> {
if (page.imageUrl == null) {
page.status = Page.ERROR
return Observable.just(page)
}
val imagePath = File(chapterDir, getImageFilename(page))
// When the image is ready, set image path, progress (just in case) and status
if (isImageDownloaded(imagePath)) {
page.imagePath = imagePath.absolutePath
page.progress = 100
page.status = Page.READY
} else {
page.status = Page.ERROR
}
return Observable.just(page)
}
// Get the filename for an image given the page
private fun getImageFilename(page: Page): String {
val url = page.imageUrl
val number = String.format("%03d", page.pageNumber + 1)
// Try to preserve file extension
return when {
UrlUtil.isJpg(url) -> "$number.jpg"
UrlUtil.isPng(url) -> "$number.png"
UrlUtil.isGif(url) -> "$number.gif"
else -> Uri.parse(url).lastPathSegment.replace(imageFilenameRegex, "_")
}
}
private fun isImageDownloaded(imagePath: File): Boolean {
return imagePath.exists()
}
// Called when a download finishes. This doesn't mean the download was successful, so we check it
private fun onDownloadCompleted(download: Download) {
checkDownloadIsSuccessful(download)
savePageList(download)
}
private fun checkDownloadIsSuccessful(download: Download) {
var actualProgress = 0
var status = Download.DOWNLOADED
// If any page has an error, the download result will be error
for (page in download.pages!!) {
actualProgress += page.progress
if (page.status != Page.READY) {
status = Download.ERROR
downloadNotifier.onError(context.getString(R.string.download_notifier_page_ready_error), download.chapter.name)
}
}
// Ensure that the chapter folder has all the images
if (!isChapterDownloaded(download.directory, download.pages)) {
status = Download.ERROR
downloadNotifier.onError(context.getString(R.string.download_notifier_page_error), download.chapter.name)
}
download.totalProgress = actualProgress
download.status = status
}
// Return the page list from the chapter's directory if it exists, null otherwise
fun getSavedPageList(source: Source, manga: Manga, chapter: Chapter): List<Page>? {
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
return try {
JsonReader(FileReader(pagesFile)).use {
val collectionType = object : TypeToken<List<Page>>() {}.type
gson.fromJson(it, collectionType)
}
} catch (e: Exception) {
null
}
}
// Shortcut for the method above
private fun getSavedPageList(download: Download): List<Page>? {
return getSavedPageList(download.source, download.manga, download.chapter)
}
// Save the page list to the chapter's directory
fun savePageList(source: Source, manga: Manga, chapter: Chapter, pages: List<Page>) {
val chapterDir = getAbsoluteChapterDirectory(source, manga, chapter)
val pagesFile = File(chapterDir, PAGE_LIST_FILE)
pagesFile.outputStream().use {
try {
it.write(gson.toJson(pages).toByteArray())
it.flush()
} catch (error: Exception) {
Timber.e(error)
}
}
}
// Shortcut for the method above
private fun savePageList(download: Download) {
savePageList(download.source, download.manga, download.chapter, download.pages!!)
}
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
val mangaRelativePath = source.toString() +
File.separator +
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
return File(preferences.downloadsDirectory().getOrDefault(), mangaRelativePath)
}
// Get the absolute path to the chapter directory
fun getAbsoluteChapterDirectory(source: Source, manga: Manga, chapter: Chapter): File {
val chapterRelativePath = chapter.name.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
return File(getAbsoluteMangaDirectory(source, manga), chapterRelativePath)
}
// Shortcut for the method above
private fun getAbsoluteChapterDirectory(download: Download): File {
return getAbsoluteChapterDirectory(download.source, download.manga, download.chapter)
}
fun deleteChapter(source: Source, manga: Manga, chapter: Chapter) {
val path = getAbsoluteChapterDirectory(source, manga, chapter)
DiskUtils.deleteFiles(path)
}
fun areAllDownloadsFinished(): Boolean {
for (download in queue) {
if (download.status <= Download.DOWNLOADING)
return false
}
return true
}
fun startDownloads(): Boolean {
if (queue.isEmpty())
return false
if (downloadsSubscription == null || downloadsSubscription!!.isUnsubscribed)
initializeSubscriptions()
val pending = ArrayList<Download>()
for (download in queue) {
if (download.status != Download.DOWNLOADED) {
if (download.status != Download.QUEUE) download.status = Download.QUEUE
pending.add(download)
}
}
downloadsQueueSubject.onNext(pending)
return !pending.isEmpty()
}
fun stopDownloads(errorMessage: String? = null) {
destroySubscriptions()
for (download in queue) {
if (download.status == Download.DOWNLOADING) {
download.status = Download.ERROR
}
}
errorMessage?.let { downloadNotifier.onError(it) }
}
fun clearQueue() {
queue.clear()
downloadNotifier.onClear()
}
}

View File

@ -0,0 +1,173 @@
package eu.kanade.tachiyomi.data.download
import android.content.Context
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.util.notificationManager
/**
* DownloadNotifier is used to show notifications when downloading one or multiple chapters.
*
* @param context context of application
*/
class DownloadNotifier(private val context: Context) {
/**
* Notification builder.
*/
private val notificationBuilder = NotificationCompat.Builder(context)
/**
* Id of the notification.
*/
private val notificationId: Int
get() = Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ID
/**
* Status of download. Used for correct notification icon.
*/
private var isDownloading = false
/**
* The size of queue on start download.
*/
internal var initialQueueSize = 0
/**
* Simultaneous download setting > 1.
*/
internal var multipleDownloadThreads = false
/**
* Called when download progress changes.
* Note: Only accepted when multi download active.
*
* @param queue the queue containing downloads.
*/
internal 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
*/
internal 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()) {
onComplete(null)
return
}
} else {
if (download != null && download.pages!!.size == download.downloadedImages) {
onComplete(download)
return
}
}
// Create notification
with (notificationBuilder) {
// Check if icon needs refresh
if (!isDownloading) {
setSmallIcon(android.R.drawable.stat_sys_download)
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 {
if (it.chapter.name.length >= 33)
setContentTitle(it.chapter.name.slice(IntRange(0, 30)).plus("..."))
else
setContentTitle(it.chapter.name)
setContentText(context.getString(R.string.chapter_downloading_progress)
.format(it.downloadedImages, it.pages!!.size))
setProgress(it.pages!!.size, it.downloadedImages, false)
}
}
}
// Displays the progress bar on notification
context.notificationManager.notify(notificationId, notificationBuilder.build())
}
/**
* Called when chapter is downloaded
*
* @param download download object containing download information
*/
private fun onComplete(download: Download?) {
// Create notification.
with(notificationBuilder) {
setContentTitle(download?.chapter?.name ?: context.getString(R.string.app_name))
setContentText(context.getString(R.string.update_check_notification_download_complete))
setSmallIcon(android.R.drawable.stat_sys_download_done)
setProgress(0, 0, false)
}
// Show notification.
context.notificationManager.notify(notificationId, notificationBuilder.build())
// Reset initial values
isDownloading = false
initialQueueSize = 0
}
/**
* Clears the notification message
*/
internal fun onClear() {
context.notificationManager.cancel(notificationId)
}
/**
* Called on error while downloading chapter
*
* @param error string containing error information
* @param chapter string containing chapter title
*/
internal fun onError(error: String? = null, chapter: String? = null) {
// Create notification
with(notificationBuilder) {
setContentTitle(chapter ?: context.getString(R.string.download_notifier_title_error))
setContentText(error ?: context.getString(R.string.download_notifier_unkown_error))
setSmallIcon(android.R.drawable.stat_sys_warning)
setProgress(0, 0, false)
}
context.notificationManager.notify(Constants.NOTIFICATION_DOWNLOAD_CHAPTER_ERROR_ID, notificationBuilder.build())
// Reset download information
onClear()
isDownloading = false
}
}

View File

@ -1,149 +0,0 @@
package eu.kanade.tachiyomi.data.download;
import android.app.Service;
import android.content.Context;
import android.content.Intent;
import android.os.IBinder;
import android.os.PowerManager;
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork;
import javax.inject.Inject;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
import eu.kanade.tachiyomi.util.EventBusHook;
import eu.kanade.tachiyomi.util.ToastUtil;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class DownloadService extends Service {
@Inject DownloadManager downloadManager;
@Inject PreferencesHelper preferences;
private PowerManager.WakeLock wakeLock;
private Subscription networkChangeSubscription;
private Subscription queueRunningSubscription;
private boolean isRunning;
public static void start(Context context) {
context.startService(new Intent(context, DownloadService.class));
}
public static void stop(Context context) {
context.stopService(new Intent(context, DownloadService.class));
}
@Override
public void onCreate() {
super.onCreate();
App.get(this).getComponent().inject(this);
createWakeLock();
listenQueueRunningChanges();
EventBus.getDefault().registerSticky(this);
listenNetworkChanges();
}
@Override
public int onStartCommand(Intent intent, int flags, int startId) {
return START_STICKY;
}
@Override
public void onDestroy() {
EventBus.getDefault().unregister(this);
queueRunningSubscription.unsubscribe();
networkChangeSubscription.unsubscribe();
downloadManager.destroySubscriptions();
destroyWakeLock();
super.onDestroy();
}
@Override
public IBinder onBind(Intent intent) {
return null;
}
@EventBusHook
public void onEvent(DownloadChaptersEvent event) {
EventBus.getDefault().removeStickyEvent(event);
downloadManager.onDownloadChaptersEvent(event);
}
private void listenNetworkChanges() {
networkChangeSubscription = new ReactiveNetwork().enableInternetCheck()
.observeConnectivity(getApplicationContext())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(state -> {
switch (state) {
case WIFI_CONNECTED_HAS_INTERNET:
// If there are no remaining downloads, destroy the service
if (!isRunning && !downloadManager.startDownloads()) {
stopSelf();
}
break;
case MOBILE_CONNECTED:
if (!preferences.downloadOnlyOverWifi()) {
if (!isRunning && !downloadManager.startDownloads()) {
stopSelf();
}
} else if (isRunning) {
downloadManager.stopDownloads();
}
break;
default:
if (isRunning) {
downloadManager.stopDownloads();
}
break;
}
}, error -> {
ToastUtil.showShort(this, R.string.download_queue_error);
stopSelf();
});
}
private void listenQueueRunningChanges() {
queueRunningSubscription = downloadManager.getRunningSubject()
.subscribe(running -> {
isRunning = running;
if (running)
acquireWakeLock();
else
releaseWakeLock();
});
}
private void createWakeLock() {
wakeLock = ((PowerManager)getSystemService(POWER_SERVICE)).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "DownloadService:WakeLock");
}
private void destroyWakeLock() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
wakeLock = null;
}
}
public void acquireWakeLock() {
if (wakeLock != null && !wakeLock.isHeld()) {
wakeLock.acquire();
}
}
public void releaseWakeLock() {
if (wakeLock != null && wakeLock.isHeld()) {
wakeLock.release();
}
}
}

View File

@ -0,0 +1,132 @@
package eu.kanade.tachiyomi.data.download
import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import android.os.PowerManager
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.toast
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
class DownloadService : Service() {
companion object {
fun start(context: Context) {
context.startService(Intent(context, DownloadService::class.java))
}
fun stop(context: Context) {
context.stopService(Intent(context, DownloadService::class.java))
}
}
val downloadManager: DownloadManager by injectLazy()
val preferences: PreferencesHelper by injectLazy()
private var wakeLock: PowerManager.WakeLock? = null
private var networkChangeSubscription: Subscription? = null
private var queueRunningSubscription: Subscription? = null
private var isRunning: Boolean = false
override fun onCreate() {
super.onCreate()
createWakeLock()
listenQueueRunningChanges()
listenNetworkChanges()
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
return Service.START_STICKY
}
override fun onDestroy() {
queueRunningSubscription?.unsubscribe()
networkChangeSubscription?.unsubscribe()
downloadManager.destroySubscriptions()
destroyWakeLock()
super.onDestroy()
}
override fun onBind(intent: Intent): IBinder? {
return null
}
private fun listenNetworkChanges() {
networkChangeSubscription = ReactiveNetwork().enableInternetCheck()
.observeConnectivity(applicationContext)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ state ->
when (state) {
ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET -> {
// If there are no remaining downloads, destroy the service
if (!isRunning && !downloadManager.startDownloads()) {
stopSelf()
}
}
ConnectivityStatus.MOBILE_CONNECTED -> {
if (!preferences.downloadOnlyOverWifi()) {
if (!isRunning && !downloadManager.startDownloads()) {
stopSelf()
}
} else if (isRunning) {
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
}
}
else -> {
if (isRunning) {
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
}
}
}
}, { error ->
toast(R.string.download_queue_error)
stopSelf()
})
}
private fun listenQueueRunningChanges() {
queueRunningSubscription = downloadManager.runningSubject.subscribe { running ->
isRunning = running
if (running)
acquireWakeLock()
else
releaseWakeLock()
}
}
private fun createWakeLock() {
wakeLock = (getSystemService(Context.POWER_SERVICE) as PowerManager).newWakeLock(
PowerManager.PARTIAL_WAKE_LOCK, "DownloadService:WakeLock")
}
private fun destroyWakeLock() {
if (wakeLock != null && wakeLock!!.isHeld) {
wakeLock!!.release()
wakeLock = null
}
}
fun acquireWakeLock() {
if (wakeLock != null && !wakeLock!!.isHeld) {
wakeLock!!.acquire()
}
}
fun releaseWakeLock() {
if (wakeLock != null && wakeLock!!.isHeld) {
wakeLock!!.release()
}
}
}

View File

@ -1,55 +0,0 @@
package eu.kanade.tachiyomi.data.download.model;
import java.io.File;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.Page;
import rx.subjects.PublishSubject;
public class Download {
public Source source;
public Manga manga;
public Chapter chapter;
public List<Page> pages;
public File directory;
public transient volatile int totalProgress;
public transient volatile int downloadedImages;
private transient volatile int status;
private transient PublishSubject<Download> statusSubject;
public static final int NOT_DOWNLOADED = 0;
public static final int QUEUE = 1;
public static final int DOWNLOADING = 2;
public static final int DOWNLOADED = 3;
public static final int ERROR = 4;
public Download(Source source, Manga manga, Chapter chapter) {
this.source = source;
this.manga = manga;
this.chapter = chapter;
}
public int getStatus() {
return status;
}
public void setStatus(int status) {
this.status = status;
notifyStatus();
}
public void setStatusSubject(PublishSubject<Download> subject) {
this.statusSubject = subject;
}
private void notifyStatus() {
if (statusSubject != null)
statusSubject.onNext(this);
}
}

View File

@ -0,0 +1,40 @@
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 rx.subjects.PublishSubject
import java.io.File
class Download(val source: OnlineSource, val manga: Manga, val chapter: Chapter) {
lateinit var directory: File
var pages: List<Page>? = null
@Volatile @Transient var totalProgress: Int = 0
@Volatile @Transient var downloadedImages: Int = 0
@Volatile @Transient var status: Int = 0
set(status) {
field = status
statusSubject?.onNext(this)
}
@Transient private var statusSubject: PublishSubject<Download>? = null
fun setStatusSubject(subject: PublishSubject<Download>?) {
statusSubject = subject
}
companion object {
const val NOT_DOWNLOADED = 0
const val QUEUE = 1
const val DOWNLOADING = 2
const val DOWNLOADED = 3
const val ERROR = 4
}
}

View File

@ -1,78 +0,0 @@
package eu.kanade.tachiyomi.data.download.model;
import java.util.ArrayList;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.source.model.Page;
import rx.Observable;
import rx.subjects.PublishSubject;
public class DownloadQueue extends ArrayList<Download> {
private PublishSubject<Download> statusSubject;
public DownloadQueue() {
super();
statusSubject = PublishSubject.create();
}
public boolean add(Download download) {
download.setStatusSubject(statusSubject);
download.setStatus(Download.QUEUE);
return super.add(download);
}
public void remove(Download download) {
super.remove(download);
download.setStatusSubject(null);
}
public void remove(Chapter chapter) {
for (Download download : this) {
if (download.chapter.id.equals(chapter.id)) {
remove(download);
break;
}
}
}
public Observable<Download> getActiveDownloads() {
return Observable.from(this)
.filter(download -> download.getStatus() == Download.DOWNLOADING);
}
public Observable<Download> getStatusObservable() {
return statusSubject.onBackpressureBuffer();
}
public Observable<Download> getProgressObservable() {
return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads())
.flatMap(download -> {
if (download.getStatus() == Download.DOWNLOADING) {
PublishSubject<Integer> pageStatusSubject = PublishSubject.create();
setPagesSubject(download.pages, pageStatusSubject);
return pageStatusSubject
.filter(status -> status == Page.READY)
.map(status -> download);
} else if (download.getStatus() == Download.DOWNLOADED ||
download.getStatus() == Download.ERROR) {
setPagesSubject(download.pages, null);
}
return Observable.just(download);
})
.filter(download -> download.getStatus() == Download.DOWNLOADING);
}
private void setPagesSubject(List<Page> pages, PublishSubject<Integer> subject) {
if (pages != null) {
for (Page page : pages) {
page.setStatusSubject(subject);
}
}
}
}

View File

@ -0,0 +1,72 @@
package eu.kanade.tachiyomi.data.download.model
import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.source.model.Page
import rx.Observable
import rx.subjects.PublishSubject
import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue(private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>())
: List<Download> by queue {
private val statusSubject = PublishSubject.create<Download>()
private val removeSubject = PublishSubject.create<Download>()
fun add(download: Download): Boolean {
download.setStatusSubject(statusSubject)
download.status = Download.QUEUE
return queue.add(download)
}
fun del(download: Download) {
val removed = queue.remove(download)
download.setStatusSubject(null)
if (removed) {
removeSubject.onNext(download)
}
}
fun del(chapter: Chapter) {
find { it.chapter.id == chapter.id }?.let { del(it) }
}
fun clear() {
queue.forEach { del(it) }
}
fun getActiveDownloads(): Observable<Download> =
Observable.from(this).filter { download -> download.status == Download.DOWNLOADING }
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
fun getRemovedObservable(): Observable<Download> = removeSubject.onBackpressureBuffer()
fun getProgressObservable(): Observable<Download> {
return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads())
.flatMap { download ->
if (download.status == Download.DOWNLOADING) {
val pageStatusSubject = PublishSubject.create<Int>()
setPagesSubject(download.pages, pageStatusSubject)
return@flatMap pageStatusSubject
.filter { it == Page.READY }
.map { download }
} else if (download.status == Download.DOWNLOADED || download.status == Download.ERROR) {
setPagesSubject(download.pages, null)
}
Observable.just(download)
}
.filter { it.status == Download.DOWNLOADING }
}
private fun setPagesSubject(pages: List<Page>?, subject: PublishSubject<Int>?) {
if (pages != null) {
for (page in pages) {
page.setStatusSubject(subject)
}
}
}
}

View File

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

@ -0,0 +1,117 @@
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.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 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:
*
* - Check in RAM LRU.
* - Check in disk LRU.
* - Check in this module.
* - Fetch from the network connection.
*
* @param context the application context.
*/
class MangaModelLoader(context: Context) : StreamModelLoader<Manga> {
/**
* Cover cache where persistent covers are stored.
*/
val coverCache: CoverCache by injectLazy()
/**
* Source manager.
*/
val sourceManager: SourceManager by injectLazy()
/**
* Base network loader.
*/
private val baseLoader = Glide.buildModelLoader(GlideUrl::class.java,
InputStream::class.java, context)
/**
* 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)
/**
* Map where request headers are stored for a source.
*/
private val cachedHeaders = hashMapOf<Int, LazyHeaders>()
/**
* Factory class for creating [MangaModelLoader] instances.
*/
class Factory : ModelLoaderFactory<Manga, InputStream> {
override fun build(context: Context, factories: GenericLoaderFactory)
= MangaModelLoader(context)
override fun teardown() {}
}
/**
* Returns a [MangaDataFetcher] 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>? {
// Check thumbnail is not null or empty
val url = manga.thumbnail_url
if (url.isNullOrEmpty()) {
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)
}
// Get the network fetcher for this request url.
val networkFetcher = baseLoader.getResourceFetcher(glideUrl, width, height)
// Return an instance of our fetcher providing the needed elements.
return MangaDataFetcher(networkFetcher, file, manga)
}
/**
* Returns the request headers for a source copying its OkHttp headers and caching them.
*
* @param manga the model.
*/
fun getHeaders(manga: Manga): Headers {
val source = sourceManager.get(manga.source) as? OnlineSource ?: return LazyHeaders.DEFAULT
return cachedHeaders.getOrPut(manga.source) {
LazyHeaders.Builder().apply {
val nullStr: String? = null
setHeader("User-Agent", nullStr)
for ((key, value) in source.headers.toMultimap()) {
addHeader(key, value[0])
}
}.build()
}
}
}

View File

@ -1,110 +0,0 @@
package eu.kanade.tachiyomi.data.io;
import android.content.ContentResolver;
import android.content.Context;
import android.database.Cursor;
import android.net.Uri;
import android.os.Build;
import android.os.ParcelFileDescriptor;
import android.provider.DocumentsContract;
import android.provider.MediaStore;
import java.io.File;
import java.io.FileDescriptor;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class IOHandler {
/**
* Get full filepath of build in Android File picker.
* If Google Drive (or other Cloud service) throw exception and download before loading
*/
public static String getFilePath(Uri uri, ContentResolver resolver, Context context) {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
String filePath = "";
String wholeID = DocumentsContract.getDocumentId(uri);
//Ugly work around. In sdk version Kitkat or higher external getDocumentId request will have no content://
if (wholeID.split(":").length == 1)
throw new IllegalArgumentException();
// Split at colon, use second item in the array
String id = wholeID.split(":")[1];
String[] column = {MediaStore.Images.Media.DATA};
// where id is equal to
String sel = MediaStore.Images.Media._ID + "=?";
Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
column, sel, new String[]{id}, null);
int columnIndex = cursor != null ? cursor.getColumnIndex(column[0]) : 0;
if (cursor != null ? cursor.moveToFirst() : false) {
filePath = cursor.getString(columnIndex);
}
cursor.close();
return filePath;
} else {
String[] fields = {MediaStore.Images.Media.DATA};
Cursor cursor = resolver.query(uri, fields, null, null, null);
if (cursor == null)
return null;
cursor.moveToFirst();
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
cursor.close();
return path;
}
} catch (IllegalArgumentException e) {
//This exception is thrown when Google Drive. Try to download file
return downloadMediaAndReturnPath(uri, resolver, context);
}
}
private static String getTempFilename(Context context) throws IOException {
File outputDir = context.getCacheDir();
File outputFile = File.createTempFile("temp_cover", "0", outputDir);
return outputFile.getAbsolutePath();
}
private static String downloadMediaAndReturnPath(Uri uri, ContentResolver resolver, Context context) {
if (uri == null) return null;
FileInputStream input = null;
FileOutputStream output = null;
try {
ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r");
FileDescriptor fd = pfd != null ? pfd.getFileDescriptor() : null;
input = new FileInputStream(fd);
String tempFilename = getTempFilename(context);
output = new FileOutputStream(tempFilename);
int read;
byte[] bytes = new byte[4096];
while ((read = input.read(bytes)) != -1) {
output.write(bytes, 0, read);
}
return tempFilename;
} catch (IOException ignored) {
} finally {
if (input != null) try {
input.close();
} catch (Exception ignored) {
}
if (output != null) try {
output.close();
} catch (Exception ignored) {
}
}
return null;
}
}

View File

@ -0,0 +1,457 @@
package eu.kanade.tachiyomi.data.library
import android.app.PendingIntent
import android.app.Service
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.BitmapFactory
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.Manga
import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start
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.ui.main.MainActivity
import eu.kanade.tachiyomi.util.AndroidComponentUtil
import eu.kanade.tachiyomi.util.notification
import eu.kanade.tachiyomi.util.notificationManager
import eu.kanade.tachiyomi.util.syncChaptersWithSource
import rx.Observable
import rx.Subscription
import rx.schedulers.Schedulers
import uy.kohesive.injekt.injectLazy
import java.util.*
import java.util.concurrent.atomic.AtomicInteger
/**
* This class will take care of updating the chapters of the manga from the library. It can be
* started calling the [start] method. If it's already running, it won't do anything.
* While the library is updating, a [PowerManager.WakeLock] will be held until the update is
* completed, preventing the device from going to sleep mode. A notification will display the
* 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()
/**
* 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
/**
* Id of the library update notification.
*/
private val notificationId: Int
get() = Constants.NOTIFICATION_LIBRARY_ID
private var notificationBitmap: Bitmap? = null
companion object {
/**
* Key for category to update.
*/
const val UPDATE_CATEGORY = "category"
/**
* Key for updating the details instead of the chapters.
*/
const val UPDATE_DETAILS = "details"
/**
* Returns the status of the service.
*
* @param context the application context.
* @return true if the service is running, false otherwise.
*/
fun isRunning(context: Context): Boolean {
return AndroidComponentUtil.isServiceRunning(context, LibraryUpdateService::class.java)
}
/**
* Starts the service. It will be started only if there isn't another instance already
* running.
*
* @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.
*/
fun start(context: Context, category: Category? = null, details: Boolean = false) {
if (!isRunning(context)) {
val intent = Intent(context, LibraryUpdateService::class.java).apply {
putExtra(UPDATE_DETAILS, details)
category?.let { putExtra(UPDATE_CATEGORY, it.id) }
}
context.startService(intent)
}
}
/**
* Stops the service.
*
* @param context the application context.
*/
fun stop(context: Context) {
context.stopService(Intent(context, LibraryUpdateService::class.java))
}
}
/**
* Method called when the service is created. It injects dagger dependencies and acquire
* the wake lock.
*/
override fun onCreate() {
super.onCreate()
createAndAcquireWakeLock()
}
/**
* Method called when the service is destroyed. It destroys the running subscription, resets
* the alarm and release the wake lock.
*/
override fun onDestroy() {
subscription?.unsubscribe()
notificationBitmap?.recycle()
notificationBitmap = null
destroyWakeLock()
super.onDestroy()
}
/**
* This method needs to be implemented, but it's not used/needed.
*/
override fun onBind(intent: Intent): IBinder? {
return 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
// Unsubscribe from any previous subscription if needed.
subscription?.unsubscribe()
// Update favorite manga. Destroy service when completed or in case of an error.
subscription = Observable
.defer {
if (notificationBitmap == null) {
notificationBitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
}
val mangaList = getMangaToUpdate(intent)
// Update either chapter list or manga details.
if (!intent.getBooleanExtra(UPDATE_DETAILS, false))
updateChapterList(mangaList)
else
updateDetails(mangaList)
}
.subscribeOn(Schedulers.io())
.subscribe({
}, {
showNotification(getString(R.string.notification_update_error), "")
stopSelf(startId)
}, {
stopSelf(startId)
})
return Service.START_REDELIVER_INTENT
}
/**
* Returns the list of manga to be updated.
*
* @param intent the update intent.
* @return a list of manga to update
*/
fun getMangaToUpdate(intent: Intent): List<Manga> {
val categoryId = intent.getIntExtra(UPDATE_CATEGORY, -1)
var listToUpdate = if (categoryId != -1)
db.getLibraryMangas().executeAsBlocking().filter { it.category == categoryId }
else {
val categoriesToUpdate = preferences.libraryUpdateCategories().getOrDefault().map { it.toInt() }
if (categoriesToUpdate.isNotEmpty())
db.getLibraryMangas().executeAsBlocking()
.filter { it.category in categoriesToUpdate }
.distinctBy { it.id }
else
db.getFavoriteMangas().executeAsBlocking().distinctBy { it.id }
}
if (!intent.getBooleanExtra(UPDATE_DETAILS, false) && preferences.updateOnlyNonCompleted()) {
listToUpdate = listToUpdate.filter { it.status != Manga.COMPLETED }
}
return listToUpdate
}
/**
* Method that updates 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 updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
val newUpdates = ArrayList<Manga>()
val failedUpdates = ArrayList<Manga>()
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) }
// 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)
}
// Filter out mangas without new chapters (or failed).
.filter { pair -> pair.first > 0 }
// Convert to the manga that contains new chapters.
.map { manga }
}
// Add manga with new chapters to the list.
.doOnNext { newUpdates.add(it) }
// Notify result of the overall update.
.doOnCompleted {
if (newUpdates.isEmpty()) {
cancelNotification()
} else {
showResultNotification(newUpdates, failedUpdates)
}
LibraryUpdateTrigger.setupTask(this)
}
}
/**
* 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()
return source.fetchChapterList(manga)
.map { syncChaptersWithSource(db, it, manga, source) }
}
/**
* 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> {
// 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) }
// Update the details of the manga.
.concatMap { manga ->
val source = sourceManager.get(manga.source) as? OnlineSource
?: return@concatMap Observable.empty<Manga>()
source.fetchMangaDetails(manga)
.doOnNext { networkManga ->
manga.copyFrom(networkManga)
db.insertManga(manga).executeAsBlocking()
}
.onErrorReturn { manga }
}
.doOnCompleted {
cancelNotification()
}
}
/**
* 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.
*/
private fun getUpdatedMangasBody(updates: List<Manga>, failedUpdates: List<Manga>): String {
return with(StringBuilder()) {
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)
}
}
if (!failedUpdates.isEmpty()) {
append("\n\n")
append(getString(R.string.notification_manga_update_failed))
for (manga in failedUpdates) {
append("\n")
append(manga.title)
}
}
toString()
}
}
/**
* 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)
})
}
/**
* Shows the notification containing the currently updating manga and the progress.
*
* @param manga the manga that's being updated.
* @param current the current progress.
* @param total the total progress.
*/
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)
})
}
/**
* 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)
notificationManager.notify(notificationId, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setLargeIcon(notificationBitmap)
setContentTitle(title)
setStyle(NotificationCompat.BigTextStyle().bigText(body))
setContentIntent(notificationIntent)
setAutoCancel(true)
})
}
/**
* Cancels the notification.
*/
private fun cancelNotification() {
notificationManager.cancel(notificationId)
}
/**
* Property that 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)
}
}
}

View File

@ -0,0 +1,52 @@
package eu.kanade.tachiyomi.data.library
import android.content.Context
import com.google.android.gms.gcm.*
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 LibraryUpdateTrigger : GcmTaskService() {
override fun onInitializeTasks() {
setupTask(this)
}
override fun onRunTask(params: TaskParams): Int {
LibraryUpdateService.start(this)
return GcmNetworkManager.RESULT_SUCCESS
}
companion object {
fun setupTask(context: Context, prefInterval: Int? = null) {
val preferences = Injekt.get<PreferencesHelper>()
val interval = prefInterval ?: preferences.libraryUpdateInterval().getOrDefault()
if (interval > 0) {
val restrictions = preferences.libraryUpdateRestriction()
val acRestriction = "ac" in restrictions
val wifiRestriction = if ("wifi" in restrictions)
Task.NETWORK_STATE_UNMETERED
else
Task.NETWORK_STATE_ANY
val task = PeriodicTask.Builder()
.setService(LibraryUpdateTrigger::class.java)
.setTag("Library periodic update")
.setPeriod(interval * 60 * 60L)
.setFlex(5 * 60)
.setRequiredNetwork(wifiRestriction)
.setRequiresCharging(acRestriction)
.setUpdateCurrent(true)
.setPersisted(true)
.build()
GcmNetworkManager.getInstance(context).schedule(task)
}
}
fun cancelTask(context: Context) {
GcmNetworkManager.getInstance(context).cancelAllTasks(LibraryUpdateTrigger::class.java)
}
}
}

View File

@ -1,40 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync;
import android.content.Context;
import java.util.ArrayList;
import java.util.List;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
public class MangaSyncManager {
private List<MangaSyncService> services;
private MyAnimeList myAnimeList;
public static final int MYANIMELIST = 1;
public MangaSyncManager(Context context) {
services = new ArrayList<>();
myAnimeList = new MyAnimeList(context);
services.add(myAnimeList);
}
public MyAnimeList getMyAnimeList() {
return myAnimeList;
}
public List<MangaSyncService> getSyncServices() {
return services;
}
public MangaSyncService getSyncService(int id) {
switch (id) {
case MYANIMELIST:
return myAnimeList;
}
return null;
}
}

View File

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

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

@ -0,0 +1,74 @@
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)
}
}
}

View File

@ -0,0 +1,132 @@
package eu.kanade.tachiyomi.data.mangasync.anilist
import android.content.Context
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
import rx.Completable
import rx.Observable
import timber.log.Timber
class Anilist(private val context: Context, id: Int) : MangaSyncService(context, id) {
companion object {
const val READING = 1
const val COMPLETED = 2
const val ON_HOLD = 3
const val DROPPED = 4
const val PLAN_TO_READ = 5
const val DEFAULT_STATUS = READING
const val DEFAULT_SCORE = 0
}
override val name = "AniList"
private val interceptor by lazy { AnilistInterceptor(getPassword()) }
private val api by lazy {
AnilistApi.createService(networkService.client.newBuilder()
.addInterceptor(interceptor)
.build())
}
override fun login(username: String, password: String) = login(password)
fun login(authCode: String): Completable {
// Create a new api with the default client to avoid request interceptions.
return AnilistApi.createService(client)
// Request the access token from the API with the authorization code.
.requestAccessToken(authCode)
// Save the token in the interceptor.
.doOnNext { interceptor.setAuth(it) }
// Obtain the authenticated user from the API.
.zipWith(api.getCurrentUser().map { it["id"].toString() })
{ oauth, user -> Pair(user, oauth.refresh_token!!) }
// Save service credentials (username and refresh token).
.doOnNext { saveCredentials(it.first, it.second) }
// Logout on any error.
.doOnError { logout() }
.toCompletable()
}
override fun logout() {
super.logout()
interceptor.setAuth(null)
}
fun search(query: String): Observable<List<MangaSync>> {
return api.search(query, 1)
.flatMap { Observable.from(it) }
.filter { it.type != "Novel" }
.map { it.toMangaSync() }
.toList()
}
fun getList(): Observable<List<MangaSync>> {
return api.getList(getUsername())
.flatMap { Observable.from(it.flatten()) }
.map { it.toMangaSync() }
.toList()
}
override fun add(manga: MangaSync): Observable<MangaSync> {
return api.addManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
manga.score.toInt())
.doOnNext { it.body().close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
.doOnError { Timber.e(it, it.message) }
.map { manga }
}
override fun update(manga: MangaSync): Observable<MangaSync> {
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
manga.status = COMPLETED
}
return api.updateManga(manga.remote_id, manga.last_chapter_read, manga.getAnilistStatus(),
manga.score.toInt())
.doOnNext { it.body().close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
.doOnError { Timber.e(it, it.message) }
.map { manga }
}
override fun bind(manga: MangaSync): Observable<MangaSync> {
return getList()
.flatMap { userlist ->
manga.sync_id = id
val mangaFromList = userlist.find { it.remote_id == manga.remote_id }
if (mangaFromList != null) {
manga.copyPersonalFrom(mangaFromList)
update(manga)
} else {
// Set default fields if it's not found in the list
manga.score = DEFAULT_SCORE.toFloat()
manga.status = DEFAULT_STATUS
add(manga)
}
}
}
override fun getStatus(status: Int): String = with(context) {
when (status) {
READING -> getString(R.string.reading)
COMPLETED -> getString(R.string.completed)
ON_HOLD -> getString(R.string.on_hold)
DROPPED -> getString(R.string.dropped)
PLAN_TO_READ -> getString(R.string.plan_to_read)
else -> ""
}
}
private fun MangaSync.getAnilistStatus() = when (status) {
READING -> "reading"
COMPLETED -> "completed"
ON_HOLD -> "on-hold"
DROPPED -> "dropped"
PLAN_TO_READ -> "plan to read"
else -> throw NotImplementedError("Unknown status")
}
}

View File

@ -0,0 +1,89 @@
package eu.kanade.tachiyomi.data.mangasync.anilist
import android.net.Uri
import com.google.gson.JsonObject
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALManga
import eu.kanade.tachiyomi.data.mangasync.anilist.model.ALUserLists
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
import eu.kanade.tachiyomi.data.network.POST
import okhttp3.FormBody
import okhttp3.OkHttpClient
import okhttp3.ResponseBody
import retrofit2.Response
import retrofit2.Retrofit
import retrofit2.adapter.rxjava.RxJavaCallAdapterFactory
import retrofit2.converter.gson.GsonConverterFactory
import retrofit2.http.*
import rx.Observable
interface AnilistApi {
companion object {
private const val clientId = "tachiyomi-hrtje"
private const val clientSecret = "nlGB5OmgE9YWq5dr3gIDbTQV0C"
private const val clientUrl = "tachiyomi://anilist-auth"
private const val baseUrl = "https://anilist.co/api/"
fun authUrl() = Uri.parse("${baseUrl}auth/authorize").buildUpon()
.appendQueryParameter("grant_type", "authorization_code")
.appendQueryParameter("client_id", clientId)
.appendQueryParameter("redirect_uri", clientUrl)
.appendQueryParameter("response_type", "code")
.build()
fun refreshTokenRequest(token: String) = POST("${baseUrl}auth/access_token",
body = FormBody.Builder()
.add("grant_type", "refresh_token")
.add("client_id", clientId)
.add("client_secret", clientSecret)
.add("refresh_token", token)
.build())
fun createService(client: OkHttpClient) = Retrofit.Builder()
.baseUrl(baseUrl)
.client(client)
.addConverterFactory(GsonConverterFactory.create())
.addCallAdapterFactory(RxJavaCallAdapterFactory.create())
.build()
.create(AnilistApi::class.java)
}
@FormUrlEncoded
@POST("auth/access_token")
fun requestAccessToken(
@Field("code") code: String,
@Field("grant_type") grant_type: String = "authorization_code",
@Field("client_id") client_id: String = clientId,
@Field("client_secret") client_secret: String = clientSecret,
@Field("redirect_uri") redirect_uri: String = clientUrl)
: Observable<OAuth>
@GET("user")
fun getCurrentUser(): Observable<JsonObject>
@GET("manga/search/{query}")
fun search(@Path("query") query: String, @Query("page") page: Int): Observable<List<ALManga>>
@GET("user/{username}/mangalist")
fun getList(@Path("username") username: String): Observable<ALUserLists>
@FormUrlEncoded
@PUT("mangalist")
fun addManga(
@Field("id") id: Int,
@Field("chapters_read") chapters_read: Int,
@Field("list_status") list_status: String,
@Field("score_raw") score_raw: Int)
: Observable<Response<ResponseBody>>
@FormUrlEncoded
@PUT("mangalist")
fun updateManga(
@Field("id") id: Int,
@Field("chapters_read") chapters_read: Int,
@Field("list_status") list_status: String,
@Field("score_raw") score_raw: Int)
: Observable<Response<ResponseBody>>
}

View File

@ -0,0 +1,61 @@
package eu.kanade.tachiyomi.data.mangasync.anilist
import com.google.gson.Gson
import eu.kanade.tachiyomi.data.mangasync.anilist.model.OAuth
import okhttp3.Interceptor
import okhttp3.Response
class AnilistInterceptor(private var refreshToken: String?) : Interceptor {
/**
* OAuth object used for authenticated requests.
*
* Anilist returns the date without milliseconds. We fix that and make the token expire 1 minute
* before its original expiration date.
*/
private var oauth: OAuth? = null
set(value) {
field = value?.copy(expires = value.expires * 1000 - 60 * 1000)
}
override fun intercept(chain: Interceptor.Chain): Response {
val originalRequest = chain.request()
if (refreshToken.isNullOrEmpty()) {
throw Exception("Not authenticated with Anilist")
}
// Refresh access token if null or expired.
if (oauth == null || oauth!!.isExpired()) {
val response = chain.proceed(AnilistApi.refreshTokenRequest(refreshToken!!))
oauth = if (response.isSuccessful) {
Gson().fromJson(response.body().string(), OAuth::class.java)
} else {
response.close()
null
}
}
// Throw on null auth.
if (oauth == null) {
throw Exception("Access token wasn't refreshed")
}
// Add the authorization header to the original request.
val authRequest = originalRequest.newBuilder()
.addHeader("Authorization", "Bearer ${oauth!!.access_token}")
.build()
return chain.proceed(authRequest)
}
/**
* Called when the user authenticates with Anilist for the first time. Sets the refresh token
* and the oauth object.
*/
fun setAuth(oauth: OAuth?) {
refreshToken = oauth?.refresh_token
this.oauth = oauth
}
}

View File

@ -0,0 +1,17 @@
package eu.kanade.tachiyomi.data.mangasync.anilist.model
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
data class ALManga(
val id: Int,
val title_romaji: String,
val type: String,
val total_chapters: Int) {
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
remote_id = this@ALManga.id
title = title_romaji
total_chapters = this@ALManga.total_chapters
}
}

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