Compare commits

...

227 Commits

Author SHA1 Message Date
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
397 changed files with 17617 additions and 11780 deletions

View File

@ -1,19 +1,20 @@
# Bugs
* Include version (Setting > About > Version)
* 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 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).**
* 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
* [ ] Not done
```
* [x] 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

View File

@ -5,17 +5,20 @@ android:
- tools
# The BuildTools version used by your project
- build-tools-23.0.3
- android-23
- build-tools-24.0.2
- android-24
- extra-android-m2repository
- extra-google-m2repository
- extra-android-support
- extra-google-google_play_services
jdk:
- oraclejdk8
before_script:
- chmod +x gradlew
#Build, and run tests
script: "./gradlew clean assembleDebug testDebugUnitTest"
script: "./gradlew clean buildDebug"
sudo: false
before_cache:
@ -25,4 +28,4 @@ cache:
- $HOME/.gradle/caches/
- $HOME/.gradle/wrapper/
env:
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"
- GRADLE_OPTS="-XX:MaxPermSize=1024m -XX:+CMSClassUnloadingEnabled -XX:+HeapDumpOnOutOfMemoryError -Xmx2048m"

View File

@ -1,6 +1,6 @@
| Build | Download | Auto Update |
|-------|----------|-------------|
| [![TeamCity (simple build status)](https://img.shields.io/teamcity/https/teamcity.kanade.eu/s/tachiyomi_Build.svg)](https://teamcity.kanade.eu/project.html?projectId=tachiyomi) [![Travis](https://img.shields.io/travis/inorichi/tachiyomi.svg)](https://travis-ci.org/inorichi/tachiyomi) | [![stable release](https://img.shields.io/badge/stable-v0.2.1-blue.svg)](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-debug-versions) |
| [![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)

2
app/.gitignore vendored
View File

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

View File

@ -4,11 +4,15 @@ apply plugin: 'com.android.application'
apply plugin: 'kotlin-android'
apply plugin: 'kotlin-android-extensions'
if (file("custom.gradle").exists()) {
apply from: "custom.gradle"
}
ext {
// 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
getCommitCount = {
return 'git rev-list --count origin/master'.execute().text.trim()
return 'git rev-list --count HEAD'.execute().text.trim()
// return "1"
}
@ -29,17 +33,17 @@ def includeUpdater() {
}
android {
compileSdkVersion 23
buildToolsVersion "23.0.3"
compileSdkVersion 24
buildToolsVersion "24.0.2"
publishNonDefault true
defaultConfig {
applicationId "eu.kanade.tachiyomi"
minSdkVersion 16
targetSdkVersion 23
targetSdkVersion 24
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode project.getProperties().get("versionCode")?.toInteger() ?: 7
versionName "0.2.1"
versionCode 11
versionName "0.3.0"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -47,16 +51,22 @@ android {
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
vectorDrawables.useSupportLibrary = true
ndk {
abiFilters "armeabi", "armeabi-v7a", "x86"
}
}
buildTypes {
debug {
versionNameSuffix ".${getCommitCount()}"
versionNameSuffix "-${getCommitCount()}"
applicationIdSuffix ".debug"
multiDexEnabled true
}
release {
minifyEnabled true
shrinkResources true
multiDexEnabled false
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
}
}
@ -78,110 +88,112 @@ android {
main.java.srcDirs += 'src/main/kotlin'
}
// http://stackoverflow.com/questions/32759529/androidhttpclient-not-found-when-running-robolectric
useLibrary 'org.apache.http.legacy'
}
kapt {
generateStubs = true
}
dependencies {
final SUPPORT_LIBRARY_VERSION = '23.3.0'
final DAGGER_VERSION = '2.2'
final OKHTTP_VERSION = '3.2.0'
final RETROFIT_VERSION = '2.0.1'
final STORIO_VERSION = '1.8.0'
final MOCKITO_VERSION = '1.10.19'
// Modified dependencies
compile 'com.github.inorichi:subsampling-scale-image-view:421fb81'
compile 'com.github.inorichi:subsampling-scale-image-view:2d9c854'
compile 'com.github.inorichi:ReactiveNetwork:69092ed'
// Android support library
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:cardview-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:preference-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:preference-v14:$SUPPORT_LIBRARY_VERSION"
final support_library_version = '24.2.1'
compile "com.android.support:support-v4:$support_library_version"
compile "com.android.support:appcompat-v7:$support_library_version"
compile "com.android.support:cardview-v7:$support_library_version"
compile "com.android.support:design:$support_library_version"
compile "com.android.support:recyclerview-v7:$support_library_version"
compile "com.android.support:support-annotations:$support_library_version"
compile "com.android.support:customtabs:$support_library_version"
compile 'com.android.support:multidex:1.0.1'
compile 'com.google.android.gms:play-services-gcm:9.6.1'
// ReactiveX
compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.1'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
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:$OKHTTP_VERSION"
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
compile "com.squareup.okhttp3:okhttp:3.4.1"
// REST
compile "com.squareup.retrofit2:retrofit:$RETROFIT_VERSION"
compile "com.squareup.retrofit2:converter-gson:$RETROFIT_VERSION"
compile "com.squareup.retrofit2:adapter-rxjava:$RETROFIT_VERSION"
final retrofit_version = '2.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.7.0'
compile 'com.squareup.okio:okio:1.10.0'
// JSON
compile 'com.google.code.gson:gson:2.6.2'
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'
// Parse HTML
compile 'org.jsoup:jsoup:1.8.3'
compile 'org.jsoup:jsoup:1.9.2'
// Changelog
compile 'com.github.gabrielemariotti.changeloglib:changelog:2.1.0'
// Database
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
kapt "com.pushtorefresh.storio:sqlite-annotations-processor:$STORIO_VERSION"
compile "com.pushtorefresh.storio:sqlite:1.11.0"
// Model View Presenter
compile 'info.android15.nucleus:nucleus:3.0.0-beta'
final nucleus_version = '3.0.0'
compile "info.android15.nucleus:nucleus:$nucleus_version"
compile "info.android15.nucleus:nucleus-support-v4:$nucleus_version"
compile "info.android15.nucleus:nucleus-support-v7:$nucleus_version"
// Dependency injection
compile "com.google.dagger:dagger:$DAGGER_VERSION"
kapt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
provided 'org.glassfish:javax.annotation:10.0-b28'
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.1.2'
compile 'com.jakewharton.timber:timber:4.3.1'
// Crash reports
compile 'ch.acra:acra:4.8.5'
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 'eu.davidea:flexible-adapter:4.2.0'
compile 'com.nononsenseapps:filepicker:2.5.2'
compile 'com.github.amulyakhare:TextDrawable:558677e'
compile('com.github.afollestad.material-dialogs:core:0.8.5.5@aar') {
transitive = true
}
compile 'com.afollestad.material-dialogs:core:0.9.0.2'
compile 'net.xpece.android:support-preference:1.0.3'
compile 'me.zhanghai.android.systemuihelper:library:1.0.0'
compile 'de.hdodenhof:circleimageview:2.1.0'
// Tests
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:1.7.1'
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION"
testCompile('org.robolectric:robolectric:3.0') {
exclude group: 'commons-logging', module: 'commons-logging'
exclude group: 'org.apache.httpcomponents', module: 'httpclient'
}
testCompile 'org.mockito:mockito-core:1.10.19'
testCompile 'org.robolectric:robolectric:3.1.2'
testCompile 'org.robolectric:shadows-multidex:3.1.2'
testCompile 'org.robolectric:shadows-play-services:3.1.2'
kaptTest "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
compile "org.jetbrains.kotlin:kotlin-stdlib:$kotlin_version"
}
buildscript {
ext.kotlin_version = '1.0.1'
ext.kotlin_version = '1.0.4'
repositories {
mavenCentral()
}

View File

@ -1,7 +1,5 @@
-dontobfuscate
-keep class eu.kanade.tachiyomi.injection.** { *; }
# OkHttp
-keepattributes Signature
-keepattributes *Annotation*
@ -118,4 +116,12 @@
# Keep the support library
-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,6 +1,8 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="eu.kanade.tachiyomi">
<manifest
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.ACCESS_NETWORK_STATE" />
@ -8,6 +10,8 @@
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.WAKE_LOCK" />
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE" tools:node="remove" />
<uses-permission android:name="com.android.launcher.permission.INSTALL_SHORTCUT" />
<application
android:name=".App"
@ -18,8 +22,7 @@
android:largeHeap="true"
android:theme="@style/Theme.Tachiyomi" >
<activity
android:name=".ui.main.MainActivity"
android:theme="@style/Theme.BrandedLaunch">
android:name=".ui.main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
@ -28,7 +31,8 @@
</activity>
<activity
android:name=".ui.manga.MangaActivity"
android:parentActivityName=".ui.main.MainActivity" >
android:parentActivityName=".ui.main.MainActivity"
android:exported="true">
</activity>
<activity
android:name=".ui.reader.ReaderActivity"
@ -59,49 +63,35 @@
<service android:name=".data.mangasync.UpdateMangaSyncService"
android:exported="false"/>
<receiver
android:name=".data.library.LibraryUpdateService$SyncOnConnectionAvailable"
android:enabled="false">
<service
android:name=".data.library.LibraryUpdateTrigger"
android:exported="true"
android:permission="com.google.android.gms.permission.BIND_NETWORK_TASK_SERVICE">
<intent-filter>
<action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
</intent-filter>
</receiver>
</service>
<receiver
android:name=".data.library.LibraryUpdateService$SyncOnPowerConnected"
android:enabled="false">
<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="android.intent.action.ACTION_POWER_CONNECTED" />
<action android:name="com.google.android.gms.gcm.ACTION_TASK_READY" />
</intent-filter>
</receiver>
</service>
<service android:name=".data.updater.UpdateDownloaderService"
android:exported="false"/>
<receiver android:name=".data.updater.UpdateNotificationReceiver"/>
<receiver
android:name=".data.library.LibraryUpdateService$CancelUpdateReceiver">
</receiver>
<receiver
android:name=".data.updater.UpdateDownloader$InstallOnReceived">
</receiver>
<receiver
android:name=".data.library.LibraryUpdateAlarm">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="eu.kanade.UPDATE_LIBRARY" />
</intent-filter>
</receiver>
<receiver
android:name=".data.updater.UpdateDownloaderAlarm">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED"/>
<action android:name="eu.kanade.CHECK_UPDATE"/>
</intent-filter>
</receiver>
<meta-data
android:name="eu.kanade.tachiyomi.data.cache.CoverGlideModule"
android:name="eu.kanade.tachiyomi.data.glide.AppGlideModule"
android:value="GlideModule" />
</application>

View File

@ -2,14 +2,13 @@ package eu.kanade.tachiyomi
import android.app.Application
import android.content.Context
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
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 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",
@ -20,43 +19,25 @@ import timber.log.Timber
)
open class App : Application() {
lateinit var component: AppComponent
private set
lateinit var componentReflection: ComponentReflectionInjector<AppComponent>
private set
var appTheme = 0
override fun onCreate() {
super.onCreate()
Injekt = InjektScope(DefaultRegistrar())
Injekt.importModule(AppModule(this))
if (BuildConfig.DEBUG) Timber.plant(Timber.DebugTree())
component = prepareAppComponent().build()
componentReflection = ComponentReflectionInjector(AppComponent::class.java, component)
setupTheme()
setupAcra()
}
private fun setupTheme() {
appTheme = PreferencesHelper.getTheme(this)
}
protected open fun prepareAppComponent(): DaggerAppComponent.Builder {
return DaggerAppComponent.builder()
.appModule(AppModule(this))
override fun attachBaseContext(base: Context) {
super.attachBaseContext(base)
if (BuildConfig.DEBUG) {
MultiDex.install(this)
}
}
protected open fun setupAcra() {
ACRA.init(this)
}
companion object {
@JvmStatic
fun get(context: Context): App {
return context.applicationContext as App
}
}
}

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

@ -1,14 +1,13 @@
package eu.kanade.tachiyomi.data.backup
import com.github.salomonbrys.kotson.fromJson
import com.google.gson.*
import com.google.gson.reflect.TypeToken
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.lang.reflect.Type
import java.util.*
/**
@ -191,8 +190,7 @@ class BackupManager(private val db: DatabaseHelper) {
private fun restoreCategories(jsonCategories: JsonArray) {
// Get categories from file and from db
val dbCategories = db.getCategories().executeAsBlocking()
val backupCategories = getArrayOrEmpty<Category>(jsonCategories,
object : TypeToken<List<Category>>() {}.type)
val backupCategories = gson.fromJson<List<CategoryImpl>>(jsonCategories)
// Iterate over them
for (category in backupCategories) {
@ -224,17 +222,13 @@ class BackupManager(private val db: DatabaseHelper) {
* @param jsonMangas the mangas and its related data (chapters, sync, categories) from the json.
*/
private fun restoreMangas(jsonMangas: JsonArray) {
val chapterToken = object : TypeToken<List<Chapter>>() {}.type
val mangaSyncToken = object : TypeToken<List<MangaSync>>() {}.type
val categoriesNamesToken = object : TypeToken<List<String>>() {}.type
for (backupManga in jsonMangas) {
// Map every entry to objects
val element = backupManga.asJsonObject
val manga = gson.fromJson(element.get(MANGA), Manga::class.java)
val chapters = getArrayOrEmpty<Chapter>(element.get(CHAPTERS), chapterToken)
val sync = getArrayOrEmpty<MangaSync>(element.get(MANGA_SYNC), mangaSyncToken)
val categories = getArrayOrEmpty<String>(element.get(CATEGORIES), categoriesNamesToken)
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)
@ -340,7 +334,7 @@ class BackupManager(private val db: DatabaseHelper) {
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
mangaSync.manga_id = manga.id!!
}
val dbSyncs = db.getMangasSync(manga).executeAsBlocking()
@ -367,15 +361,4 @@ class BackupManager(private val db: DatabaseHelper) {
}
}
/**
* Returns a list of items from a json element, or an empty list if the element is null.
*
* @param element the json to be mapped to a list of items.
* @param type the gson mapping to restore the list.
* @return a list of items.
*/
private fun <T> getArrayOrEmpty(element: JsonElement?, type: Type): List<T> {
return gson.fromJson<List<T>>(element, type) ?: ArrayList<T>()
}
}

View File

@ -1,11 +1,6 @@
package eu.kanade.tachiyomi.data.cache
import android.content.Context
import com.bumptech.glide.Glide
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 eu.kanade.tachiyomi.util.DiskUtils
import java.io.File
import java.io.IOException
@ -27,80 +22,19 @@ class CoverCache(private val context: Context) {
*/
private val cacheDir: File = File(context.externalCacheDir, "cover_disk_cache")
/**
* Download the cover with Glide and save the file.
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
* @param onReady function to call when the image is ready
*/
fun save(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)? = null) {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
return
// Download the cover with Glide and save the file.
val url = GlideUrl(thumbnailUrl, headers)
Glide.with(context)
.load(url)
.downloadOnly(object : SimpleTarget<File>() {
override fun onResourceReady(resource: File, anim: GlideAnimation<in File>) {
try {
// Copy the cover from Glide's cache to local cache.
copyToCache(thumbnailUrl!!, resource)
onReady?.invoke(resource)
} catch (e: IOException) {
// Do nothing.
}
}
})
}
/**
* Save or load the image from cache
* @param thumbnailUrl the thumbnail url.
* @param headers headers included in Glide request.
* @param onReady function to call when the image is ready
*/
fun saveOrLoadFromCache(thumbnailUrl: String?, headers: LazyHeaders?, onReady: ((File) -> Unit)?) {
// Check if url is empty.
if (thumbnailUrl.isNullOrEmpty())
return
// If file exist load it otherwise save it.
val localCover = getCoverFromCache(thumbnailUrl!!)
if (localCover.exists()) {
onReady?.invoke(localCover)
} else {
save(thumbnailUrl, headers, onReady)
}
}
/**
* Returns the cover from cache.
*
* @param thumbnailUrl the thumbnail url.
* @return cover image.
*/
private fun getCoverFromCache(thumbnailUrl: String): File {
fun getCoverFile(thumbnailUrl: String): File {
return File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
}
/**
* Copy the given file to this cache.
* @param thumbnailUrl url of thumbnail.
* @param sourceFile the source file of the cover image.
* @throws IOException if there's any error.
*/
@Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, sourceFile: File) {
// Get destination file.
val destFile = getCoverFromCache(thumbnailUrl)
sourceFile.copyTo(destFile, overwrite = true)
}
/**
* 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.
@ -108,13 +42,14 @@ class CoverCache(private val context: Context) {
@Throws(IOException::class)
fun copyToCache(thumbnailUrl: String, inputStream: InputStream) {
// Get destination file.
val destFile = getCoverFromCache(thumbnailUrl)
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.
*/
@ -124,7 +59,7 @@ class CoverCache(private val context: Context) {
return false
// Remove file.
val file = File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl))
val file = getCoverFile(thumbnailUrl!!)
return file.exists() && file.delete()
}

View File

@ -1,22 +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.engine.cache.InternalCacheDiskCacheFactory
import com.bumptech.glide.module.GlideModule
/**
* Class used to update Glide module settings
*/
class CoverGlideModule : 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) {
// Nothing to see here!
}
}

View File

@ -1,315 +1,27 @@
package eu.kanade.tachiyomi.data.database
import android.content.Context
import android.util.Pair
import com.pushtorefresh.storio.Queries
import com.pushtorefresh.storio.sqlite.impl.DefaultStorIOSQLite
import com.pushtorefresh.storio.sqlite.operations.get.PreparedGetObject
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.mappers.*
import eu.kanade.tachiyomi.data.database.models.*
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver
import eu.kanade.tachiyomi.data.database.tables.*
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.util.ChapterRecognition
import rx.Observable
import java.util.*
import eu.kanade.tachiyomi.data.database.queries.*
open class DatabaseHelper(context: Context) {
/**
* This class provides operations to manage the database through its interfaces.
*/
open class DatabaseHelper(context: Context)
: MangaQueries, ChapterQueries, MangaSyncQueries, CategoryQueries, MangaCategoryQueries, HistoryQueries {
val db = DefaultStorIOSQLite.builder()
override val db = DefaultStorIOSQLite.builder()
.sqliteOpenHelper(DbOpenHelper(context))
.addTypeMapping(Manga::class.java, MangaSQLiteTypeMapping())
.addTypeMapping(Chapter::class.java, ChapterSQLiteTypeMapping())
.addTypeMapping(MangaSync::class.java, MangaSyncSQLiteTypeMapping())
.addTypeMapping(Category::class.java, CategorySQLiteTypeMapping())
.addTypeMapping(MangaCategory::class.java, MangaCategorySQLiteTypeMapping())
.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(func: DatabaseHelper.() -> Unit) {
db.internal().beginTransaction()
try {
func()
db.internal().setTransactionSuccessful()
} finally {
db.internal().endTransaction()
}
}
// Mangas related queries
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.COLUMN_FAVORITE} = ?")
.whereArgs(1)
.orderBy(MangaTable.COLUMN_TITLE)
.build())
.prepare()
fun getManga(url: String, sourceId: Int) = db.get()
.`object`(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COLUMN_URL} = ? AND ${MangaTable.COLUMN_SOURCE} = ?")
.whereArgs(url, sourceId)
.build())
.prepare()
fun getManga(id: Long) = db.get()
.`object`(Manga::class.java)
.withQuery(Query.builder()
.table(MangaTable.TABLE)
.where("${MangaTable.COLUMN_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 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.COLUMN_FAVORITE} = ?")
.whereArgs(0)
.build())
.prepare()
// Chapters related queries
fun getChapters(manga: Manga) = db.get()
.listOfObjects(Chapter::class.java)
.withQuery(Query.builder()
.table(ChapterTable.TABLE)
.where("${ChapterTable.COLUMN_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
fun getRecentChapters(date: Date) = db.get()
.listOfObjects(MangaChapter::class.java)
.withQuery(RawQuery.builder()
.query(getRecentsQuery(date))
.observesTables(ChapterTable.TABLE)
.build())
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare()
fun getNextChapter(chapter: Chapter): PreparedGetObject<Chapter> {
// Add a delta to the chapter number, because binary decimal representation
// can retrieve the same chapter again
val chapterNumber = chapter.chapter_number + 0.00001
return db.get()
.`object`(Chapter::class.java)
.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()
}
fun getPreviousChapter(chapter: Chapter): PreparedGetObject<Chapter> {
// Add a delta to the chapter number, because binary decimal representation
// can retrieve the same chapter again
val chapterNumber = chapter.chapter_number - 0.00001
return db.get()
.`object`(Chapter::class.java)
.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()
}
fun getNextUnreadChapter(manga: Manga) = db.get()
.`object`(Chapter::class.java)
.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()
fun insertChapter(chapter: Chapter) = db.put().`object`(chapter).prepare()
fun insertChapters(chapters: List<Chapter>) = db.put().objects(chapters).prepare()
// Add new chapters or delete if the source deletes them
open fun insertOrRemoveChapters(manga: Manga, sourceChapters: List<Chapter>, source: Source): Observable<Pair<Int, Int>> {
val dbChapters = getChapters(manga).executeAsBlocking()
val newChapters = Observable.from(sourceChapters)
.filter { it !in dbChapters }
.doOnNext { c ->
c.manga_id = manga.id
source.parseChapterNumber(c)
ChapterRecognition.parseChapterNumber(c, manga)
}.toList()
val deletedChapters = Observable.from(dbChapters)
.filter { it !in sourceChapters }
.toList()
return Observable.zip(newChapters, deletedChapters) { toAdd, toDelete ->
var added = 0
var deleted = 0
var readded = 0
inTransaction {
val deletedReadChapterNumbers = TreeSet<Float>()
if (!toDelete.isEmpty()) {
for (c in toDelete) {
if (c.read) {
deletedReadChapterNumbers.add(c.chapter_number)
}
}
deleted = deleteChapters(toDelete).executeAsBlocking().results().size
}
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.
var now = Date().time
for (i in toAdd.indices.reversed()) {
val c = toAdd[i]
c.date_fetch = now++
// Try to mark already read chapters as read when the source deletes them
if (c.chapter_number != -1f && c.chapter_number in deletedReadChapterNumbers) {
c.read = true
readded++
}
}
added = insertChapters(toAdd).executeAsBlocking().numberOfInserts()
}
}
Pair.create(added - readded, deleted - readded)
}
}
fun deleteChapter(chapter: Chapter) = db.delete().`object`(chapter).prepare()
fun deleteChapters(chapters: List<Chapter>) = db.delete().objects(chapters).prepare()
// Manga sync related queries
fun getMangaSync(manga: Manga, sync: MangaSyncService) = db.get()
.`object`(MangaSync::class.java)
.withQuery(Query.builder()
.table(MangaSyncTable.TABLE)
.where("${MangaSyncTable.COLUMN_MANGA_ID} = ? AND " +
"${MangaSyncTable.COLUMN_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.COLUMN_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.COLUMN_MANGA_ID} = ?")
.whereArgs(manga.id)
.build())
.prepare()
// Categories related queries
fun getCategories() = db.get()
.listOfObjects(Category::class.java)
.withQuery(Query.builder()
.table(CategoryTable.TABLE)
.orderBy(CategoryTable.COLUMN_ORDER)
.build())
.prepare()
fun getCategoriesForManga(manga: Manga) = db.get()
.listOfObjects(Category::class.java)
.withQuery(RawQuery.builder()
.query(getCategoriesForMangaQuery(manga))
.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()
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.COLUMN_MANGA_ID} IN (${Queries.placeholders(mangas.size)})")
.whereArgs(*mangas.map { it.id }.toTypedArray())
.build())
.prepare()
fun setMangaCategories(mangasCategories: List<MangaCategory>, mangas: List<Manga>) {
inTransaction {
deleteOldMangasCategories(mangas).executeAsBlocking()
insertMangasCategories(mangasCategories).executeAsBlocking()
}
}
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

@ -5,7 +5,8 @@ import android.database.sqlite.SQLiteDatabase
import android.database.sqlite.SQLiteOpenHelper
import eu.kanade.tachiyomi.data.database.tables.*
class DbOpenHelper(context: Context) : SQLiteOpenHelper(context, DbOpenHelper.DATABASE_NAME, null, DbOpenHelper.DATABASE_VERSION) {
class DbOpenHelper(context: Context)
: SQLiteOpenHelper(context, DATABASE_NAME, null, DATABASE_VERSION) {
companion object {
/**
@ -16,28 +17,41 @@ class DbOpenHelper(context: Context) : SQLiteOpenHelper(context, DbOpenHelper.DA
/**
* Version of the database.
*/
const val DATABASE_VERSION = 1
const val DATABASE_VERSION = 3
}
override fun onCreate(db: SQLiteDatabase) = with(db) {
execSQL(MangaTable.getCreateTableQuery())
execSQL(ChapterTable.getCreateTableQuery())
execSQL(MangaSyncTable.getCreateTableQuery())
execSQL(CategoryTable.getCreateTableQuery())
execSQL(MangaCategoryTable.getCreateTableQuery())
execSQL(MangaTable.createTableQuery)
execSQL(ChapterTable.createTableQuery)
execSQL(MangaSyncTable.createTableQuery)
execSQL(CategoryTable.createTableQuery)
execSQL(MangaCategoryTable.createTableQuery)
execSQL(HistoryTable.createTableQuery)
// DB indexes
execSQL(MangaTable.getCreateUrlIndexQuery())
execSQL(MangaTable.getCreateFavoriteIndexQuery())
execSQL(ChapterTable.getCreateMangaIdIndexQuery())
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

@ -1,54 +0,0 @@
package eu.kanade.tachiyomi.data.database
import java.util.*
import eu.kanade.tachiyomi.data.database.models.Manga as MangaModel
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.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.COLUMN_CATEGORY_ID}, 0) AS ${Manga.COLUMN_CATEGORY} " +
"FROM (" +
"SELECT ${Manga.TABLE}.*, COALESCE(C.unread, 0) AS ${Manga.COLUMN_UNREAD} " +
"FROM ${Manga.TABLE} " +
"LEFT JOIN (" +
"SELECT ${Chapter.COLUMN_MANGA_ID}, COUNT(*) AS unread " +
"FROM ${Chapter.TABLE} " +
"WHERE ${Chapter.COLUMN_READ} = 0 " +
"GROUP BY ${Chapter.COLUMN_MANGA_ID}" +
") AS C " +
"ON ${Manga.COLUMN_ID} = C.${Chapter.COLUMN_MANGA_ID} " +
"WHERE ${Manga.COLUMN_FAVORITE} = 1 " +
"GROUP BY ${Manga.COLUMN_ID} " +
"ORDER BY ${Manga.COLUMN_TITLE}" +
") AS M " +
"LEFT JOIN (" +
"SELECT * FROM ${MangaCategory.TABLE}) AS MC " +
"ON MC.${MangaCategory.COLUMN_MANGA_ID} = M.${Manga.COLUMN_ID}"
/**
* Query to get the recent chapters of manga from the library up to a date.
*
* @param date the delimiting date.
*/
fun getRecentsQuery(date: Date): String =
"SELECT ${Manga.TABLE}.${Manga.COLUMN_URL} as mangaUrl, * FROM ${Manga.TABLE} JOIN ${Chapter.TABLE} " +
"ON ${Manga.TABLE}.${Manga.COLUMN_ID} = ${Chapter.TABLE}.${Chapter.COLUMN_MANGA_ID} " +
"WHERE ${Manga.COLUMN_FAVORITE} = 1 AND ${Chapter.COLUMN_DATE_UPLOAD} > ${date.time} " +
"ORDER BY ${Chapter.COLUMN_DATE_UPLOAD} DESC"
/**
* Query to get the categorias for a manga.
*
* @param manga the manga.
*/
fun getCategoriesForMangaQuery(manga: MangaModel) =
"SELECT ${Category.TABLE}.* FROM ${Category.TABLE} " +
"JOIN ${MangaCategory.TABLE} ON ${Category.TABLE}.${Category.COLUMN_ID} = " +
"${MangaCategory.TABLE}.${MangaCategory.COLUMN_CATEGORY_ID} " +
"WHERE ${MangaCategory.COLUMN_MANGA_ID} = ${manga.id}"

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,57 +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;
}
public String getNameLower() {
return name.toLowerCase();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Category category = (Category) o;
return name.equals(category.name);
}
@Override
public int hashCode() {
return name.hashCode();
}
}

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,87 +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 java.util.List;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.data.source.model.Page;
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;
private transient List<Page> pages;
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;
}
public List<Page> getPages() {
return pages;
}
public void setPages(List<Page> pages) {
this.pages = pages;
}
public boolean isDownloaded() {
return status == Download.DOWNLOADED;
}
}

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,201 +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 transient int unread;
public transient 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 static Manga create(String pathUrl, int source) {
Manga m = new Manga();
m.url = pathUrl;
m.source = source;
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,78 +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() {
return new MangaSync();
}
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;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
MangaSync mangaSync = (MangaSync) o;
if (manga_id != mangaSync.manga_id) return false;
if (sync_id != mangaSync.sync_id) return false;
return remote_id == mangaSync.remote_id;
}
@Override
public int hashCode() {
int result = (int) (manga_id ^ (manga_id >>> 32));
result = 31 * result + sync_id;
result = 31 * result + remote_id;
return result;
}
}

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,12 +1,11 @@
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.models.MangaStorIOSQLiteGetResolver
import eu.kanade.tachiyomi.data.database.tables.MangaTable
class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() {
class LibraryMangaGetResolver : MangaGetResolver() {
companion object {
val INSTANCE = LibraryMangaGetResolver()
@ -15,10 +14,10 @@ class LibraryMangaGetResolver : MangaStorIOSQLiteGetResolver() {
override fun mapFromCursor(cursor: Cursor): Manga {
val manga = super.mapFromCursor(cursor)
val unreadColumn = cursor.getColumnIndex(MangaTable.COLUMN_UNREAD)
val unreadColumn = cursor.getColumnIndex(MangaTable.COL_UNREAD)
manga.unread = cursor.getInt(unreadColumn)
val categoryColumn = cursor.getColumnIndex(MangaTable.COLUMN_CATEGORY)
val categoryColumn = cursor.getColumnIndex(MangaTable.COL_CATEGORY)
manga.category = cursor.getInt(categoryColumn)
return manga

View File

@ -2,9 +2,9 @@ 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.models.ChapterStorIOSQLiteGetResolver
import eu.kanade.tachiyomi.data.database.mappers.ChapterGetResolver
import eu.kanade.tachiyomi.data.database.mappers.MangaGetResolver
import eu.kanade.tachiyomi.data.database.models.MangaChapter
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver
class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
@ -12,15 +12,15 @@ class MangaChapterGetResolver : DefaultGetResolver<MangaChapter>() {
val INSTANCE = MangaChapterGetResolver()
}
private val mangaGetResolver = MangaStorIOSQLiteGetResolver()
private val mangaGetResolver = MangaGetResolver()
private val chapterGetResolver = ChapterStorIOSQLiteGetResolver()
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"));
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

@ -5,16 +5,17 @@ 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.base.Source
import eu.kanade.tachiyomi.data.source.model.Page
import eu.kanade.tachiyomi.event.DownloadChaptersEvent
import eu.kanade.tachiyomi.data.source.online.OnlineSource
import eu.kanade.tachiyomi.util.*
import rx.Observable
import rx.Subscription
@ -23,12 +24,17 @@ 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.*
import java.util.concurrent.TimeUnit
class DownloadManager(private val context: Context, private val sourceManager: SourceManager, private val preferences: PreferencesHelper) {
class DownloadManager(
private val context: Context,
private val sourceManager: SourceManager = Injekt.get(),
private val preferences: PreferencesHelper = Injekt.get()
) {
private val gson = Gson()
@ -36,6 +42,8 @@ class DownloadManager(private val context: Context, private val sourceManager: S
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
@ -45,27 +53,37 @@ class DownloadManager(private val context: Context, private val sourceManager: S
val PAGE_LIST_FILE = "index.json"
@Volatile private var isRunning: Boolean = false
@Volatile var isRunning: Boolean = false
private set
private fun initializeSubscriptions() {
downloadsSubscription?.unsubscribe()
threadsSubscription = preferences.downloadThreads().asObservable()
.subscribe { threadsSubject.onNext(it) }
.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())
.map { download -> areAllDownloadsFinished() }
.subscribe({ finished ->
if (finished!!) {
.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)
}
}, { e ->
}, { error ->
DownloadService.stop(context)
Timber.e(e, e.message)
context.toast(e.message)
Timber.e(error)
downloadNotifier.onError(error.message)
})
if (!isRunning) {
@ -91,16 +109,18 @@ class DownloadManager(private val context: Context, private val sourceManager: S
}
// Create a download object for every chapter in the event and add them to the downloads queue
fun onDownloadChaptersEvent(event: DownloadChaptersEvent) {
val manga = event.manga
val source = sourceManager.get(manga.source)
// 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 event.chapters) {
for (chapter in sortedChapters) {
if (addedChapters.contains(chapter.name))
continue
@ -112,6 +132,12 @@ class DownloadManager(private val context: Context, private val sourceManager: S
pending.add(download)
}
}
// Initialize queue size
downloadNotifier.initialQueueSize = queue.size
// Show notification
downloadNotifier.onProgressChange(queue)
if (isRunning) downloadsQueueSubject.onNext(pending)
}
@ -161,35 +187,41 @@ class DownloadManager(private val context: Context, private val sourceManager: S
private fun downloadChapter(download: Download): Observable<Download> {
DiskUtils.createDirectory(download.directory)
val pageListObservable = if (download.pages == null)
val pageListObservable: Observable<List<Page>> = if (download.pages == null)
// Pull page list from network and add them to download object
download.source.pullPageListFromNetwork(download.chapter.url)
download.source.fetchPageListFromNetwork(download.chapter)
.doOnNext { pages ->
download.pages = pages
savePageList(download)
}
else
// Or if the page list already exists, start from the file
// 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.getAllImageUrlsFromPageList(it) }
// 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.status = Download.ERROR
Observable.just(download)
}
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())
}
@ -225,24 +257,23 @@ class DownloadManager(private val context: Context, private val sourceManager: S
}
// Save image on disk
private fun downloadImage(page: Page, source: Source, directory: File, filename: String): Observable<Page> {
private fun downloadImage(page: Page, source: OnlineSource, directory: File, filename: String): Observable<Page> {
page.status = Page.DOWNLOAD_IMAGE
return source.getImageProgressResponse(page)
.flatMap {
return source.imageResponse(page)
.map {
val file = File(directory, filename)
try {
val file = File(directory, filename)
file.parentFile.mkdirs()
it.body().source().saveImageTo(file.outputStream(), preferences.reencodeImage())
} catch (e: Exception) {
it.body().close()
it.close()
file.delete()
throw e
}
Observable.just(page)
}
.retryWhen {
it.zipWith(Observable.range(1, 3)) { errors, retries -> retries }
.flatMap { retries -> Observable.timer((retries * 2).toLong(), TimeUnit.SECONDS) }
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
@ -293,20 +324,20 @@ class DownloadManager(private val context: Context, private val sourceManager: S
var actualProgress = 0
var status = Download.DOWNLOADED
// If any page has an error, the download result will be error
for (page in download.pages) {
for (page in download.pages!!) {
actualProgress += page.progress
if (page.status != Page.READY) status = Download.ERROR
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
// Delete successful downloads from queue after notifying
if (status == Download.DOWNLOADED) {
queue.del(download)
}
}
// Return the page list from the chapter's directory if it exists, null otherwise
@ -338,19 +369,19 @@ class DownloadManager(private val context: Context, private val sourceManager: S
try {
it.write(gson.toJson(pages).toByteArray())
it.flush()
} catch (e: Exception) {
Timber.e(e, e.message)
} 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)
savePageList(download.source, download.manga, download.chapter, download.pages!!)
}
fun getAbsoluteMangaDirectory(source: Source, manga: Manga): File {
val mangaRelativePath = source.visibleName +
val mangaRelativePath = source.toString() +
File.separator +
manga.title.replace("[^\\sa-zA-Z0-9.-]".toRegex(), "_")
@ -401,13 +432,19 @@ class DownloadManager(private val context: Context, private val sourceManager: S
return !pending.isEmpty()
}
fun stopDownloads() {
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

@ -7,14 +7,13 @@ 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.App
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 javax.inject.Inject
import uy.kohesive.injekt.injectLazy
class DownloadService : Service() {
@ -29,8 +28,8 @@ class DownloadService : Service() {
}
}
@Inject lateinit var downloadManager: DownloadManager
@Inject lateinit var preferences: PreferencesHelper
val downloadManager: DownloadManager by injectLazy()
val preferences: PreferencesHelper by injectLazy()
private var wakeLock: PowerManager.WakeLock? = null
private var networkChangeSubscription: Subscription? = null
@ -39,7 +38,6 @@ class DownloadService : Service() {
override fun onCreate() {
super.onCreate()
App.get(this).component.inject(this)
createWakeLock()
@ -82,12 +80,12 @@ class DownloadService : Service() {
stopSelf()
}
} else if (isRunning) {
downloadManager.stopDownloads()
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
}
}
else -> {
if (isRunning) {
downloadManager.stopDownloads()
downloadManager.stopDownloads(getString(R.string.download_notifier_text_only_wifi))
}
}
}

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

@ -6,34 +6,41 @@ import rx.Observable
import rx.subjects.PublishSubject
import java.util.concurrent.CopyOnWriteArrayList
class DownloadQueue : CopyOnWriteArrayList<Download>() {
class DownloadQueue(private val queue: MutableList<Download> = CopyOnWriteArrayList<Download>())
: List<Download> by queue {
private val statusSubject = PublishSubject.create<Download>()
override fun add(download: Download): Boolean {
private val removeSubject = PublishSubject.create<Download>()
fun add(download: Download): Boolean {
download.setStatusSubject(statusSubject)
download.status = Download.QUEUE
return super.add(download)
return queue.add(download)
}
fun del(download: Download) {
super.remove(download)
val removed = queue.remove(download)
download.setStatusSubject(null)
}
fun del(chapter: Chapter) {
for (download in this) {
if (download.chapter.id == chapter.id) {
del(download)
break
}
if (removed) {
removeSubject.onNext(download)
}
}
fun getActiveDownloads() =
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() = statusSubject.onBackpressureBuffer()
fun getStatusObservable(): Observable<Download> = statusSubject.onBackpressureBuffer()
fun getRemovedObservable(): Observable<Download> = removeSubject.onBackpressureBuffer()
fun getProgressObservable(): Observable<Download> {
return statusSubject.onBackpressureBuffer()

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,82 +0,0 @@
package eu.kanade.tachiyomi.data.library
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.os.SystemClock
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.util.alarmManager
/**
* This class is used to update the library by firing an alarm after a specified time.
* It has a receiver reacting to system's boot and the intent fired by this alarm.
* See [onReceive] for more information.
*/
class LibraryUpdateAlarm : BroadcastReceiver() {
companion object {
const val LIBRARY_UPDATE_ACTION = "eu.kanade.UPDATE_LIBRARY"
/**
* Sets the alarm to run the intent that updates the library.
* @param context the application context.
* @param intervalInHours the time in hours when it will be executed. Defaults to the
* value stored in preferences.
*/
@JvmStatic
@JvmOverloads
fun startAlarm(context: Context,
intervalInHours: Int = PreferencesHelper.getLibraryUpdateInterval(context)) {
// Stop previous running alarms if needed, and do not restart it if the interval is 0.
stopAlarm(context)
if (intervalInHours == 0)
return
// Get the time the alarm should fire the event to update.
val intervalInMillis = intervalInHours * 60 * 60 * 1000
val nextRun = SystemClock.elapsedRealtime() + intervalInMillis
// Start the alarm.
val pendingIntent = getPendingIntent(context)
context.alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME_WAKEUP,
nextRun, intervalInMillis.toLong(), pendingIntent)
}
/**
* Stops the alarm if it's running.
* @param context the application context.
*/
fun stopAlarm(context: Context) {
val pendingIntent = getPendingIntent(context)
context.alarmManager.cancel(pendingIntent)
}
/**
* Get the intent the alarm should run when it's fired.
* @param context the application context.
* @return the intent that will run when the alarm is fired.
*/
private fun getPendingIntent(context: Context): PendingIntent {
val intent = Intent(context, LibraryUpdateAlarm::class.java)
intent.action = LIBRARY_UPDATE_ACTION
return PendingIntent.getBroadcast(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
}
/**
* Handle the intents received by this [BroadcastReceiver].
* @param context the application context.
* @param intent the intent to process.
*/
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
// Start the alarm when the system is booted.
Intent.ACTION_BOOT_COMPLETED -> startAlarm(context)
// Update the library when the alarm fires an event.
LIBRARY_UPDATE_ACTION -> LibraryUpdateService.start(context)
}
}
}

View File

@ -5,26 +5,32 @@ 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 android.util.Pair
import com.github.pwittchen.reactivenetwork.library.ConnectivityStatus
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork
import eu.kanade.tachiyomi.App
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.*
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
import javax.inject.Inject
/**
* This class will take care of updating the chapters of the manga from the library. It can be
@ -36,38 +42,54 @@ import javax.inject.Inject
*/
class LibraryUpdateService : Service() {
// Dependencies injected through dagger.
@Inject lateinit var db: DatabaseHelper
@Inject lateinit var sourceManager: SourceManager
@Inject lateinit var preferences: PreferencesHelper
/**
* Database helper.
*/
val db: DatabaseHelper by injectLazy()
// Wake lock that will be held until the service is destroyed.
/**
* 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.
/**
* 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 {
val UPDATE_NOTIFICATION_ID = 1
// Intent key for manual library update
val UPDATE_IS_MANUAL = "is_manual"
/**
* Get the start intent for [LibraryUpdateService].
* @param context the application context.
* @param isManual true when user triggers library update.
* @return the intent of the service.
* Key for category to update.
*/
fun getIntent(context: Context, isManual: Boolean = false): Intent {
return Intent(context, LibraryUpdateService::class.java).apply {
putExtra(UPDATE_IS_MANUAL, isManual)
}
}
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.
*/
@ -76,19 +98,30 @@ class LibraryUpdateService : Service() {
}
/**
* Static method to start the service. It will be started only if there isn't another
* instance already running.
* 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.
*/
@JvmStatic
fun start(context: Context, isForced: Boolean = false) {
fun start(context: Context, category: Category? = null, details: Boolean = false) {
if (!isRunning(context)) {
context.startService(getIntent(context, isForced))
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(getIntent(context))
context.stopService(Intent(context, LibraryUpdateService::class.java))
}
}
@ -99,17 +132,17 @@ class LibraryUpdateService : Service() {
*/
override fun onCreate() {
super.onCreate()
App.get(this).component.inject(this)
createAndAcquireWakeLock()
}
/**
* Method called when the service is destroyed. It destroy the running subscription, resets
* 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()
LibraryUpdateAlarm.startAlarm(this)
notificationBitmap?.recycle()
notificationBitmap = null
destroyWakeLock()
super.onDestroy()
}
@ -121,80 +154,85 @@ class LibraryUpdateService : Service() {
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 {
// Get connectivity status
val connection = ReactiveNetwork().getConnectivityStatus(this, true)
// Get library update restrictions
val restrictions = preferences.libraryUpdateRestriction()
// Check if users updates library manual
val isManualUpdate = intent?.getBooleanExtra(UPDATE_IS_MANUAL, false) ?: false
// Whether to cancel the update.
var cancelUpdate = false
// Check if device has internet connection
// Check if device has wifi connection if only wifi is enabled
if (connection == ConnectivityStatus.OFFLINE || ("wifi" in restrictions
&& connection != ConnectivityStatus.WIFI_CONNECTED_HAS_INTERNET)) {
if (isManualUpdate) {
toast(R.string.notification_no_connection_title)
}
// Enable library update when connection available
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, true)
cancelUpdate = true
}
if (!isManualUpdate && "ac" in restrictions && !DeviceUtil.isPowerConnected(this)) {
AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, true)
cancelUpdate = true
}
if (cancelUpdate) {
stopSelf(startId)
return Service.START_NOT_STICKY
}
// Stop enabled components.
AndroidComponentUtil.toggleComponent(this, SyncOnConnectionAvailable::class.java, false)
AndroidComponentUtil.toggleComponent(this, SyncOnPowerConnected::class.java, false)
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 { updateLibrary() }
.subscribeOn(Schedulers.io())
.subscribe({},
{
showNotification(getString(R.string.notification_update_error), "")
stopSelf(startId)
}, {
stopSelf(startId)
})
subscription = Observable
.defer {
if (notificationBitmap == null) {
notificationBitmap = BitmapFactory.decodeResource(resources, R.mipmap.ic_launcher)
}
return Service.START_STICKY
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
}
/**
* Method that updates the library. It's called in a background thread, so it's safe to do
* heavy operations or network calls here.
* 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 updateLibrary(): Observable<Manga> {
fun updateChapterList(mangaToUpdate: List<Manga>): Observable<Manga> {
// Initialize the variables holding the progress of the updates.
val count = AtomicInteger(0)
val newUpdates = ArrayList<Manga>()
@ -203,17 +241,10 @@ class LibraryUpdateService : Service() {
val cancelIntent = PendingIntent.getBroadcast(this, 0,
Intent(this, CancelUpdateReceiver::class.java), 0)
// Get the manga list that is going to be updated.
val allLibraryMangas = db.getFavoriteMangas().executeAsBlocking()
val toUpdate = if (!preferences.updateOnlyNonCompleted())
allLibraryMangas
else
allLibraryMangas.filter { it.status != Manga.COMPLETED }
// Emit each manga and update it sequentially.
return Observable.from(toUpdate)
return Observable.from(mangaToUpdate)
// Notify manga that will update.
.doOnNext { showProgressNotification(it, count.andIncrement, toUpdate.size, cancelIntent) }
.doOnNext { showProgressNotification(it, count.andIncrement, mangaToUpdate.size, cancelIntent) }
// Update the chapters of the manga.
.concatMap { manga ->
updateManga(manga)
@ -236,23 +267,62 @@ class LibraryUpdateService : Service() {
} 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)
return source!!
.pullChaptersFromNetwork(manga.url)
.flatMap { db.insertOrRemoveChapters(manga, it, source) }
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.
@ -301,12 +371,14 @@ class LibraryUpdateService : Service() {
/**
* 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(UPDATE_NOTIFICATION_ID, notification() {
notificationManager.notify(notificationId, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setLargeIcon(notificationBitmap)
setContentTitle(title)
setContentText(body)
})
@ -314,13 +386,15 @@ class LibraryUpdateService : Service() {
/**
* 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(UPDATE_NOTIFICATION_ID, notification() {
notificationManager.notify(notificationId, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setLargeIcon(notificationBitmap)
setContentTitle(manga.title)
setProgress(total, current, false)
setOngoing(true)
@ -331,6 +405,7 @@ class LibraryUpdateService : Service() {
/**
* 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.
*/
@ -338,8 +413,9 @@ class LibraryUpdateService : Service() {
val title = getString(R.string.notification_update_completed)
val body = getUpdatedMangasBody(updates, failed)
notificationManager.notify(UPDATE_NOTIFICATION_ID, notification() {
notificationManager.notify(notificationId, notification() {
setSmallIcon(R.drawable.ic_refresh_white_24dp_img)
setLargeIcon(notificationBitmap)
setContentTitle(title)
setStyle(NotificationCompat.BigTextStyle().bigText(body))
setContentIntent(notificationIntent)
@ -351,7 +427,7 @@ class LibraryUpdateService : Service() {
* Cancels the notification.
*/
private fun cancelNotification() {
notificationManager.cancel(UPDATE_NOTIFICATION_ID)
notificationManager.cancel(notificationId)
}
/**
@ -364,39 +440,6 @@ class LibraryUpdateService : Service() {
return PendingIntent.getActivity(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
}
/**
* Class that triggers the library to update when a connection is available. It receives
* network changes.
*/
class SyncOnConnectionAvailable : BroadcastReceiver() {
/**
* Method called when a network change occurs.
* @param context the application context.
* @param intent the intent received.
*/
override fun onReceive(context: Context, intent: Intent) {
if (DeviceUtil.isNetworkConnected(context)) {
AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
start(context)
}
}
}
/**
* Class that triggers the library to update when connected to power.
*/
class SyncOnPowerConnected: BroadcastReceiver() {
/**
* Method called when AC is connected.
* @param context the application context.
* @param intent the intent received.
*/
override fun onReceive(context: Context, intent: Intent) {
AndroidComponentUtil.toggleComponent(context, this.javaClass, false)
start(context)
}
}
/**
* Class that stops updating the library.
*/
@ -408,8 +451,7 @@ class LibraryUpdateService : Service() {
*/
override fun onReceive(context: Context, intent: Intent) {
LibraryUpdateService.stop(context)
context.notificationManager.cancel(UPDATE_NOTIFICATION_ID)
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,23 +1,23 @@
package eu.kanade.tachiyomi.data.mangasync
import android.content.Context
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
import eu.kanade.tachiyomi.data.mangasync.myanimelist.MyAnimeList
class MangaSyncManager(private val context: Context) {
val services: List<MangaSyncService>
val myAnimeList: MyAnimeList
companion object {
const val MYANIMELIST = 1
const val ANILIST = 2
}
init {
myAnimeList = MyAnimeList(context, MYANIMELIST)
services = listOf(myAnimeList)
}
val myAnimeList = MyAnimeList(context, MYANIMELIST)
fun getService(id: Int): MangaSyncService = services.find { it.id == id }!!
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

@ -4,25 +4,23 @@ import android.app.Service
import android.content.Context
import android.content.Intent
import android.os.IBinder
import eu.kanade.tachiyomi.App
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 javax.inject.Inject
import uy.kohesive.injekt.injectLazy
class UpdateMangaSyncService : Service() {
@Inject lateinit var syncManager: MangaSyncManager
@Inject lateinit var db: DatabaseHelper
val syncManager: MangaSyncManager by injectLazy()
val db: DatabaseHelper by injectLazy()
private lateinit var subscriptions: CompositeSubscription
override fun onCreate() {
super.onCreate()
App.get(this).component.inject(this)
subscriptions = CompositeSubscription()
}
@ -48,15 +46,13 @@ class UpdateMangaSyncService : Service() {
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 {
if (it.isSuccessful) {
db.insertMangaSync(mangaSync).asRxObservable()
} else {
Observable.error(Exception("Could not update manga in remote service"))
}
}
.flatMap { db.insertMangaSync(mangaSync).asRxObservable() }
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe({ stopSelf(startId) },

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
}
}

View File

@ -0,0 +1,6 @@
package eu.kanade.tachiyomi.data.mangasync.anilist.model
data class ALUserLists(val lists: Map<String, List<ALUserManga>>) {
fun flatten() = lists.values.flatten()
}

View File

@ -0,0 +1,29 @@
package eu.kanade.tachiyomi.data.mangasync.anilist.model
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager
import eu.kanade.tachiyomi.data.mangasync.anilist.Anilist
data class ALUserManga(
val id: Int,
val list_status: String,
val score_raw: Int,
val chapters_read: Int,
val manga: ALManga) {
fun toMangaSync() = MangaSync.create(MangaSyncManager.ANILIST).apply {
remote_id = manga.id
status = getMangaSyncStatus()
score = score_raw.toFloat()
last_chapter_read = chapters_read
}
fun getMangaSyncStatus() = when (list_status) {
"reading" -> Anilist.READING
"completed" -> Anilist.COMPLETED
"on-hold" -> Anilist.ON_HOLD
"dropped" -> Anilist.DROPPED
"plan to read" -> Anilist.PLAN_TO_READ
else -> throw NotImplementedError("Unknown status")
}
}

View File

@ -0,0 +1,11 @@
package eu.kanade.tachiyomi.data.mangasync.anilist.model
data class OAuth(
val access_token: String,
val token_type: String,
val expires: Long,
val expires_in: Long,
val refresh_token: String?) {
fun isExpired() = System.currentTimeMillis() > expires
}

View File

@ -1,38 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync.base
import android.content.Context
import eu.kanade.tachiyomi.App
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.network.NetworkHelper
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import okhttp3.Response
import rx.Observable
import javax.inject.Inject
abstract class MangaSyncService(private val context: Context, val id: Int) {
@Inject lateinit var preferences: PreferencesHelper
@Inject lateinit var networkService: NetworkHelper
init {
App.get(context).component.inject(this)
}
// Name of the manga sync service to display
abstract val name: String
abstract fun login(username: String, password: String): Observable<Boolean>
open val isLogged: Boolean
get() = !preferences.mangaSyncUsername(this).isEmpty() &&
!preferences.mangaSyncPassword(this).isEmpty()
abstract fun update(manga: MangaSync): Observable<Response>
abstract fun add(manga: MangaSync): Observable<Response>
abstract fun bind(manga: MangaSync): Observable<Response>
abstract fun getStatus(status: Int): String
}

View File

@ -0,0 +1,222 @@
package eu.kanade.tachiyomi.data.mangasync.myanimelist
import android.content.Context
import android.net.Uri
import android.util.Xml
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
import eu.kanade.tachiyomi.data.network.GET
import eu.kanade.tachiyomi.data.network.POST
import eu.kanade.tachiyomi.data.network.asObservable
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import okhttp3.Credentials
import okhttp3.FormBody
import okhttp3.Headers
import okhttp3.RequestBody
import org.jsoup.Jsoup
import org.xmlpull.v1.XmlSerializer
import rx.Completable
import rx.Observable
import java.io.StringWriter
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
private lateinit var headers: Headers
companion object {
val BASE_URL = "https://myanimelist.net"
private val ENTRY_TAG = "entry"
private val CHAPTER_TAG = "chapter"
private val SCORE_TAG = "score"
private val STATUS_TAG = "status"
val READING = 1
val COMPLETED = 2
val ON_HOLD = 3
val DROPPED = 4
val PLAN_TO_READ = 6
val DEFAULT_STATUS = READING
val DEFAULT_SCORE = 0
}
init {
val username = getUsername()
val password = getPassword()
if (!username.isEmpty() && !password.isEmpty()) {
createHeaders(username, password)
}
}
override val name: String
get() = "MyAnimeList"
fun getLoginUrl() = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/account/verify_credentials.xml")
.toString()
fun getSearchUrl(query: String) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/manga/search.xml")
.appendQueryParameter("q", query)
.toString()
fun getListUrl(username: String) = Uri.parse(BASE_URL).buildUpon()
.appendPath("malappinfo.php")
.appendQueryParameter("u", username)
.appendQueryParameter("status", "all")
.appendQueryParameter("type", "manga")
.toString()
fun getUpdateUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath("${manga.remote_id}.xml")
.toString()
fun getAddUrl(manga: MangaSync) = Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/add")
.appendPath("${manga.remote_id}.xml")
.toString()
override fun login(username: String, password: String): Completable {
createHeaders(username, password)
return client.newCall(GET(getLoginUrl(), headers))
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (it.code() != 200) throw Exception("Login error") }
.toCompletable()
}
fun search(query: String): Observable<List<MangaSync>> {
return client.newCall(GET(getSearchUrl(query), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
MangaSync.create(id).apply {
title = it.selectText("title")!!
remote_id = it.selectInt("id")
total_chapters = it.selectInt("chapters")
}
}
.toList()
}
// MAL doesn't support score with decimals
fun getList(): Observable<List<MangaSync>> {
return networkService.forceCacheClient
.newCall(GET(getListUrl(getUsername()), headers))
.asObservable()
.map { Jsoup.parse(it.body().string()) }
.flatMap { Observable.from(it.select("manga")) }
.map {
MangaSync.create(id).apply {
title = it.selectText("series_title")!!
remote_id = it.selectInt("series_mangadb_id")
last_chapter_read = it.selectInt("my_read_chapters")
status = it.selectInt("my_status")
score = it.selectInt("my_score").toFloat()
total_chapters = it.selectInt("series_chapters")
}
}
.toList()
}
override fun update(manga: MangaSync): Observable<MangaSync> {
return Observable.defer {
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
manga.status = COMPLETED
}
client.newCall(POST(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not update manga") }
.map { manga }
}
}
override fun add(manga: MangaSync): Observable<MangaSync> {
return Observable.defer {
client.newCall(POST(getAddUrl(manga), headers, getMangaPostPayload(manga)))
.asObservable()
.doOnNext { it.close() }
.doOnNext { if (!it.isSuccessful) throw Exception("Could not add manga") }
.map { manga }
}
}
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
val xml = Xml.newSerializer()
val writer = StringWriter()
with(xml) {
setOutput(writer)
startDocument("UTF-8", false)
startTag("", ENTRY_TAG)
// Last chapter read
if (manga.last_chapter_read != 0) {
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
}
// Manga status in the list
inTag(STATUS_TAG, manga.status.toString())
// Manga score
inTag(SCORE_TAG, manga.score.toString())
endTag("", ENTRY_TAG)
endDocument()
}
val form = FormBody.Builder()
form.add("data", writer.toString())
return form.build()
}
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
startTag(namespace, tag)
text(body)
endTag(namespace, tag)
}
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 -> ""
}
}
fun createHeaders(username: String, password: String) {
val builder = Headers.Builder()
builder.add("Authorization", Credentials.basic(username, password))
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
headers = builder.build()
}
}

View File

@ -1,216 +0,0 @@
package eu.kanade.tachiyomi.data.mangasync.services
import android.content.Context
import android.net.Uri
import android.util.Xml
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.database.models.MangaSync
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.network.get
import eu.kanade.tachiyomi.data.network.post
import eu.kanade.tachiyomi.util.selectInt
import eu.kanade.tachiyomi.util.selectText
import okhttp3.*
import org.jsoup.Jsoup
import org.xmlpull.v1.XmlSerializer
import rx.Observable
import java.io.StringWriter
fun XmlSerializer.inTag(tag: String, body: String, namespace: String = "") {
startTag(namespace, tag)
text(body)
endTag(namespace, tag)
}
class MyAnimeList(private val context: Context, id: Int) : MangaSyncService(context, id) {
private lateinit var headers: Headers
private lateinit var username: String
companion object {
val BASE_URL = "http://myanimelist.net"
private val ENTRY_TAG = "entry"
private val CHAPTER_TAG = "chapter"
private val SCORE_TAG = "score"
private val STATUS_TAG = "status"
val READING = 1
val COMPLETED = 2
val ON_HOLD = 3
val DROPPED = 4
val PLAN_TO_READ = 6
val DEFAULT_STATUS = READING
val DEFAULT_SCORE = 0
}
init {
val username = preferences.mangaSyncUsername(this)
val password = preferences.mangaSyncPassword(this)
if (!username.isEmpty() && !password.isEmpty()) {
createHeaders(username, password)
}
}
override val name: String
get() = "MyAnimeList"
fun getLoginUrl(): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/account/verify_credentials.xml")
.toString()
}
override fun login(username: String, password: String): Observable<Boolean> {
createHeaders(username, password)
return networkService.request(get(getLoginUrl(), headers))
.map { it.code() == 200 }
}
fun getSearchUrl(query: String): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/manga/search.xml")
.appendQueryParameter("q", query)
.toString()
}
fun search(query: String): Observable<List<MangaSync>> {
return networkService.requestBody(get(getSearchUrl(query), headers))
.map { Jsoup.parse(it) }
.flatMap { Observable.from(it.select("entry")) }
.filter { it.select("type").text() != "Novel" }
.map {
val manga = MangaSync.create(this)
manga.title = it.selectText("title")
manga.remote_id = it.selectInt("id")
manga.total_chapters = it.selectInt("chapters")
manga
}
.toList()
}
fun getListUrl(username: String): String {
return Uri.parse(BASE_URL).buildUpon()
.appendPath("malappinfo.php")
.appendQueryParameter("u", username)
.appendQueryParameter("status", "all")
.appendQueryParameter("type", "manga")
.toString()
}
// MAL doesn't support score with decimals
fun getList(): Observable<List<MangaSync>> {
return networkService.requestBody(get(getListUrl(username), headers), true)
.map { Jsoup.parse(it) }
.flatMap { Observable.from(it.select("manga")) }
.map {
val manga = MangaSync.create(this)
manga.title = it.selectText("series_title")
manga.remote_id = it.selectInt("series_mangadb_id")
manga.last_chapter_read = it.selectInt("my_read_chapters")
manga.status = it.selectInt("my_status")
manga.score = it.selectInt("my_score").toFloat()
manga.total_chapters = it.selectInt("series_chapters")
manga
}
.toList()
}
fun getUpdateUrl(manga: MangaSync): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/update")
.appendPath(manga.remote_id.toString() + ".xml")
.toString()
}
override fun update(manga: MangaSync): Observable<Response> {
return Observable.defer {
if (manga.total_chapters != 0 && manga.last_chapter_read == manga.total_chapters) {
manga.status = COMPLETED
}
networkService.request(post(getUpdateUrl(manga), headers, getMangaPostPayload(manga)))
}
}
fun getAddUrl(manga: MangaSync): String {
return Uri.parse(BASE_URL).buildUpon()
.appendEncodedPath("api/mangalist/add")
.appendPath(manga.remote_id.toString() + ".xml")
.toString()
}
override fun add(manga: MangaSync): Observable<Response> {
return Observable.defer {
networkService.request(post(getAddUrl(manga), headers, getMangaPostPayload(manga)))
}
}
private fun getMangaPostPayload(manga: MangaSync): RequestBody {
val xml = Xml.newSerializer()
val writer = StringWriter()
with(xml) {
setOutput(writer)
startDocument("UTF-8", false)
startTag("", ENTRY_TAG)
// Last chapter read
if (manga.last_chapter_read != 0) {
inTag(CHAPTER_TAG, manga.last_chapter_read.toString())
}
// Manga status in the list
inTag(STATUS_TAG, manga.status.toString())
// Manga score
inTag(SCORE_TAG, manga.score.toString())
endTag("", ENTRY_TAG)
endDocument()
}
val form = FormBody.Builder()
form.add("data", writer.toString())
return form.build()
}
override fun bind(manga: MangaSync): Observable<Response> {
return getList()
.flatMap {
manga.sync_id = id
for (remoteManga in it) {
if (remoteManga.remote_id == manga.remote_id) {
// Manga is already in the list
manga.copyPersonalFrom(remoteManga)
return@flatMap update(manga)
}
}
// Set default fields if it's not found in the list
manga.score = DEFAULT_SCORE.toFloat()
manga.status = DEFAULT_STATUS
return@flatMap 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 -> ""
}
}
fun createHeaders(username: String, password: String) {
this.username = username
val builder = Headers.Builder()
builder.add("Authorization", Credentials.basic(username, password))
builder.add("User-Agent", "api-indiv-9F93C52A963974CF674325391990191C")
headers = builder.build()
}
}

View File

@ -0,0 +1,80 @@
package eu.kanade.tachiyomi.data.network
import com.squareup.duktape.Duktape
import okhttp3.HttpUrl
import okhttp3.Interceptor
import okhttp3.Request
import okhttp3.Response
class CloudflareInterceptor(private val cookies: PersistentCookieStore) : Interceptor {
//language=RegExp
private val operationPattern = Regex("""setTimeout\(function\(\)\{\s+(var (?:\w,)+f.+?\r?\n[\s\S]+?a\.value =.+?)\r?\n""")
//language=RegExp
private val passPattern = Regex("""name="pass" value="(.+?)"""")
//language=RegExp
private val challengePattern = Regex("""name="jschl_vc" value="(\w+)"""")
override fun intercept(chain: Interceptor.Chain): Response {
val response = chain.proceed(chain.request())
// Check if we already solved a challenge
if (response.code() != 503 &&
cookies.get(response.request().url()).any { it.name() == "cf_clearance" }) {
return response
}
// Check if Cloudflare anti-bot is on
if ("URL=/cdn-cgi/" in response.header("Refresh", "")
&& response.header("Server", "") == "cloudflare-nginx") {
return chain.proceed(resolveChallenge(response))
}
return response
}
private fun resolveChallenge(response: Response): Request {
val duktape = Duktape.create()
try {
val originalRequest = response.request()
val domain = originalRequest.url().host()
val content = response.body().string()
// CloudFlare requires waiting 4 seconds before resolving the challenge
Thread.sleep(4000)
val operation = operationPattern.find(content)?.groups?.get(1)?.value
val challenge = challengePattern.find(content)?.groups?.get(1)?.value
val pass = passPattern.find(content)?.groups?.get(1)?.value
if (operation == null || challenge == null || pass == null) {
throw RuntimeException("Failed resolving Cloudflare challenge")
}
val js = operation
//language=RegExp
.replace(Regex("""a\.value =(.+?) \+.*"""), "$1")
//language=RegExp
.replace(Regex("""\s{3,}[a-z](?: = |\.).+"""), "")
.replace("\n", "")
val result = (duktape.evaluate(js) as Double).toInt()
val answer = "${result + domain.length}"
val url = HttpUrl.parse("http://$domain/cdn-cgi/l/chk_jschl").newBuilder()
.addQueryParameter("jschl_vc", challenge)
.addQueryParameter("pass", pass)
.addQueryParameter("jschl_answer", answer)
.toString()
val referer = originalRequest.url().toString()
return GET(url, originalRequest.headers().newBuilder().add("Referer", referer).build())
} finally {
duktape.close()
}
}
}

View File

@ -1,12 +1,9 @@
package eu.kanade.tachiyomi.data.network
import android.content.Context
import okhttp3.*
import rx.Observable
import okhttp3.Cache
import okhttp3.OkHttpClient
import java.io.File
import java.net.CookieManager
import java.net.CookiePolicy
import java.net.CookieStore
class NetworkHelper(context: Context) {
@ -14,63 +11,28 @@ class NetworkHelper(context: Context) {
private val cacheSize = 5L * 1024 * 1024 // 5 MiB
private val cookieManager = CookieManager().apply {
setCookiePolicy(CookiePolicy.ACCEPT_ALL)
}
private val cookieManager = PersistentCookieJar(context)
private val forceCacheInterceptor = { chain: Interceptor.Chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=" + 600)
.build()
}
private val client = OkHttpClient.Builder()
.cookieJar(JavaNetCookieJar(cookieManager))
val client = OkHttpClient.Builder()
.cookieJar(cookieManager)
.cache(Cache(cacheDir, cacheSize))
.build()
private val forceCacheClient = client.newBuilder()
.addNetworkInterceptor(forceCacheInterceptor)
val forceCacheClient = client.newBuilder()
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=600")
.build()
}
.build()
val cookies: CookieStore
get() = cookieManager.cookieStore
@JvmOverloads
fun request(request: Request, forceCache: Boolean = false): Observable<Response> {
return Observable.fromCallable {
val c = if (forceCache) forceCacheClient else client
c.newCall(request).execute().apply { body().close() }
}
}
@JvmOverloads
fun requestBody(request: Request, forceCache: Boolean = false): Observable<String> {
return Observable.fromCallable {
val c = if (forceCache) forceCacheClient else client
c.newCall(request).execute().body().string()
}
}
fun requestBodyProgress(request: Request, listener: ProgressListener): Observable<Response> {
return Observable.fromCallable { requestBodyProgressBlocking(request, listener) }
}
fun requestBodyProgressBlocking(request: Request, listener: ProgressListener): Response {
val progressClient = client.newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body(), listener))
.build()
}
.build()
return progressClient.newCall(request).execute()
}
val cloudflareClient = client.newBuilder()
.addInterceptor(CloudflareInterceptor(cookies))
.build()
val cookies: PersistentCookieStore
get() = cookieManager.store
}

View File

@ -0,0 +1,66 @@
package eu.kanade.tachiyomi.data.network
import okhttp3.Call
import okhttp3.OkHttpClient
import okhttp3.Request
import okhttp3.Response
import rx.Observable
import rx.Producer
import rx.Subscription
import java.util.concurrent.atomic.AtomicBoolean
fun Call.asObservable(): Observable<Response> {
return Observable.create { subscriber ->
// Since Call is a one-shot type, clone it for each new subscriber.
val call = if (!isExecuted) this else {
// TODO use clone method in OkHttp 3.5
val field = javaClass.getDeclaredField("client").apply { isAccessible = true }
val client = field.get(this) as OkHttpClient
client.newCall(request())
}
// Wrap the call in a helper which handles both unsubscription and backpressure.
val requestArbiter = object : AtomicBoolean(), Producer, Subscription {
override fun request(n: Long) {
if (n == 0L || !compareAndSet(false, true)) return
try {
val response = call.execute()
if (!subscriber.isUnsubscribed) {
subscriber.onNext(response)
subscriber.onCompleted()
}
} catch (error: Exception) {
if (!subscriber.isUnsubscribed) {
subscriber.onError(error)
}
}
}
override fun unsubscribe() {
call.cancel()
}
override fun isUnsubscribed(): Boolean {
return call.isCanceled
}
}
subscriber.add(requestArbiter)
subscriber.setProducer(requestArbiter)
}
}
fun OkHttpClient.newCallWithProgress(request: Request, listener: ProgressListener): Call {
val progressClient = newBuilder()
.cache(null)
.addNetworkInterceptor { chain ->
val originalResponse = chain.proceed(chain.request())
originalResponse.newBuilder()
.body(ProgressResponseBody(originalResponse.body(), listener))
.build()
}
.build()
return progressClient.newCall(request)
}

View File

@ -0,0 +1,19 @@
package eu.kanade.tachiyomi.data.network
import android.content.Context
import okhttp3.Cookie
import okhttp3.CookieJar
import okhttp3.HttpUrl
class PersistentCookieJar(context: Context) : CookieJar {
val store = PersistentCookieStore(context)
override fun saveFromResponse(url: HttpUrl, cookies: List<Cookie>) {
store.addAll(url, cookies)
}
override fun loadForRequest(url: HttpUrl): List<Cookie> {
return store.get(url)
}
}

View File

@ -0,0 +1,75 @@
package eu.kanade.tachiyomi.data.network
import android.content.Context
import okhttp3.Cookie
import okhttp3.HttpUrl
import java.net.URI
import java.util.concurrent.ConcurrentHashMap
class PersistentCookieStore(context: Context) {
private val cookieMap = ConcurrentHashMap<String, List<Cookie>>()
private val prefs = context.getSharedPreferences("cookie_store", Context.MODE_PRIVATE)
init {
for ((key, value) in prefs.all) {
@Suppress("UNCHECKED_CAST")
val cookies = value as? Set<String>
if (cookies != null) {
try {
val url = HttpUrl.parse("http://$key")
val nonExpiredCookies = cookies.map { Cookie.parse(url, it) }
.filter { !it.hasExpired() }
cookieMap.put(key, nonExpiredCookies)
} catch (e: Exception) {
// Ignore
}
}
}
}
fun addAll(url: HttpUrl, cookies: List<Cookie>) {
synchronized(this) {
val key = url.uri().host
// Append or replace the cookies for this domain.
val cookiesForDomain = cookieMap[key].orEmpty().toMutableList()
for (cookie in cookies) {
// Find a cookie with the same name. Replace it if found, otherwise add a new one.
val pos = cookiesForDomain.indexOfFirst { it.name() == cookie.name() }
if (pos == -1) {
cookiesForDomain.add(cookie)
} else {
cookiesForDomain[pos] = cookie
}
}
cookieMap.put(key, cookiesForDomain)
// Get cookies to be stored in disk
val newValues = cookiesForDomain.asSequence()
.filter { it.persistent() && !it.hasExpired() }
.map { it.toString() }
.toSet()
prefs.edit().putStringSet(key, newValues).apply()
}
}
fun removeAll() {
synchronized(this) {
prefs.edit().clear().apply()
cookieMap.clear()
}
}
fun get(url: HttpUrl) = get(url.uri().host)
fun get(uri: URI) = get(uri.host)
private fun get(url: String): List<Cookie> {
return cookieMap[url].orEmpty().filter { !it.hasExpired() }
}
private fun Cookie.hasExpired() = System.currentTimeMillis() >= expiresAt()
}

View File

@ -1,14 +1,13 @@
package eu.kanade.tachiyomi.data.network
import okhttp3.*
import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeUnit.MINUTES
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, TimeUnit.MINUTES).build()
private val DEFAULT_CACHE_CONTROL = CacheControl.Builder().maxAge(10, MINUTES).build()
private val DEFAULT_HEADERS = Headers.Builder().build()
private val DEFAULT_BODY: RequestBody = FormBody.Builder().build()
@JvmOverloads
fun get(url: String,
fun GET(url: String,
headers: Headers = DEFAULT_HEADERS,
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {
@ -19,8 +18,7 @@ fun get(url: String,
.build()
}
@JvmOverloads
fun post(url: String,
fun POST(url: String,
headers: Headers = DEFAULT_HEADERS,
body: RequestBody = DEFAULT_BODY,
cache: CacheControl = DEFAULT_CACHE_CONTROL): Request {

View File

@ -10,13 +10,15 @@ import eu.kanade.tachiyomi.R
*/
class PreferenceKeys(context: Context) {
val theme = context.getString(R.string.pref_theme_key)
val rotation = context.getString(R.string.pref_rotation_type_key)
val enableTransitions = context.getString(R.string.pref_enable_transitions_key)
val showPageNumber = context.getString(R.string.pref_show_page_number_key)
val hideStatusBar = context.getString(R.string.pref_hide_status_bar_key)
val fullscreen = context.getString(R.string.pref_fullscreen_key)
val keepScreenOn = context.getString(R.string.pref_keep_screen_on_key)
@ -24,6 +26,10 @@ class PreferenceKeys(context: Context) {
val customBrightnessValue = context.getString(R.string.pref_custom_brightness_value_key)
val colorFilter = context.getString(R.string.pref_color_filter_key)
val colorFilterValue = context.getString(R.string.pref_color_filter_value_key)
val defaultViewer = context.getString(R.string.pref_default_viewer_key)
val imageScaleType = context.getString(R.string.pref_image_scale_type_key)
@ -52,7 +58,7 @@ class PreferenceKeys(context: Context) {
val lastUsedCatalogueSource = context.getString(R.string.pref_last_catalogue_source_key)
val seamlessMode = context.getString(R.string.pref_seamless_mode_key)
val lastUsedCategory = context.getString(R.string.pref_last_used_category_key)
val catalogueAsList = context.getString(R.string.pref_display_catalogue_as_list)
@ -64,9 +70,7 @@ class PreferenceKeys(context: Context) {
val downloadOnlyOverWifi = context.getString(R.string.pref_download_only_over_wifi_key)
val removeAfterRead = context.getString(R.string.pref_remove_after_read_key)
val removeAfterReadPrevious = context.getString(R.string.pref_remove_after_read_previous_key)
val removeAfterReadSlots = context.getString(R.string.pref_remove_after_read_slots_key)
val removeAfterMarkedAsRead = context.getString(R.string.pref_remove_after_marked_as_read_key)
@ -74,12 +78,17 @@ class PreferenceKeys(context: Context) {
val libraryUpdateRestriction = context.getString(R.string.pref_library_update_restriction_key)
val libraryUpdateCategories = context.getString(R.string.pref_library_update_categories_key)
val filterDownloaded = context.getString(R.string.pref_filter_downloaded_key)
val filterUnread = context.getString(R.string.pref_filter_unread_key)
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
val automaticUpdates = context.getString(R.string.pref_enable_automatic_updates_key)
val startScreen = context.getString(R.string.pref_start_screen_key)
fun sourceUsername(sourceId: Int) = "pref_source_username_$sourceId"
fun sourcePassword(sourceId: Int) = "pref_source_password_$sourceId"
@ -87,4 +96,6 @@ class PreferenceKeys(context: Context) {
fun syncPassword(syncId: Int) = "pref_mangasync_password_$syncId"
val libraryAsList = context.getString(R.string.pref_display_library_as_list)
}

View File

@ -6,14 +6,14 @@ import android.preference.PreferenceManager
import com.f2prateek.rx.preferences.Preference
import com.f2prateek.rx.preferences.RxSharedPreferences
import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService
import eu.kanade.tachiyomi.data.source.base.Source
import eu.kanade.tachiyomi.data.mangasync.MangaSyncService
import eu.kanade.tachiyomi.data.source.Source
import java.io.File
import java.io.IOException
fun <T> Preference<T>.getOrDefault(): T = get() ?: defaultValue()!!
class PreferencesHelper(private val context: Context) {
class PreferencesHelper(context: Context) {
val keys = PreferenceKeys(context)
@ -32,28 +32,11 @@ class PreferencesHelper(private val context: Context) {
}
}
companion object {
fun startScreen() = prefs.getInt(keys.startScreen, 1)
fun getLibraryUpdateInterval(context: Context): Int {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
context.getString(R.string.pref_library_update_interval_key), 0)
}
fun clear() = prefs.edit().clear().apply()
fun getAutomaticUpdateStatus(context: Context): Boolean {
return PreferenceManager.getDefaultSharedPreferences(context).getBoolean(
context.getString(R.string.pref_enable_automatic_updates), false)
}
@JvmStatic
fun getTheme(context: Context): Int {
return PreferenceManager.getDefaultSharedPreferences(context).getInt(
context.getString(R.string.pref_theme_key), 1)
}
}
fun clear() {
prefs.edit().clear().apply()
}
fun theme() = prefs.getInt(keys.theme, 1)
fun rotation() = rxPrefs.getInteger(keys.rotation, 1)
@ -61,13 +44,17 @@ class PreferencesHelper(private val context: Context) {
fun showPageNumber() = rxPrefs.getBoolean(keys.showPageNumber, true)
fun hideStatusBar() = rxPrefs.getBoolean(keys.hideStatusBar, true)
fun fullscreen() = rxPrefs.getBoolean(keys.fullscreen, true)
fun keepScreenOn() = rxPrefs.getBoolean(keys.keepScreenOn, true)
fun customBrightness() = rxPrefs.getBoolean(keys.customBrightness, false)
fun customBrightnessValue() = rxPrefs.getFloat(keys.customBrightnessValue, 0f)
fun customBrightnessValue() = rxPrefs.getInteger(keys.customBrightnessValue, 0)
fun colorFilter() = rxPrefs.getBoolean(keys.colorFilter, false)
fun colorFilterValue() = rxPrefs.getInteger(keys.colorFilterValue, 0)
fun defaultViewer() = prefs.getInt(keys.defaultViewer, 1)
@ -97,7 +84,9 @@ class PreferencesHelper(private val context: Context) {
fun lastUsedCatalogueSource() = rxPrefs.getInteger(keys.lastUsedCatalogueSource, -1)
fun seamlessMode() = prefs.getBoolean(keys.seamlessMode, true)
fun lastUsedCategory() = rxPrefs.getInteger(keys.lastUsedCategory, 0)
fun lastVersionCode() = rxPrefs.getInteger("last_version_code", 0)
fun catalogueAsList() = rxPrefs.getBoolean(keys.catalogueAsList, false)
@ -131,9 +120,7 @@ class PreferencesHelper(private val context: Context) {
fun downloadOnlyOverWifi() = prefs.getBoolean(keys.downloadOnlyOverWifi, true)
fun removeAfterRead() = prefs.getBoolean(keys.removeAfterRead, false)
fun removeAfterReadPrevious() = prefs.getBoolean(keys.removeAfterReadPrevious, false)
fun removeAfterReadSlots() = prefs.getInt(keys.removeAfterReadSlots, -1)
fun removeAfterMarkedAsRead() = prefs.getBoolean(keys.removeAfterMarkedAsRead, false)
@ -141,8 +128,16 @@ class PreferencesHelper(private val context: Context) {
fun libraryUpdateRestriction() = prefs.getStringSet(keys.libraryUpdateRestriction, emptySet())
fun libraryUpdateCategories() = rxPrefs.getStringSet(keys.libraryUpdateCategories, emptySet())
fun libraryAsList() = rxPrefs.getBoolean(keys.libraryAsList, false)
fun filterDownloaded() = rxPrefs.getBoolean(keys.filterDownloaded, false)
fun filterUnread() = rxPrefs.getBoolean(keys.filterUnread, false)
fun automaticUpdates() = prefs.getBoolean(keys.automaticUpdates, false)
fun hiddenCatalogues() = rxPrefs.getStringSet("hidden_catalogues", emptySet())
}

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