Compare commits

...

147 Commits

Author SHA1 Message Date
110df59197 Release 0.1.4 2016-02-21 15:59:07 +01:00
75ae4081d8 Merge pull request #167 from j2ghz/patch-1
Fix link broken by PR #164
2016-02-20 23:36:24 +01:00
2efca050b3 Fix link broken by PR #164 2016-02-20 23:33:55 +01:00
37e119c4f2 Merge pull request #164 from j2ghz/patch-1
Create ISSUE_TEMPLATE.md
2016-02-20 22:07:47 +01:00
9786074119 Move github files to .github/ 2016-02-20 12:06:53 +01:00
d4876b426f Create ISSUE_TEMPLATE.md 2016-02-20 12:02:57 +01:00
8581e4667a Merge pull request #160 from NoodleMage/issue_118
Implements Issue #118 download from recent tab
2016-02-19 20:36:31 +01:00
b94f86765d Code cleanup, implements #118 2016-02-18 19:01:40 +01:00
ba0f3778ce Can now mark as read / unread 2016-02-18 17:40:12 +01:00
aac6b242a0 Can now delete manga from recent + added missing res files #118 2016-02-18 17:29:31 +01:00
dec9442a65 Can now download from recent tab. #118 2016-02-18 17:29:29 +01:00
b33da641d9 Fix crash in chapters list #159 2016-02-18 14:25:35 +01:00
96d498e7e5 Merge pull request #152 from icewind1991/chapter-parsing
Chapter recognition improvements
2016-02-16 21:08:10 +01:00
eee137a084 prefer numbers at the start of the chapter title if otherwise unparsed 2016-02-16 21:03:52 +01:00
5e834ae3be improve colon handling 2016-02-16 21:03:21 +01:00
dcfda61aba Always create nomedia file 2016-02-16 20:47:23 +01:00
5ac7f7057a Merge pull request #150 from NoodleMage/comments
Improved comments
2016-02-16 20:41:45 +01:00
ff46c61f63 Merge pull request #151 from icewind1991/chapter-recognition-fallback
Fix infinite loop when no chapter number is parsed
2016-02-16 20:41:34 +01:00
57b64a412e Fix infinite loop when no chapter number is parsed 2016-02-16 20:37:57 +01:00
1e81f75377 Possible fix for #120 2016-02-16 18:19:54 +01:00
1dd49a2ab1 Improved comments 2016-02-16 15:30:15 +01:00
1cd77a97a7 Merge pull request #143 from NoodleMage/fab_improvement
Moved edit to cover select and update manga info view
2016-02-15 23:28:04 +01:00
f820522a69 Show keep screen on in reader settings. Closes #146 2016-02-15 21:25:01 +01:00
3da613dedb Moved edit cover to library | Updated manga info view | Updated catalogue
grid
2016-02-15 16:59:24 +01:00
5c329d2314 Incorrect mark as read with seamless mode. #142 2016-02-14 15:35:58 +01:00
4c073e713d Merge pull request #139 from j2ghz/patch-1
Make CONTRIBUTING.md more visible
2016-02-13 18:08:57 +01:00
2832f4ae5e Update README.md 2016-02-13 17:57:17 +01:00
7690e8a53f Merge pull request #137 from NoodleMage/fab_improvement
FAB animation update
2016-02-13 14:53:51 +01:00
3a19f8e40b FAB animation update 2016-02-13 12:08:15 +01:00
a33b525f9e Merge pull request #136 from icewind1991/search-sort
sort by views for mangafox and mangahere search results
2016-02-12 22:01:43 +01:00
7d6ce46829 sort by views for mangafox and mangahere search results 2016-02-12 21:49:38 +01:00
a90a4bf80c Remove old orientation lock. Add orientation types to preferences 2016-02-12 21:22:54 +01:00
140bf8caee Allow to force a rotation 2016-02-12 19:36:00 +01:00
56a45f263e Strip html tags from batoto notice and directly throw an exception 2016-02-12 15:38:16 +01:00
01d6ddfafb Merge pull request #132 from icewind1991/batato-staff-notice
Show batoto staff notice if updating chapters failed
2016-02-11 23:32:32 +01:00
393b4916f6 Show batoto staff notice if updating chapters failed 2016-02-11 22:59:24 +01:00
cb3c3af865 Include reactive network as library 2016-02-11 14:16:36 +01:00
5a83976fa5 Remove unneeded dependency. 2016-02-10 21:15:02 +01:00
a81f6c3ac4 Trying to give write permissions on SD card 2016-02-10 15:41:59 +01:00
6846ce5bfb Increase maximum allowed scale on pagers 2016-02-10 15:19:31 +01:00
0c0ebe06e5 Volume keys scroll pages. Closes #95 2016-02-10 15:06:18 +01:00
e50c683159 Fix tests failing after upgrading EventBus 2016-02-09 22:07:14 +01:00
872af276ea Merge pull request #130 from icewind1991/chapter-number-parsing
Improve chapter number parsing
2016-02-09 21:50:32 +01:00
e6faee9779 handle chapters with part numbers 2016-02-09 21:23:57 +01:00
bc1ddd4379 fallback to parsing parts to handle arc numbers 2016-02-09 21:20:17 +01:00
e348d6c1cf Upgrade to EventBus 3 2016-02-09 21:19:11 +01:00
7835921045 Merge pull request #126 from beschoenen/downloading
Download features
2016-02-09 21:15:36 +01:00
1611a274b9 differentiate subchapters denoted by an alpha prefix 2016-02-09 20:57:26 +01:00
fa4a8204a4 prefer numbers without anything appended when parsing chapter numbers 2016-02-09 20:43:10 +01:00
5977e9f47f handle chapter versions which are attached to the chapter number 2016-02-09 20:26:51 +01:00
63d0161da5 move clear queue to presenter 2016-02-09 17:42:39 +01:00
d8b46c1969 set display mode title 2016-02-09 16:57:11 +01:00
f84731c2df cleanup chapter action menu 2016-02-09 16:50:26 +01:00
50d71d1395 clear the download queue 2016-02-09 16:34:41 +01:00
4be0b2502e Change stop to pause in download queue view 2016-02-09 16:25:36 +01:00
6c069ad87b multiple chapter download from manga view 2016-02-09 16:01:11 +01:00
e69011ac5b Use a shorter description for seamless mode 2016-02-08 22:33:23 +01:00
ea130a0899 Merge pull request #112 from icewind1991/seamless-chapters
Seamless chapter transition
2016-02-08 22:25:56 +01:00
2566862e0f seamless chapter transitions 2016-02-08 22:24:47 +01:00
16081817c2 Upgrade dependencies 2016-02-08 22:14:48 +01:00
945625d3ad Cancel notification when no new chapters are found. Closes #121 2016-02-07 19:15:45 +01:00
050b9c9fce Remember last used source. Closes #30 2016-02-06 19:03:15 +01:00
c35184abdc Upgrade gradle. Other minor changes 2016-02-06 00:37:11 +01:00
34c5f0b7ba Try to mark readded chapters as read. #119 2016-02-05 22:08:54 +01:00
6435eeb251 Use network cache 2016-02-05 20:18:39 +01:00
eec2dcd981 Fix a crash 2016-02-05 17:30:58 +01:00
57ba368ae0 Add library search. Closes #64 2016-02-05 16:24:34 +01:00
ed06469885 Trying to fix a backpressure isue 2016-02-05 15:42:53 +01:00
79cd8c691e Minor changes 2016-02-05 14:53:07 +01:00
391550f49a Implement zoom start position. Closes #92. Rapid decoder properly throws an error when it fails to decode. 2016-02-04 17:16:47 +01:00
6aa07dd17e Download the first image of the next chapter 2016-02-03 22:21:15 +01:00
aada373a0c Replace onProcessRestart with the new startables. 2016-02-03 21:09:40 +01:00
3deac86bbe Merge pull request #98 from NoodleMage/download_updates
Download updates
2016-02-03 17:17:15 +01:00
d7aef2e97a Application can now check if update available 2016-02-03 17:12:26 +01:00
7953ba6e78 Display date in local format. Fix #108 2016-02-03 13:28:55 +01:00
8aa3c2a260 Update readme 2016-02-03 12:58:07 +01:00
c204548df5 Release 0.1.3 2016-02-03 12:56:12 +01:00
4d47f5a387 Show brigthness preference in reader settings. #106 2016-02-03 00:32:16 +01:00
7944bb8479 Fix #100 2016-02-01 20:53:06 +01:00
c4ae88a8ff Use Rapid only for regions. Fixes #97 (probably) 2016-01-31 22:41:45 +01:00
ad953b7bf6 Ask for external storage permissions on Marshmallow. Fixes #76 and #36 2016-01-31 22:38:54 +01:00
d799ae5d72 Webtoon reader "restores" position on rotation. Fixes #93 2016-01-31 18:48:13 +01:00
a3ec057384 Now tap on edges of webtoon reader scrolls by 3/4 screen 2016-01-31 02:40:05 +01:00
486f129e62 Merge pull request #86 from j2ghz/patch-1
CONTRIBUTING.md
2016-01-30 18:29:30 +01:00
e6c3864c71 Create CONTRIBUTING.md 2016-01-30 18:01:10 +01:00
7461f12066 Merge pull request #90 from cyalins/patch-1
Reworded and shortened some strings
2016-01-30 16:55:07 +01:00
e53b05feba Fix gestures on vertical readers 2016-01-30 16:40:41 +01:00
bcefc176c1 Use Rapid decoder also when no regions are required 2016-01-30 16:10:53 +01:00
d0580d0df1 Merge pull request #94 from NoodleMage/local_cover_small_fix
Small fix for local cover loading
2016-01-30 13:59:53 +01:00
28fd22dfe0 Manga initialized check. Now takes network cover image if something went
wrong
2016-01-30 13:46:18 +01:00
742924625d Update strings.xml 2016-01-30 11:57:27 +11:00
78a2eae719 Minor changes 2016-01-30 00:41:39 +01:00
38bb0b61d4 Merge pull request #91 from NoodleMage/change_cover
Can now manually set cover pictures. #79
2016-01-30 00:12:54 +01:00
8b52fea602 Can now manually set cover pictures. #79
Forgot to add IOHandler

Removed FAB library now use the internal one. Changed getTimestamp to modification date.

Rewrote IOHandler.  Fixed Drive Bug. More bug fixes. Tested working for API 16 and 23

Fixed merge bugs
2016-01-29 20:44:51 +01:00
c03495be94 All chapter filters are now saved 2016-01-29 19:36:08 +01:00
f19889c222 Avoid OutOfMemory crashes on webtoon viewer increasing view holders height 2016-01-29 16:17:26 +01:00
af0ab5ec86 Reworded and shortened some strings 2016-01-30 02:12:20 +11:00
ea4fa60e01 Trying improvements for webtoon viewer. #71 2016-01-29 14:54:53 +01:00
4b60560a9f Add smart fit. Closes #85 2016-01-28 18:26:43 +01:00
733b0da461 Upgrade OkHttp to 3.0.1 2016-01-28 16:44:18 +01:00
db074a371d Merge pull request #82 from cyalins/master
Changed the wording on some strings
2016-01-28 13:39:06 +01:00
bb110ce353 Changed the wording on some strings
Fixed grammar issues and reworded some strings for clarity
2016-01-28 14:14:07 +11:00
74c32f9e16 Minor refactor on caches 2016-01-28 01:01:55 +01:00
d8ab8f297f Let Glide cache local covers, it improves performance loading the covers from the library 2016-01-27 19:42:01 +01:00
ec7df6b1f2 Merge pull request #77 from NoodleMage/material_nav
Added icons to navigation drawer #47
2016-01-27 18:52:16 +01:00
ef03ca22d1 Added icons to navigation drawer. #47
Settings now inline with rest of menu

@Bind is onelined

Added icons to navigation drawer. Moved settings to the bottom of nav drawer.

Settings now inline with rest of menu

@Bind is onelined

Added icons to navigation drawer. #47
2016-01-27 18:48:43 +01:00
82865dd3fd Format fixes 2016-01-27 17:47:43 +01:00
ba5d13936c Merge pull request #78 from NoodleMage/upstream
Code optimization. Added javadoc. Removed setSize for it is not used
2016-01-27 17:41:53 +01:00
23a6f76c37 Code optimization. Added javadoc. Removed setSize for it is not used
Fixed some mistakes.

Code optimization. Added comments. Few comment mistake fixes

Few comments

Added classes because of renaming

Fixed refactor mistakes :(.

typo + removed todo empty class

Changed o to 0. Some renaming.  Checked for nullability on string.isEmpty() function to prevent crashes

Removed redundant null check

Update ChapterCache.java

Another o to 0 change. Damn this .o! :)
2016-01-27 17:37:36 +01:00
0c9bc97fe8 Initial support for custom images scaling (#40) 2016-01-27 01:48:40 +01:00
c6ecfb2f67 Trying to fix some crashes 2016-01-26 19:18:31 +01:00
8ca0814aff Add a way to search in MAL only from the user's list 2016-01-26 16:33:19 +01:00
eceb4c3682 Reorganize readme 2016-01-26 16:19:55 +01:00
e7ecd5a5c2 Add F-Droid badge 2016-01-26 15:52:14 +01:00
f7c20a5517 Update readme 2016-01-26 15:19:17 +01:00
6f409c0e3b Add an alternative way to display the chapter title (#54) 2016-01-25 19:57:13 +01:00
0a31c223e3 Don't lint release builds 2016-01-25 15:15:47 +01:00
0f42956f3f Update readme 2016-01-25 14:01:37 +01:00
ac2485d4a7 Release 0.1.2 2016-01-25 13:56:58 +01:00
7993ec5074 Make toolbar always visible 2016-01-25 13:54:23 +01:00
4521174138 Fix layout overlapping 2016-01-25 13:43:21 +01:00
27b95e9d73 Minor changes 2016-01-25 13:19:03 +01:00
a54425f47d Merge pull request #69 from icewind1991/info-show-source
Show manga source in info panel
2016-01-25 13:01:44 +01:00
4918e67fda Show manga source in info panel 2016-01-25 12:49:56 +01:00
b174adbab0 Use a gradient at the bottom of the cover. Remove external repositories from gradle 2016-01-24 23:41:21 +01:00
59cc87c583 Fix #58 and #59 2016-01-24 13:57:20 +01:00
0e87dc995a Add backpressure buffer for downloads 2016-01-24 13:23:29 +01:00
fad7b75b96 Place reload button above the image 2016-01-24 12:52:41 +01:00
c99c90fc4c Merge pull request #57 from icewind1991/chapter-list-ellipsize
elipsize chapter list in the middle
2016-01-24 12:42:49 +01:00
594219848d Fix number of simultaneous downloads ignored (again) 2016-01-24 12:37:41 +01:00
fa301bfbd2 elipsize chapter list in the middle 2016-01-24 12:15:43 +01:00
50306f6ea3 Merge pull request #53 from icewind1991/sort-order
save per-manga sort order
2016-01-24 00:10:02 +01:00
9b90ad0a3b save per-manga sort order 2016-01-24 00:01:24 +01:00
5c854984e4 Fix #52 2016-01-23 21:58:36 +01:00
6c844cfd9c Merge pull request #51 from icewind1991/last-page
Load the last page when switching to the previous chapter (Fix #48)
2016-01-23 19:13:21 +01:00
9e666dcdb3 Load the last page when switching to the previous chapter 2016-01-23 17:10:56 +01:00
e81f98a975 Fix an UI refresh issue 2016-01-23 14:17:01 +01:00
11dc0d7e9e Change filename for downloaded chapters, using the last path from the url is not reliable. This will break compatibility with previously downloaded chapters, they have to be deleted and downloaded again.
Disable download progress in the chapters view, it will avoid some crashes.
2016-01-23 13:58:53 +01:00
07ed2e2ebb Hold the same manga instance (allowing to refresh manga state from the catalogue). Other minor changes. 2016-01-22 20:22:16 +01:00
e1aa460106 Allow to display manga from catalogue as a simple list (#35) 2016-01-22 17:37:23 +01:00
75a77566cf Trying switches instead of checkboxes 2016-01-21 16:55:18 +01:00
dd0a2d842a Improve recent chapters layout 2016-01-21 16:38:25 +01:00
fa71e906c9 Change recent chapters query, now it shows last month updates. Download manager now uses a thread pool. 2016-01-21 02:26:34 +01:00
e6a17e25a9 Tint navigation bar on Lollipop and higher 2016-01-20 22:06:22 +01:00
d88513de56 Reenable recent updates tab 2016-01-20 19:43:44 +01:00
ad97d03f1d Change toolbar color (Fix #43). Allow to also remove from library (Fix #44). Rewrite RxPager. 2016-01-20 19:21:17 +01:00
7fc23d526b Update readme 2016-01-20 14:46:05 +01:00
239 changed files with 5354 additions and 1947 deletions

30
.github/CONTRIBUTING.md vendored Normal file
View File

@ -0,0 +1,30 @@
# 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
* Include steps to reproduce (if not obvious from description)
* Include screenshot (if needed)
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
* For large logs use http://pastebin.com/ (or similar)
* For multipart issues use list like this:
* [x] Done
* [ ] Not done
```
* [x] Done
* [ ] Not done
```
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
DON'T: https://github.com/inorichi/tachiyomi/issues/75
# Feature requests
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
* Include screenshot (if needed)
# Translations
File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated.
Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs)

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

@ -0,0 +1,6 @@
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
Remove line above and describe your issue here. Fill out version below.
---
Version: r000 or v0.0.0

View File

@ -1,8 +1,16 @@
[![stable release](https://img.shields.io/badge/release-v0.1.4-blue.svg)](https://github.com/inorichi/tachiyomi/releases)
[![fdroid release](https://img.shields.io/badge/release-F--Droid-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi)
[![latest debug build](https://img.shields.io/badge/debug-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk)
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
**Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened issues.**
Tachiyomi is a free and open source manga reader for Android. Tachiyomi is a free and open source manga reader for Android.
Keep in mind it's still a beta, so expect it to crash sometimes. Keep in mind it's still a beta, so expect it to crash sometimes.
Current features: ## Features
* Online and offline reading * Online and offline reading
* Configurable reader with multiple viewers and settings * Configurable reader with multiple viewers and settings
@ -12,11 +20,6 @@ Current features:
* Schedule searching for updates * Schedule searching for updates
* Categories to organize your library * Categories to organize your library
## Download
[![stable release](https://img.shields.io/badge/release-v0.1.0-blue.svg)](https://github.com/inorichi/tachiyomi/releases/download/v0.1.0/tachiyomi-v0.1.0.apk)
[![latest debug build](https://img.shields.io/badge/debug-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk)
## License ## License
Copyright 2015 Javier Tomás Copyright 2015 Javier Tomás

View File

@ -1,6 +1,5 @@
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
apply plugin: 'android-sdk-manager'
apply plugin: 'com.android.application' apply plugin: 'com.android.application'
apply plugin: 'com.neenbedankt.android-apt' apply plugin: 'com.neenbedankt.android-apt'
apply plugin: 'me.tatarka.retrolambda' apply plugin: 'me.tatarka.retrolambda'
@ -29,6 +28,10 @@ ext {
} }
} }
def includeUpdater() {
return hasProperty("include_updater");
}
android { android {
compileSdkVersion 23 compileSdkVersion 23
buildToolsVersion "23.0.2" buildToolsVersion "23.0.2"
@ -39,12 +42,13 @@ android {
minSdkVersion 16 minSdkVersion 16
targetSdkVersion 23 targetSdkVersion 23
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner" testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 2 versionCode 5
versionName "0.1.1" versionName "0.1.4"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\"" buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\"" buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\"" buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
} }
compileOptions { compileOptions {
@ -73,19 +77,29 @@ android {
lintOptions { lintOptions {
abortOnError false abortOnError false
checkReleaseBuilds false
} }
} }
apt {
arguments {
eventBusIndex "eu.kanade.tachiyomi.EventBusIndex"
}
}
dependencies { dependencies {
final SUPPORT_LIBRARY_VERSION = '23.1.1' final SUPPORT_LIBRARY_VERSION = '23.1.1'
final DAGGER_VERSION = '2.0.2' final DAGGER_VERSION = '2.0.2'
final MOCKITO_VERSION = '1.10.19' final EVENTBUS_VERSION = '3.0.0'
final OKHTTP_VERSION = '3.1.1'
final STORIO_VERSION = '1.8.0' final STORIO_VERSION = '1.8.0'
final ICEPICK_VERSION = '3.1.0' final ICEPICK_VERSION = '3.1.0'
final MOCKITO_VERSION = '1.10.19'
compile fileTree(dir: 'libs', include: ['*.jar']) compile fileTree(dir: 'libs', include: ['*.jar'])
compile project(":SubsamplingScaleImageView") compile project(":SubsamplingScaleImageView")
compile project(":ReactiveNetwork")
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION"
@ -93,31 +107,33 @@ dependencies {
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION" compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:recyclerview-v7:$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:support-annotations:$SUPPORT_LIBRARY_VERSION"
compile 'com.squareup.okhttp:okhttp-urlconnection:2.7.2' compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION"
compile 'com.squareup.okhttp:okhttp:2.7.2' compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
compile 'com.squareup.okio:okio:1.6.0' compile 'com.squareup.okio:okio:1.6.0'
compile 'com.google.code.gson:gson:2.5' compile 'com.google.code.gson:gson:2.5'
compile 'com.jakewharton:disklrucache:2.0.2' compile 'com.jakewharton:disklrucache:2.0.2'
compile 'org.jsoup:jsoup:1.8.3' compile 'org.jsoup:jsoup:1.8.3'
compile 'io.reactivex:rxandroid:1.1.0' compile 'io.reactivex:rxandroid:1.1.0'
compile 'io.reactivex:rxjava:1.1.0' compile 'io.reactivex:rxjava:1.1.0'
compile 'com.squareup.retrofit:retrofit:1.9.0'
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1' compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION" compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION" compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
compile 'info.android15.nucleus:nucleus:2.0.4' compile 'info.android15.nucleus:nucleus:2.0.4'
compile 'de.greenrobot:eventbus:2.4.0'
compile 'com.github.bumptech.glide:glide:3.6.1' compile 'com.github.bumptech.glide:glide:3.6.1'
compile 'com.jakewharton:butterknife:7.0.1' compile 'com.jakewharton:butterknife:7.0.1'
compile 'com.jakewharton.timber:timber:4.1.0' compile 'com.jakewharton.timber:timber:4.1.0'
compile 'uk.co.ribot:easyadapter:1.5.0@aar' compile 'ch.acra:acra:4.8.1'
compile 'ch.acra:acra:4.7.0'
compile "frankiesardo:icepick:$ICEPICK_VERSION" compile "frankiesardo:icepick:$ICEPICK_VERSION"
provided "frankiesardo:icepick-processor:$ICEPICK_VERSION" provided "frankiesardo:icepick-processor:$ICEPICK_VERSION"
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4' compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
compile 'eu.davidea:flexible-adapter:4.2.0@aar' compile 'eu.davidea:flexible-adapter:4.2.0'
compile 'com.nononsenseapps:filepicker:2.5.1' compile 'com.nononsenseapps:filepicker:2.5.1'
compile 'com.amulyakhare:com.amulyakhare.textdrawable:1.0.1' compile 'com.github.amulyakhare:TextDrawable:558677e'
compile 'com.github.pwittchen:reactivenetwork:0.1.5'
compile "org.greenrobot:eventbus:$EVENTBUS_VERSION"
apt "org.greenrobot:eventbus-annotation-processor:$EVENTBUS_VERSION"
compile "com.google.dagger:dagger:$DAGGER_VERSION" compile "com.google.dagger:dagger:$DAGGER_VERSION"
apt "com.google.dagger:dagger-compiler:$DAGGER_VERSION" apt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
@ -127,10 +143,15 @@ dependencies {
compile('com.mikepenz:materialdrawer:4.6.4@aar') { compile('com.mikepenz:materialdrawer:4.6.4@aar') {
transitive = true transitive = true
} }
compile('com.github.afollestad.material-dialogs:core:0.8.5.3@aar') {
// Google material icons SVG.
compile 'com.mikepenz:google-material-typeface:2.1.0.1.original@aar'
compile('com.github.afollestad.material-dialogs:core:0.8.5.4@aar') {
transitive = true transitive = true
} }
testCompile 'junit:junit:4.12' testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:2.3.0' testCompile 'org.assertj:assertj-core:2.3.0'
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION" testCompile "org.mockito:mockito-core:$MOCKITO_VERSION"

View File

@ -8,9 +8,9 @@
# OkHttp # OkHttp
-keepattributes Signature -keepattributes Signature
-keepattributes *Annotation* -keepattributes *Annotation*
-keep class com.squareup.okhttp.** { *; } -keep class okhttp3.** { *; }
-keep interface com.squareup.okhttp.** { *; } -keep interface okhttp3.** { *; }
-dontwarn com.squareup.okhttp.** -dontwarn okhttp3.**
-dontwarn okio.** -dontwarn okio.**
# Okio # Okio
@ -39,14 +39,17 @@
} }
## GreenRobot EventBus specific rules ## ## GreenRobot EventBus specific rules ##
# https://github.com/greenrobot/EventBus/blob/master/HOWTO.md#proguard-configuration # http://greenrobot.org/eventbus/documentation/proguard/
-keepattributes *Annotation*
-keepclassmembers class ** { -keepclassmembers class ** {
public void onEvent*(***); @org.greenrobot.eventbus.Subscribe <methods>;
} }
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
# Don't warn for missing support classes # Only required if you use AsyncExecutor
-dontwarn de.greenrobot.event.util.*$Support -keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
-dontwarn de.greenrobot.event.util.*$SupportManagerFragment <init>(java.lang.Throwable);
}
# Glide specific rules # # Glide specific rules #
# https://github.com/bumptech/glide # https://github.com/bumptech/glide
@ -73,6 +76,27 @@
rx.internal.util.atomic.LinkedQueueNode consumerNode; rx.internal.util.atomic.LinkedQueueNode consumerNode;
} }
# Retrofit 1.X
-keep class com.squareup.okhttp.** { *; }
-keep class retrofit.** { *; }
-keep interface com.squareup.okhttp.** { *; }
-dontwarn com.squareup.okhttp.**
-dontwarn okio.**
-dontwarn retrofit.**
-dontwarn rx.**
-keepclasseswithmembers class * {
@retrofit.http.* <methods>;
}
# If in your rest service interface you use methods with Callback argument.
-keepattributes Exceptions
# If your rest service methods throw custom exceptions, because you've defined an ErrorHandler.
-keepattributes Signature
# AppCombat # AppCombat
-keep public class android.support.v7.widget.** { *; } -keep public class android.support.v7.widget.** { *; }
-keep public class android.support.v7.internal.widget.** { *; } -keep public class android.support.v7.internal.widget.** { *; }

View File

@ -5,6 +5,7 @@ import android.content.Context;
import org.acra.ACRA; import org.acra.ACRA;
import org.acra.annotation.ReportsCrashes; import org.acra.annotation.ReportsCrashes;
import org.greenrobot.eventbus.EventBus;
import eu.kanade.tachiyomi.injection.ComponentReflectionInjector; import eu.kanade.tachiyomi.injection.ComponentReflectionInjector;
import eu.kanade.tachiyomi.injection.component.AppComponent; import eu.kanade.tachiyomi.injection.component.AppComponent;
@ -23,6 +24,10 @@ public class App extends Application {
AppComponent applicationComponent; AppComponent applicationComponent;
ComponentReflectionInjector<AppComponent> componentInjector; ComponentReflectionInjector<AppComponent> componentInjector;
public static App get(Context context) {
return (App) context.getApplicationContext();
}
@Override @Override
public void onCreate() { public void onCreate() {
super.onCreate(); super.onCreate();
@ -35,23 +40,28 @@ public class App extends Application {
componentInjector = componentInjector =
new ComponentReflectionInjector<>(AppComponent.class, applicationComponent); new ComponentReflectionInjector<>(AppComponent.class, applicationComponent);
setupEventBus();
ACRA.init(this); ACRA.init(this);
} }
public static App get(Context context) { protected void setupEventBus() {
return (App) context.getApplicationContext(); EventBus.builder()
.addIndex(new EventBusIndex())
.logNoSubscriberMessages(false)
.installDefaultEventBus();
} }
public AppComponent getComponent() { public AppComponent getComponent() {
return applicationComponent; return applicationComponent;
} }
public ComponentReflectionInjector<AppComponent> getComponentReflection() {
return componentInjector;
}
// Needed to replace the component with a test specific one // Needed to replace the component with a test specific one
public void setComponent(AppComponent applicationComponent) { public void setComponent(AppComponent applicationComponent) {
this.applicationComponent = applicationComponent; this.applicationComponent = applicationComponent;
} }
public ComponentReflectionInjector<AppComponent> getComponentReflection() {
return componentInjector;
}
} }

View File

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

View File

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

View File

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

View File

@ -19,6 +19,7 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.TreeSet;
import eu.kanade.tachiyomi.data.database.models.Category; import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.data.database.models.CategorySQLiteTypeMapping; import eu.kanade.tachiyomi.data.database.models.CategorySQLiteTypeMapping;
@ -162,11 +163,11 @@ public class DatabaseHelper {
.prepare(); .prepare();
} }
public PreparedGetListOfObjects<MangaChapter> getRecentChapters() { public PreparedGetListOfObjects<MangaChapter> getRecentChapters(Date date) {
return db.get() return db.get()
.listOfObjects(MangaChapter.class) .listOfObjects(MangaChapter.class)
.withQuery(RawQuery.builder() .withQuery(RawQuery.builder()
.query(MangaChapterGetResolver.RECENT_CHAPTERS_QUERY) .query(MangaChapterGetResolver.getRecentChaptersQuery(date))
.observesTables(ChapterTable.TABLE) .observesTables(ChapterTable.TABLE)
.build()) .build())
.withGetResolver(MangaChapterGetResolver.INSTANCE) .withGetResolver(MangaChapterGetResolver.INSTANCE)
@ -259,21 +260,32 @@ public class DatabaseHelper {
int deleted = 0; int deleted = 0;
db.internal().beginTransaction(); db.internal().beginTransaction();
try { try {
TreeSet<Float> deletedReadChapterNumbers = new TreeSet<>();
if (!toDelete.isEmpty()) {
for (Chapter c : toDelete) {
if (c.read) {
deletedReadChapterNumbers.add(c.chapter_number);
}
}
deleted = deleteChapters(toDelete).executeAsBlocking().results().size();
}
if (!toAdd.isEmpty()) { if (!toAdd.isEmpty()) {
// Set the date fetch for new items in reverse order to allow another sorting method. // 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. // Sources MUST return the chapters from most to less recent, which is common.
long now = new Date().getTime(); long now = new Date().getTime();
for (int i = toAdd.size() - 1; i >= 0; i--) { for (int i = toAdd.size() - 1; i >= 0; i--) {
toAdd.get(i).date_fetch = now++; Chapter c = toAdd.get(i);
c.date_fetch = now++;
// Try to mark already read chapters as read when the source deletes them
if (c.chapter_number != -1 && deletedReadChapterNumbers.contains(c.chapter_number)) {
c.read = true;
}
} }
added = insertChapters(toAdd).executeAsBlocking().numberOfInserts(); added = insertChapters(toAdd).executeAsBlocking().numberOfInserts();
} }
if (!toDelete.isEmpty()) {
deleted = deleteChapters(toDelete).executeAsBlocking().results().size();
}
db.internal().setTransactionSuccessful(); db.internal().setTransactionSuccessful();
} finally { } finally {
db.internal().endTransaction(); db.internal().endTransaction();

View File

@ -4,8 +4,11 @@ import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType; import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
import java.io.Serializable; import java.io.Serializable;
import java.util.List;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable; 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; import eu.kanade.tachiyomi.util.UrlUtil;
@StorIOSQLiteType(table = ChapterTable.TABLE) @StorIOSQLiteType(table = ChapterTable.TABLE)
@ -40,6 +43,8 @@ public class Chapter implements Serializable {
public int status; public int status;
private transient List<Page> pages;
public Chapter() {} public Chapter() {}
public void setUrl(String url) { public void setUrl(String url) {
@ -68,4 +73,15 @@ public class Chapter implements Serializable {
return chapter; 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

@ -68,6 +68,25 @@ public class Manga implements Serializable {
public static final int COMPLETED = 2; public static final int COMPLETED = 2;
public static final int LICENSED = 3; 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 Manga() {}
public static Manga create(String pathUrl) { public static Manga create(String pathUrl) {
@ -120,6 +139,43 @@ public class Manga implements Serializable {
} }
} }
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 @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) return true;

View File

@ -5,6 +5,8 @@ import android.support.annotation.NonNull;
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver; import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver;
import java.util.Date;
import eu.kanade.tachiyomi.data.database.models.Chapter; import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.ChapterStorIOSQLiteGetResolver; import eu.kanade.tachiyomi.data.database.models.ChapterStorIOSQLiteGetResolver;
import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.Manga;
@ -24,10 +26,12 @@ public class MangaChapterGetResolver extends DefaultGetResolver<MangaChapter> {
MangaTable.COLUMN_ID, MangaTable.COLUMN_ID,
ChapterTable.COLUMN_MANGA_ID); ChapterTable.COLUMN_MANGA_ID);
public static final String RECENT_CHAPTERS_QUERY = String.format( public static String getRecentChaptersQuery(Date date) {
QUERY + " WHERE %1$s = 1 ORDER BY %2$s DESC LIMIT 100", return QUERY + String.format(" WHERE %1$s = 1 AND %2$s > %3$d ORDER BY %2$s DESC",
MangaTable.COLUMN_FAVORITE, MangaTable.COLUMN_FAVORITE,
ChapterTable.COLUMN_DATE_UPLOAD); ChapterTable.COLUMN_DATE_UPLOAD,
date.getTime());
}
@NonNull @NonNull
private final MangaStorIOSQLiteGetResolver mangaGetResolver; private final MangaStorIOSQLiteGetResolver mangaGetResolver;
@ -45,6 +49,7 @@ public class MangaChapterGetResolver extends DefaultGetResolver<MangaChapter> {
public MangaChapter mapFromCursor(@NonNull Cursor cursor) { public MangaChapter mapFromCursor(@NonNull Cursor cursor) {
final Manga manga = mangaGetResolver.mapFromCursor(cursor); final Manga manga = mangaGetResolver.mapFromCursor(cursor);
final Chapter chapter = chapterGetResolver.mapFromCursor(cursor); final Chapter chapter = chapterGetResolver.mapFromCursor(cursor);
manga.id = chapter.manga_id;
return new MangaChapter(manga, chapter); return new MangaChapter(manga, chapter);
} }

View File

@ -8,7 +8,6 @@ import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonReader;
import java.io.File; import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.FileReader; import java.io.FileReader;
import java.io.IOException; import java.io.IOException;
@ -26,6 +25,8 @@ import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.Page; import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.event.DownloadChaptersEvent; import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
import eu.kanade.tachiyomi.util.DiskUtils; import eu.kanade.tachiyomi.util.DiskUtils;
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator;
import eu.kanade.tachiyomi.util.UrlUtil;
import rx.Observable; import rx.Observable;
import rx.Subscription; import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
@ -45,6 +46,9 @@ public class DownloadManager {
private BehaviorSubject<Boolean> runningSubject; private BehaviorSubject<Boolean> runningSubject;
private Subscription downloadsSubscription; private Subscription downloadsSubscription;
private BehaviorSubject<Integer> threadsSubject;
private Subscription threadsSubscription;
private DownloadQueue queue; private DownloadQueue queue;
private volatile boolean isRunning; private volatile boolean isRunning;
@ -60,15 +64,19 @@ public class DownloadManager {
downloadsQueueSubject = PublishSubject.create(); downloadsQueueSubject = PublishSubject.create();
runningSubject = BehaviorSubject.create(); runningSubject = BehaviorSubject.create();
threadsSubject = BehaviorSubject.create();
} }
private void initializeSubscriptions() { private void initializeSubscriptions() {
if (downloadsSubscription != null && !downloadsSubscription.isUnsubscribed()) if (downloadsSubscription != null && !downloadsSubscription.isUnsubscribed())
downloadsSubscription.unsubscribe(); downloadsSubscription.unsubscribe();
threadsSubscription = preferences.downloadThreads().asObservable()
.subscribe(threadsSubject::onNext);
downloadsSubscription = downloadsQueueSubject downloadsSubscription = downloadsQueueSubject
.concatMap(downloads -> Observable.from(downloads) .flatMap(Observable::from)
.flatMap(this::downloadChapter, preferences.downloadThreads())) .lift(new DynamicConcurrentMergeOperator<>(this::downloadChapter, threadsSubject))
.onBackpressureBuffer() .onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.map(download -> areAllDownloadsFinished()) .map(download -> areAllDownloadsFinished())
@ -76,7 +84,7 @@ public class DownloadManager {
if (finished) { if (finished) {
DownloadService.stop(context); DownloadService.stop(context);
} }
}, e -> Timber.e(e.getCause(), e.getMessage())); }, e -> DownloadService.stop(context));
if (!isRunning) { if (!isRunning) {
isRunning = true; isRunning = true;
@ -94,6 +102,11 @@ public class DownloadManager {
downloadsSubscription.unsubscribe(); downloadsSubscription.unsubscribe();
downloadsSubscription = null; downloadsSubscription = null;
} }
if (threadsSubscription != null && !threadsSubscription.isUnsubscribed()) {
threadsSubscription.unsubscribe();
}
} }
// Create a download object for every chapter in the event and add them to the downloads queue // Create a download object for every chapter in the event and add them to the downloads queue
@ -181,8 +194,7 @@ public class DownloadManager {
// 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); Observable.just(download.pages);
return pageListObservable return Observable.defer(() -> pageListObservable
.subscribeOn(Schedulers.io())
.doOnNext(pages -> { .doOnNext(pages -> {
download.downloadedImages = 0; download.downloadedImages = 0;
download.setStatus(Download.DOWNLOADING); download.setStatus(Download.DOWNLOADING);
@ -199,7 +211,8 @@ public class DownloadManager {
.onErrorResumeNext(error -> { .onErrorResumeNext(error -> {
download.setStatus(Download.ERROR); download.setStatus(Download.ERROR);
return Observable.just(download); return Observable.just(download);
}); }))
.subscribeOn(Schedulers.io());
} }
// Get the image from the filesystem if it exists or download from network // Get the image from the filesystem if it exists or download from network
@ -271,6 +284,15 @@ public class DownloadManager {
// Get the filename for an image given the page // Get the filename for an image given the page
private String getImageFilename(Page page) { private String getImageFilename(Page page) {
String url = page.getImageUrl(); String url = page.getImageUrl();
int number = page.getPageNumber() + 1;
// Try to preserve file extension
if (UrlUtil.isJpg(url)) {
return number + ".jpg";
} else if (UrlUtil.isPng(url)) {
return number + ".png";
} else if (UrlUtil.isGif(url)) {
return number + ".gif";
}
return Uri.parse(url).getLastPathSegment().replaceAll("[^\\sa-zA-Z0-9.-]", "_"); return Uri.parse(url).getLastPathSegment().replaceAll("[^\\sa-zA-Z0-9.-]", "_");
} }
@ -306,7 +328,6 @@ public class DownloadManager {
// Return the page list from the chapter's directory if it exists, null otherwise // Return the page list from the chapter's directory if it exists, null otherwise
public List<Page> getSavedPageList(Source source, Manga manga, Chapter chapter) { public List<Page> getSavedPageList(Source source, Manga manga, Chapter chapter) {
List<Page> pages = null;
File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter); File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter);
File pagesFile = new File(chapterDir, PAGE_LIST_FILE); File pagesFile = new File(chapterDir, PAGE_LIST_FILE);
@ -315,14 +336,14 @@ public class DownloadManager {
if (pagesFile.exists()) { if (pagesFile.exists()) {
reader = new JsonReader(new FileReader(pagesFile.getAbsolutePath())); reader = new JsonReader(new FileReader(pagesFile.getAbsolutePath()));
Type collectionType = new TypeToken<List<Page>>() {}.getType(); Type collectionType = new TypeToken<List<Page>>() {}.getType();
pages = gson.fromJson(reader, collectionType); return gson.fromJson(reader, collectionType);
} }
} catch (FileNotFoundException e) { } catch (Exception e) {
Timber.e(e.getCause(), e.getMessage()); Timber.e(e.getCause(), e.getMessage());
} finally { } finally {
if (reader != null) try { reader.close(); } catch (IOException e) { /* Do nothing */ } if (reader != null) try { reader.close(); } catch (IOException e) { /* Do nothing */ }
} }
return pages; return null;
} }
// Shortcut for the method above // Shortcut for the method above

View File

@ -8,14 +8,16 @@ import android.os.PowerManager;
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork; import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import javax.inject.Inject; import javax.inject.Inject;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.App; import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper; import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.event.DownloadChaptersEvent; import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
import eu.kanade.tachiyomi.util.EventBusHook;
import eu.kanade.tachiyomi.util.ToastUtil; import eu.kanade.tachiyomi.util.ToastUtil;
import rx.Subscription; import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
@ -47,7 +49,7 @@ public class DownloadService extends Service {
createWakeLock(); createWakeLock();
listenQueueRunningChanges(); listenQueueRunningChanges();
EventBus.getDefault().registerSticky(this); EventBus.getDefault().register(this);
listenNetworkChanges(); listenNetworkChanges();
} }
@ -71,7 +73,7 @@ public class DownloadService extends Service {
return null; return null;
} }
@EventBusHook @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEvent(DownloadChaptersEvent event) { public void onEvent(DownloadChaptersEvent event) {
EventBus.getDefault().removeStickyEvent(event); EventBus.getDefault().removeStickyEvent(event);
downloadManager.onDownloadChaptersEvent(event); downloadManager.onDownloadChaptersEvent(event);

View File

@ -43,11 +43,11 @@ public class DownloadQueue extends ArrayList<Download> {
} }
public Observable<Download> getStatusObservable() { public Observable<Download> getStatusObservable() {
return statusSubject; return statusSubject.onBackpressureBuffer();
} }
public Observable<Download> getProgressObservable() { public Observable<Download> getProgressObservable() {
return statusSubject return statusSubject.onBackpressureBuffer()
.startWith(getActiveDownloads()) .startWith(getActiveDownloads())
.flatMap(download -> { .flatMap(download -> {
if (download.getStatus() == Download.DOWNLOADING) { if (download.getStatus() == Download.DOWNLOADING) {

View File

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

View File

@ -1,8 +1,7 @@
package eu.kanade.tachiyomi.data.mangasync.base; package eu.kanade.tachiyomi.data.mangasync.base;
import com.squareup.okhttp.Response;
import eu.kanade.tachiyomi.data.database.models.MangaSync; import eu.kanade.tachiyomi.data.database.models.MangaSync;
import okhttp3.Response;
import rx.Observable; import rx.Observable;
public abstract class MangaSyncService { public abstract class MangaSyncService {

View File

@ -4,12 +4,6 @@ import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.util.Xml; import android.util.Xml;
import com.squareup.okhttp.Credentials;
import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.xmlpull.v1.XmlSerializer; import org.xmlpull.v1.XmlSerializer;
@ -26,6 +20,11 @@ import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService; import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import eu.kanade.tachiyomi.data.network.NetworkHelper; import eu.kanade.tachiyomi.data.network.NetworkHelper;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper; import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import okhttp3.Credentials;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.RequestBody;
import okhttp3.Response;
import rx.Observable; import rx.Observable;
public class MyAnimeList extends MangaSyncService { public class MyAnimeList extends MangaSyncService {
@ -84,7 +83,7 @@ public class MyAnimeList extends MangaSyncService {
public Observable<Boolean> login(String username, String password) { public Observable<Boolean> login(String username, String password) {
createHeaders(username, password); createHeaders(username, password);
return networkService.getResponse(getLoginUrl(), headers, null) return networkService.getResponse(getLoginUrl(), headers, false)
.map(response -> response.code() == 200); .map(response -> response.code() == 200);
} }
@ -102,7 +101,7 @@ public class MyAnimeList extends MangaSyncService {
} }
public Observable<List<MangaSync>> search(String query) { public Observable<List<MangaSync>> search(String query) {
return networkService.getStringResponse(getSearchUrl(query), headers, null) return networkService.getStringResponse(getSearchUrl(query), headers, true)
.map(Jsoup::parse) .map(Jsoup::parse)
.flatMap(doc -> Observable.from(doc.select("entry"))) .flatMap(doc -> Observable.from(doc.select("entry")))
.filter(entry -> !entry.select("type").text().equals("Novel")) .filter(entry -> !entry.select("type").text().equals("Novel"))
@ -127,7 +126,7 @@ public class MyAnimeList extends MangaSyncService {
public Observable<List<MangaSync>> getList() { public Observable<List<MangaSync>> getList() {
// TODO cache this list for a few minutes // TODO cache this list for a few minutes
return networkService.getStringResponse(getListUrl(username), headers, null) return networkService.getStringResponse(getListUrl(username), headers, true)
.map(Jsoup::parse) .map(Jsoup::parse)
.flatMap(doc -> Observable.from(doc.select("manga"))) .flatMap(doc -> Observable.from(doc.select("manga")))
.map(entry -> { .map(entry -> {
@ -209,7 +208,7 @@ public class MyAnimeList extends MangaSyncService {
xml.endTag("", ENTRY_TAG); xml.endTag("", ENTRY_TAG);
xml.endDocument(); xml.endDocument();
FormEncodingBuilder form = new FormEncodingBuilder(); FormBody.Builder form = new FormBody.Builder();
form.add("data", writer.toString()); form.add("data", writer.toString());
return form.build(); return form.build();
} }

View File

@ -1,46 +1,78 @@
package eu.kanade.tachiyomi.data.network; package eu.kanade.tachiyomi.data.network;
import com.squareup.okhttp.CacheControl; import android.content.Context;
import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.OkHttpClient;
import com.squareup.okhttp.Request;
import com.squareup.okhttp.RequestBody;
import com.squareup.okhttp.Response;
import java.io.File;
import java.net.CookieManager; import java.net.CookieManager;
import java.net.CookiePolicy; import java.net.CookiePolicy;
import java.net.CookieStore; import java.net.CookieStore;
import java.util.concurrent.TimeUnit;
import okhttp3.Cache;
import okhttp3.CacheControl;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Interceptor;
import okhttp3.JavaNetCookieJar;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;
import rx.Observable; import rx.Observable;
public final class NetworkHelper { public final class NetworkHelper {
private OkHttpClient client; private OkHttpClient client;
private OkHttpClient forceCacheClient;
private CookieManager cookieManager; private CookieManager cookieManager;
public final CacheControl NULL_CACHE_CONTROL = new CacheControl.Builder().noCache().build();
public final Headers NULL_HEADERS = new Headers.Builder().build(); public final Headers NULL_HEADERS = new Headers.Builder().build();
public final RequestBody NULL_REQUEST_BODY = new FormEncodingBuilder().build(); public final RequestBody NULL_REQUEST_BODY = new FormBody.Builder().build();
public final CacheControl CACHE_CONTROL = new CacheControl.Builder()
.maxAge(10, TimeUnit.MINUTES)
.build();
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = chain -> {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.removeHeader("Pragma")
.header("Cache-Control", "max-age=" + 600)
.build();
};
private static final int CACHE_SIZE = 5 * 1024 * 1024; // 5 MiB
private static final String CACHE_DIR_NAME = "network_cache";
public NetworkHelper(Context context) {
File cacheDir = new File(context.getCacheDir(), CACHE_DIR_NAME);
public NetworkHelper() {
client = new OkHttpClient();
cookieManager = new CookieManager(); cookieManager = new CookieManager();
cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL); cookieManager.setCookiePolicy(CookiePolicy.ACCEPT_ALL);
client.setCookieHandler(cookieManager);
client = new OkHttpClient.Builder()
.cookieJar(new JavaNetCookieJar(cookieManager))
.cache(new Cache(cacheDir, CACHE_SIZE))
.build();
forceCacheClient = client.newBuilder()
.addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
.build();
} }
public Observable<Response> getResponse(final String url, final Headers headers, final CacheControl cacheControl) { public Observable<Response> getResponse(final String url, final Headers headers, boolean forceCache) {
return Observable.defer(() -> { return Observable.defer(() -> {
try { try {
OkHttpClient c = forceCache ? forceCacheClient : client;
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .url(url)
.cacheControl(cacheControl != null ? cacheControl : NULL_CACHE_CONTROL)
.headers(headers != null ? headers : NULL_HEADERS) .headers(headers != null ? headers : NULL_HEADERS)
.cacheControl(CACHE_CONTROL)
.build(); .build();
return Observable.just(client.newCall(request).execute()); return Observable.just(c.newCall(request).execute());
} catch (Throwable e) { } catch (Throwable e) {
return Observable.error(e); return Observable.error(e);
} }
@ -57,8 +89,8 @@ public final class NetworkHelper {
}); });
} }
public Observable<String> getStringResponse(final String url, final Headers headers, final CacheControl cacheControl) { public Observable<String> getStringResponse(final String url, final Headers headers, boolean forceCache) {
return getResponse(url, headers, cacheControl) return getResponse(url, headers, forceCache)
.flatMap(this::mapResponseToString); .flatMap(this::mapResponseToString);
} }
@ -82,23 +114,24 @@ public final class NetworkHelper {
try { try {
Request request = new Request.Builder() Request request = new Request.Builder()
.url(url) .url(url)
.cacheControl(NULL_CACHE_CONTROL) .cacheControl(CacheControl.FORCE_NETWORK)
.headers(headers != null ? headers : NULL_HEADERS) .headers(headers != null ? headers : NULL_HEADERS)
.build(); .build();
OkHttpClient progressClient = client.clone(); OkHttpClient progressClient = client.newBuilder()
.cache(null)
.addNetworkInterceptor(chain -> {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new ProgressResponseBody(originalResponse.body(), listener))
.build();
}).build();
progressClient.networkInterceptors().add(chain -> {
Response originalResponse = chain.proceed(chain.request());
return originalResponse.newBuilder()
.body(new ProgressResponseBody(originalResponse.body(), listener))
.build();
});
return Observable.just(progressClient.newCall(request).execute()); return Observable.just(progressClient.newCall(request).execute());
} catch (Throwable e) { } catch (Throwable e) {
return Observable.error(e); return Observable.error(e);
} }
}).retry(2); }).retry(1);
} }
public CookieStore getCookies() { public CookieStore getCookies() {

View File

@ -1,10 +1,9 @@
package eu.kanade.tachiyomi.data.network; package eu.kanade.tachiyomi.data.network;
import com.squareup.okhttp.MediaType;
import com.squareup.okhttp.ResponseBody;
import java.io.IOException; import java.io.IOException;
import okhttp3.MediaType;
import okhttp3.ResponseBody;
import okio.Buffer; import okio.Buffer;
import okio.BufferedSource; import okio.BufferedSource;
import okio.ForwardingSource; import okio.ForwardingSource;
@ -26,11 +25,11 @@ public class ProgressResponseBody extends ResponseBody {
return responseBody.contentType(); return responseBody.contentType();
} }
@Override public long contentLength() throws IOException { @Override public long contentLength() {
return responseBody.contentLength(); return responseBody.contentLength();
} }
@Override public BufferedSource source() throws IOException { @Override public BufferedSource source() {
if (bufferedSource == null) { if (bufferedSource == null) {
bufferedSource = Okio.buffer(source(responseBody.source())); bufferedSource = Okio.buffer(source(responseBody.source()));
} }
@ -40,6 +39,7 @@ public class ProgressResponseBody extends ResponseBody {
private Source source(Source source) { private Source source(Source source) {
return new ForwardingSource(source) { return new ForwardingSource(source) {
long totalBytesRead = 0L; long totalBytesRead = 0L;
@Override public long read(Buffer sink, long byteCount) throws IOException { @Override public long read(Buffer sink, long byteCount) throws IOException {
long bytesRead = super.read(sink, byteCount); long bytesRead = super.read(sink, byteCount);
// read() returns the number of bytes read, or -1 if this source is exhausted. // read() returns the number of bytes read, or -1 if this source is exhausted.

View File

@ -42,10 +42,12 @@ public class PreferencesHelper {
if (getDownloadsDirectory().equals(defaultDownloadsDir.getAbsolutePath()) && if (getDownloadsDirectory().equals(defaultDownloadsDir.getAbsolutePath()) &&
!defaultDownloadsDir.exists()) { !defaultDownloadsDir.exists()) {
defaultDownloadsDir.mkdirs(); defaultDownloadsDir.mkdirs();
try {
new File(defaultDownloadsDir, ".nomedia").createNewFile();
} catch (IOException e) { /* Ignore */ }
} }
// Don't display downloaded chapters in gallery apps creating a ".nomedia" file
try {
new File(getDownloadsDirectory(), ".nomedia").createNewFile();
} catch (IOException e) { /* Ignore */ }
} }
private String getKey(int keyResource) { private String getKey(int keyResource) {
@ -56,8 +58,8 @@ public class PreferencesHelper {
prefs.edit().clear().apply(); prefs.edit().clear().apply();
} }
public Preference<Boolean> lockOrientation() { public Preference<Integer> rotation() {
return rxPrefs.getBoolean(getKey(R.string.pref_lock_orientation_key), true); return rxPrefs.getInteger(getKey(R.string.pref_rotation_type_key), 1);
} }
public Preference<Boolean> enableTransitions() { public Preference<Boolean> enableTransitions() {
@ -88,6 +90,22 @@ public class PreferencesHelper {
return prefs.getInt(getKey(R.string.pref_default_viewer_key), 1); return prefs.getInt(getKey(R.string.pref_default_viewer_key), 1);
} }
public Preference<Integer> imageScaleType() {
return rxPrefs.getInteger(getKey(R.string.pref_image_scale_type_key), 1);
}
public Preference<Integer> imageDecoder() {
return rxPrefs.getInteger(getKey(R.string.pref_image_decoder_key), 0);
}
public Preference<Integer> zoomStart() {
return rxPrefs.getInteger(getKey(R.string.pref_zoom_start_key), 1);
}
public Preference<Integer> readerTheme() {
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0);
}
public Preference<Integer> portraitColumns() { public Preference<Integer> portraitColumns() {
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_portrait_key), 0); return rxPrefs.getInteger(getKey(R.string.pref_library_columns_portrait_key), 0);
} }
@ -108,12 +126,16 @@ public class PreferencesHelper {
return prefs.getBoolean(getKey(R.string.pref_ask_update_manga_sync_key), false); return prefs.getBoolean(getKey(R.string.pref_ask_update_manga_sync_key), false);
} }
public Preference<Integer> imageDecoder() { public Preference<Integer> lastUsedCatalogueSource() {
return rxPrefs.getInteger(getKey(R.string.pref_image_decoder_key), 0); return rxPrefs.getInteger(getKey(R.string.pref_last_catalogue_source_key), -1);
} }
public Preference<Integer> readerTheme() { public boolean seamlessMode() {
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0); return prefs.getBoolean(getKey(R.string.pref_seamless_mode_key), true);
}
public Preference<Boolean> catalogueAsList() {
return rxPrefs.getBoolean(getKey(R.string.pref_display_catalogue_as_list), false);
} }
public String getSourceUsername(Source source) { public String getSourceUsername(Source source) {
@ -155,8 +177,8 @@ public class PreferencesHelper {
prefs.edit().putString(getKey(R.string.pref_download_directory_key), path).apply(); prefs.edit().putString(getKey(R.string.pref_download_directory_key), path).apply();
} }
public int downloadThreads() { public Preference<Integer> downloadThreads() {
return prefs.getInt(getKey(R.string.pref_download_slots_key), 1); return rxPrefs.getInteger(getKey(R.string.pref_download_slots_key), 1);
} }
public boolean downloadOnlyOverWifi() { public boolean downloadOnlyOverWifi() {

View File

@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.data.rest;
import retrofit.http.GET;
import rx.Observable;
/**
* Used to connect with the Github API
*/
public interface GithubService {
String SERVICE_ENDPOINT = "https://api.github.com";
@GET("/repos/inorichi/tachiyomi/releases/latest") Observable<Release> getLatestVersion();
}

View File

@ -0,0 +1,93 @@
package eu.kanade.tachiyomi.data.rest;
import com.google.gson.annotations.SerializedName;
import java.util.List;
/**
* Release object
* Contains information about the latest release
*/
public class Release {
/**
* Version name V0.0.0
*/
@SerializedName("tag_name")
private final String version;
/** Change Log */
@SerializedName("body")
private final String log;
/** Assets containing download url */
@SerializedName("assets")
private final List<Assets> assets;
/**
* Release constructor
*
* @param version version of latest release
* @param log log of latest release
* @param assets assets of latest release
*/
public Release(String version, String log, List<Assets> assets) {
this.version = version;
this.log = log;
this.assets = assets;
}
/**
* Get latest release version
*
* @return latest release version
*/
public String getVersion() {
return version;
}
/**
* Get change log of latest release
*
* @return change log of latest release
*/
public String getChangeLog() {
return log;
}
/**
* Get download link of latest release
*
* @return download link of latest release
*/
public String getDownloadLink() {
return assets.get(0).getDownloadLink();
}
/**
* Assets class containing download url
*/
class Assets {
@SerializedName("browser_download_url")
private final String download_url;
/**
* Assets Constructor
*
* @param download_url download url
*/
@SuppressWarnings("unused") public Assets(String download_url) {
this.download_url = download_url;
}
/**
* Get download link of latest release
*
* @return download link of latest release
*/
public String getDownloadLink() {
return download_url;
}
}
}

View File

@ -0,0 +1,21 @@
package eu.kanade.tachiyomi.data.rest;
import retrofit.RestAdapter;
public class ServiceFactory {
/**
* Creates a retrofit service from an arbitrary class (clazz)
*
* @param clazz Java interface of the retrofit service
* @param endPoint REST endpoint url
* @return retrofit service with defined endpoint
*/
public static <T> T createRetrofitService(final Class<T> clazz, final String endPoint) {
final RestAdapter restAdapter = new RestAdapter.Builder()
.setEndpoint(endPoint)
.build();
return restAdapter.create(clazz);
}
}

View File

@ -1,8 +1,5 @@
package eu.kanade.tachiyomi.data.source.base; package eu.kanade.tachiyomi.data.source.base;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Response;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import java.util.List; import java.util.List;
@ -10,6 +7,8 @@ import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter; import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.model.MangasPage; import eu.kanade.tachiyomi.data.source.model.MangasPage;
import okhttp3.Headers;
import okhttp3.Response;
import rx.Observable; import rx.Observable;
public abstract class BaseSource { public abstract class BaseSource {

View File

@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.data.source.base;
import android.content.Context; import android.content.Context;
import com.bumptech.glide.load.model.LazyHeaders; import com.bumptech.glide.load.model.LazyHeaders;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Response;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
@ -23,6 +21,8 @@ import eu.kanade.tachiyomi.data.network.NetworkHelper;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper; import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.data.source.model.MangasPage; import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page; import eu.kanade.tachiyomi.data.source.model.Page;
import okhttp3.Headers;
import okhttp3.Response;
import rx.Observable; import rx.Observable;
import rx.schedulers.Schedulers; import rx.schedulers.Schedulers;
@ -53,7 +53,7 @@ public abstract class Source extends BaseSource {
page.url = getInitialPopularMangasUrl(); page.url = getInitialPopularMangasUrl();
return networkService return networkService
.getStringResponse(page.url, requestHeaders, null) .getStringResponse(page.url, requestHeaders, true)
.map(Jsoup::parse) .map(Jsoup::parse)
.doOnNext(doc -> page.mangas = parsePopularMangasFromHtml(doc)) .doOnNext(doc -> page.mangas = parsePopularMangasFromHtml(doc))
.doOnNext(doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page)) .doOnNext(doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page))
@ -66,7 +66,7 @@ public abstract class Source extends BaseSource {
page.url = getInitialSearchUrl(query); page.url = getInitialSearchUrl(query);
return networkService return networkService
.getStringResponse(page.url, requestHeaders, null) .getStringResponse(page.url, requestHeaders, true)
.map(Jsoup::parse) .map(Jsoup::parse)
.doOnNext(doc -> page.mangas = parseSearchFromHtml(doc)) .doOnNext(doc -> page.mangas = parseSearchFromHtml(doc))
.doOnNext(doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query)) .doOnNext(doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query))
@ -76,14 +76,14 @@ public abstract class Source extends BaseSource {
// Get manga details from the source // Get manga details from the source
public Observable<Manga> pullMangaFromNetwork(final String mangaUrl) { public Observable<Manga> pullMangaFromNetwork(final String mangaUrl) {
return networkService return networkService
.getStringResponse(getBaseUrl() + overrideMangaUrl(mangaUrl), requestHeaders, null) .getStringResponse(getBaseUrl() + overrideMangaUrl(mangaUrl), requestHeaders, true)
.flatMap(unparsedHtml -> Observable.just(parseHtmlToManga(mangaUrl, unparsedHtml))); .flatMap(unparsedHtml -> Observable.just(parseHtmlToManga(mangaUrl, unparsedHtml)));
} }
// Get chapter list of a manga from the source // Get chapter list of a manga from the source
public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) { public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
return networkService return networkService
.getStringResponse(getBaseUrl() + mangaUrl, requestHeaders, null) .getStringResponse(getBaseUrl() + mangaUrl, requestHeaders, false)
.flatMap(unparsedHtml -> { .flatMap(unparsedHtml -> {
List<Chapter> chapters = parseHtmlToChapters(unparsedHtml); List<Chapter> chapters = parseHtmlToChapters(unparsedHtml);
return !chapters.isEmpty() ? return !chapters.isEmpty() ?
@ -93,7 +93,7 @@ public abstract class Source extends BaseSource {
} }
public Observable<List<Page>> getCachedPageListOrPullFromNetwork(final String chapterUrl) { public Observable<List<Page>> getCachedPageListOrPullFromNetwork(final String chapterUrl) {
return chapterCache.getPageUrlsFromDiskCache(getChapterCacheKey(chapterUrl)) return chapterCache.getPageListFromCache(getChapterCacheKey(chapterUrl))
.onErrorResumeNext(throwable -> { .onErrorResumeNext(throwable -> {
return pullPageListFromNetwork(chapterUrl); return pullPageListFromNetwork(chapterUrl);
}) })
@ -102,7 +102,7 @@ public abstract class Source extends BaseSource {
public Observable<List<Page>> pullPageListFromNetwork(final String chapterUrl) { public Observable<List<Page>> pullPageListFromNetwork(final String chapterUrl) {
return networkService return networkService
.getStringResponse(getBaseUrl() + overrideChapterUrl(chapterUrl), requestHeaders, null) .getStringResponse(getBaseUrl() + overrideChapterUrl(chapterUrl), requestHeaders, false)
.flatMap(unparsedHtml -> { .flatMap(unparsedHtml -> {
List<Page> pages = convertToPages(parseHtmlToPageUrls(unparsedHtml)); List<Page> pages = convertToPages(parseHtmlToPageUrls(unparsedHtml));
return !pages.isEmpty() ? return !pages.isEmpty() ?
@ -127,7 +127,7 @@ public abstract class Source extends BaseSource {
public Observable<Page> getImageUrlFromPage(final Page page) { public Observable<Page> getImageUrlFromPage(final Page page) {
page.setStatus(Page.LOAD_PAGE); page.setStatus(Page.LOAD_PAGE);
return networkService return networkService
.getStringResponse(overridePageUrl(page.getUrl()), requestHeaders, null) .getStringResponse(overridePageUrl(page.getUrl()), requestHeaders, false)
.flatMap(unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml))) .flatMap(unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)))
.onErrorResumeNext(e -> { .onErrorResumeNext(e -> {
page.setStatus(Page.ERROR); page.setStatus(Page.ERROR);
@ -168,7 +168,7 @@ public abstract class Source extends BaseSource {
return getImageProgressResponse(page) return getImageProgressResponse(page)
.flatMap(resp -> { .flatMap(resp -> {
try { try {
chapterCache.putImageToDiskCache(page.getImageUrl(), resp); chapterCache.putImageToCache(page.getImageUrl(), resp);
} catch (IOException e) { } catch (IOException e) {
return Observable.error(e); return Observable.error(e);
} }
@ -182,7 +182,7 @@ public abstract class Source extends BaseSource {
public void savePageList(String chapterUrl, List<Page> pages) { public void savePageList(String chapterUrl, List<Page> pages) {
if (pages != null) if (pages != null)
chapterCache.putPageUrlsToDiskCache(getChapterCacheKey(chapterUrl), pages); chapterCache.putPageListToCache(getChapterCacheKey(chapterUrl), pages);
} }
protected List<Page> convertToPages(List<String> pageUrls) { protected List<Page> convertToPages(List<String> pageUrls) {

View File

@ -1,5 +1,9 @@
package eu.kanade.tachiyomi.data.source.model; package eu.kanade.tachiyomi.data.source.model;
import java.io.Serializable;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.network.ProgressListener; import eu.kanade.tachiyomi.data.network.ProgressListener;
import rx.subjects.PublishSubject; import rx.subjects.PublishSubject;
@ -8,6 +12,7 @@ public class Page implements ProgressListener {
private int pageNumber; private int pageNumber;
private String url; private String url;
private String imageUrl; private String imageUrl;
private transient Chapter chapter;
private transient String imagePath; private transient String imagePath;
private transient volatile int status; private transient volatile int status;
private transient volatile int progress; private transient volatile int progress;
@ -82,4 +87,16 @@ public class Page implements ProgressListener {
this.statusSubject = subject; this.statusSubject = subject;
} }
public Chapter getChapter() {
return chapter;
}
public void setChapter(Chapter chapter) {
this.chapter = chapter;
}
public boolean isLastPage() {
List<Page> chapterPages = chapter.getPages();
return chapterPages.size() -1 == pageNumber;
}
} }

View File

@ -2,12 +2,9 @@ package eu.kanade.tachiyomi.data.source.online.english;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import android.text.Html;
import android.text.TextUtils; import android.text.TextUtils;
import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Response;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
@ -35,6 +32,9 @@ import eu.kanade.tachiyomi.data.source.base.LoginSource;
import eu.kanade.tachiyomi.data.source.model.MangasPage; import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page; import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser; import eu.kanade.tachiyomi.util.Parser;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Response;
import rx.Observable; import rx.Observable;
public class Batoto extends LoginSource { public class Batoto extends LoginSource {
@ -48,6 +48,8 @@ public class Batoto extends LoginSource {
public static final String MANGA_URL = "/comic_pop?id=%s"; public static final String MANGA_URL = "/comic_pop?id=%s";
public static final String LOGIN_URL = BASE_URL + "/forums/index.php?app=core&module=global&section=login"; public static final String LOGIN_URL = BASE_URL + "/forums/index.php?app=core&module=global&section=login";
public static final Pattern staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE);
private Pattern datePattern; private Pattern datePattern;
private Map<String, Integer> dateFields; private Map<String, Integer> dateFields;
@ -205,6 +207,12 @@ public class Batoto extends LoginSource {
@Override @Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) { protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
Matcher matcher = staffNotice.matcher(unparsedHtml);
if (matcher.find()) {
String notice = Html.fromHtml(matcher.group(1)).toString().trim();
throw new RuntimeException(notice);
}
Document parsedDocument = Jsoup.parse(unparsedHtml); Document parsedDocument = Jsoup.parse(unparsedHtml);
List<Chapter> chapterList = new ArrayList<>(); List<Chapter> chapterList = new ArrayList<>();
@ -235,6 +243,7 @@ public class Batoto extends LoginSource {
return chapter; return chapter;
} }
@SuppressWarnings("WrongConstant")
private long parseDateFromElement(Element dateElement) { private long parseDateFromElement(Element dateElement) {
String dateAsString = dateElement.text(); String dateAsString = dateElement.text();
@ -250,7 +259,6 @@ public class Batoto extends LoginSource {
String unit = m.group(2); String unit = m.group(2);
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
// Not an error
cal.add(dateFields.get(unit), -amount); cal.add(dateFields.get(unit), -amount);
date = cal.getTime(); date = cal.getTime();
} else { } else {
@ -310,7 +318,7 @@ public class Batoto extends LoginSource {
@Override @Override
public Observable<Boolean> login(String username, String password) { public Observable<Boolean> login(String username, String password) {
return networkService.getStringResponse(LOGIN_URL, requestHeaders, null) return networkService.getStringResponse(LOGIN_URL, requestHeaders, false)
.flatMap(response -> doLogin(response, username, password)) .flatMap(response -> doLogin(response, username, password))
.map(this::isAuthenticationSuccessful); .map(this::isAuthenticationSuccessful);
} }
@ -320,7 +328,7 @@ public class Batoto extends LoginSource {
Element form = doc.select("#login").first(); Element form = doc.select("#login").first();
String postUrl = form.attr("action"); String postUrl = form.attr("action");
FormEncodingBuilder formBody = new FormEncodingBuilder(); FormBody.Builder formBody = new FormBody.Builder();
Element authKey = form.select("input[name=auth_key]").first(); Element authKey = form.select("input[name=auth_key]").first();
formBody.add(authKey.attr("name"), authKey.attr("value")); formBody.add(authKey.attr("name"), authKey.attr("value"));
@ -354,8 +362,13 @@ public class Batoto extends LoginSource {
@Override @Override
public Observable<List<Chapter>> pullChaptersFromNetwork(String mangaUrl) { public Observable<List<Chapter>> pullChaptersFromNetwork(String mangaUrl) {
Observable<List<Chapter>> observable; Observable<List<Chapter>> observable;
if (!isLogged()) { String username = prefs.getSourceUsername(this);
observable = login(prefs.getSourceUsername(this), prefs.getSourcePassword(this)) String password = prefs.getSourcePassword(this);
if (username.isEmpty() && password.isEmpty()) {
observable = Observable.error(new Exception("User not logged"));
}
else if (!isLogged()) {
observable = login(username, password)
.flatMap(result -> super.pullChaptersFromNetwork(mangaUrl)); .flatMap(result -> super.pullChaptersFromNetwork(mangaUrl));
} }
else { else {

View File

@ -3,10 +3,6 @@ package eu.kanade.tachiyomi.data.source.online.english;
import android.content.Context; import android.content.Context;
import android.net.Uri; import android.net.Uri;
import com.squareup.okhttp.FormEncodingBuilder;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Response;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
@ -26,6 +22,9 @@ import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage; import eu.kanade.tachiyomi.data.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page; import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.util.Parser; import eu.kanade.tachiyomi.util.Parser;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Response;
import rx.Observable; import rx.Observable;
public class Kissmanga extends Source { public class Kissmanga extends Source {
@ -109,7 +108,7 @@ public class Kissmanga extends Source {
if (page.page == 1) if (page.page == 1)
page.url = getInitialSearchUrl(query); page.url = getInitialSearchUrl(query);
FormEncodingBuilder form = new FormEncodingBuilder(); FormBody.Builder form = new FormBody.Builder();
form.add("authorArtist", ""); form.add("authorArtist", "");
form.add("mangaName", query); form.add("mangaName", query);
form.add("status", ""); form.add("status", "");

View File

@ -29,7 +29,7 @@ public class Mangafox extends Source {
public static final String BASE_URL = "http://mangafox.me"; public static final String BASE_URL = "http://mangafox.me";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s"; public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
public static final String SEARCH_URL = public static final String SEARCH_URL =
BASE_URL + "/search.php?name_method=cw&advopts=1&order=az&sort=name&name=%s&page=%s"; BASE_URL + "/search.php?name_method=cw&advopts=1&order=za&sort=views&name=%s&page=%s";
public Mangafox(Context context) { public Mangafox(Context context) {
super(context); super(context);

View File

@ -28,7 +28,7 @@ public class Mangahere extends Source {
public static final String NAME = "Mangahere (EN)"; public static final String NAME = "Mangahere (EN)";
public static final String BASE_URL = "http://www.mangahere.co"; public static final String BASE_URL = "http://www.mangahere.co";
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s"; public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
public static final String SEARCH_URL = BASE_URL + "/search.php?name=%s&page=%s"; public static final String SEARCH_URL = BASE_URL + "/search.php?name=%s&page=%s&sort=views&order=za";
public Mangahere(Context context) { public Mangahere(Context context) {
super(context); super(context);

View File

@ -111,7 +111,7 @@ public class LibraryUpdateService extends Service {
.toList().toBlocking().single(); .toList().toBlocking().single();
return Observable.from(mangas) return Observable.from(mangas)
.doOnNext(manga -> showNotification( .doOnNext(manga -> showProgressNotification(
getString(R.string.notification_update_progress, getString(R.string.notification_update_progress,
count.incrementAndGet(), mangas.size()), manga.title)) count.incrementAndGet(), mangas.size()), manga.title))
.concatMap(manga -> updateManga(manga) .concatMap(manga -> updateManga(manga)
@ -123,8 +123,14 @@ public class LibraryUpdateService extends Service {
.filter(pair -> pair.first > 0) .filter(pair -> pair.first > 0)
.map(pair -> new MangaUpdate(manga, pair.first))) .map(pair -> new MangaUpdate(manga, pair.first)))
.doOnNext(updates::add) .doOnNext(updates::add)
.doOnCompleted(() -> showBigNotification(getString(R.string.notification_update_completed), .doOnCompleted(() -> {
getUpdatedMangas(updates, failedUpdates))); if (updates.isEmpty()) {
cancelNotification();
} else {
showResultNotification(getString(R.string.notification_update_completed),
getUpdatedMangasResult(updates, failedUpdates));
}
});
} }
private Observable<Pair<Integer, Integer>> updateManga(Manga manga) { private Observable<Pair<Integer, Integer>> updateManga(Manga manga) {
@ -133,7 +139,7 @@ public class LibraryUpdateService extends Service {
.flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters)); .flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters));
} }
private String getUpdatedMangas(List<MangaUpdate> updates, List<Manga> failedUpdates) { private String getUpdatedMangasResult(List<MangaUpdate> updates, List<Manga> failedUpdates) {
final StringBuilder result = new StringBuilder(); final StringBuilder result = new StringBuilder();
if (updates.isEmpty()) { if (updates.isEmpty()) {
result.append(getString(R.string.notification_no_new_chapters)).append("\n"); result.append(getString(R.string.notification_no_new_chapters)).append("\n");
@ -185,7 +191,20 @@ public class LibraryUpdateService extends Service {
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build()); notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
} }
private void showBigNotification(String title, String body) { private void showProgressNotification(String title, String body) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_action_refresh)
.setContentTitle(title)
.setContentText(body)
.setOngoing(true);
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
}
private void showResultNotification(String title, String body) {
NotificationCompat.Builder builder = new NotificationCompat.Builder(this) NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
.setSmallIcon(R.drawable.ic_action_refresh) .setSmallIcon(R.drawable.ic_action_refresh)
.setContentTitle(title) .setContentTitle(title)
@ -199,6 +218,13 @@ public class LibraryUpdateService extends Service {
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build()); notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
} }
private void cancelNotification() {
NotificationManager notificationManager =
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
notificationManager.cancel(UPDATE_NOTIFICATION_ID);
}
private PendingIntent getNotificationIntent() { private PendingIntent getNotificationIntent() {
Intent intent = new Intent(this, MainActivity.class); Intent intent = new Intent(this, MainActivity.class);
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP); intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);

View File

@ -0,0 +1,31 @@
package eu.kanade.tachiyomi.data.updater;
import android.content.Context;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.rest.GithubService;
import eu.kanade.tachiyomi.data.rest.Release;
import eu.kanade.tachiyomi.data.rest.ServiceFactory;
import eu.kanade.tachiyomi.util.ToastUtil;
import rx.Observable;
public class UpdateChecker {
private final Context context;
public UpdateChecker(Context context) {
this.context = context;
}
/**
* Returns observable containing release information
*
*/
public Observable<Release> checkForApplicationUpdate() {
ToastUtil.showShort(context, context.getString(R.string.update_check_look_for_updates));
//Create Github service to retrieve Github data
GithubService service = ServiceFactory.createRetrofitService(GithubService.class, GithubService.SERVICE_ENDPOINT);
return service.getLatestVersion();
}
}

View File

@ -0,0 +1,99 @@
package eu.kanade.tachiyomi.data.updater;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.os.AsyncTask;
import java.io.File;
import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.HttpURLConnection;
import java.net.URL;
import javax.inject.Inject;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
public class UpdateDownloader extends AsyncTask<String, Void, Void> {
/**
* Name of cache directory.
*/
private static final String PARAMETER_CACHE_DIRECTORY = "apk_downloads";
/**
* Interface to global information about an application environment.
*/
private final Context context;
/**
* Cache directory used for cache management.
*/
private final File cacheDir;
@Inject PreferencesHelper preferencesHelper;
/**
* Constructor of UpdaterCache.
*
* @param context application environment interface.
*/
public UpdateDownloader(Context context) {
App.get(context).getComponent().inject(this);
this.context = context;
// Get cache directory from parameter.
cacheDir = new File(preferencesHelper.getDownloadsDirectory(), PARAMETER_CACHE_DIRECTORY);
// Create cache directory.
createCacheDir();
}
/**
* Create cache directory if it doesn't exist
*
* @return true if cache dir is created otherwise false.
*/
@SuppressWarnings("UnusedReturnValue")
private boolean createCacheDir() {
return !cacheDir.exists() && cacheDir.mkdirs();
}
@Override
protected Void doInBackground(String... args) {
try {
createCacheDir();
URL url = new URL(args[0]);
HttpURLConnection c = (HttpURLConnection) url.openConnection();
c.connect();
File outputFile = new File(cacheDir, "update.apk");
if (outputFile.exists()) {
//noinspection ResultOfMethodCallIgnored
outputFile.delete();
}
FileOutputStream fos = new FileOutputStream(outputFile);
InputStream is = c.getInputStream();
byte[] buffer = new byte[1024];
int len1;
while ((len1 = is.read(buffer)) != -1) {
fos.write(buffer, 0, len1);
}
fos.close();
is.close();
// Prompt install interface
Intent intent = new Intent(Intent.ACTION_VIEW);
intent.setDataAndType(Uri.fromFile(outputFile), "application/vnd.android.package-archive");
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // without this flag android returned a intent error!
context.startActivity(intent);
} catch (Exception e) {
e.printStackTrace();
}
return null;
}
}

View File

@ -0,0 +1,12 @@
package eu.kanade.tachiyomi.event;
import eu.kanade.tachiyomi.data.database.models.Manga;
public class MangaEvent {
public final Manga manga;
public MangaEvent(Manga manga) {
this.manga = manga;
}
}

View File

@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
import eu.kanade.tachiyomi.data.source.base.Source; import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.sync.LibraryUpdateService; import eu.kanade.tachiyomi.data.sync.LibraryUpdateService;
import eu.kanade.tachiyomi.data.sync.UpdateMangaSyncService; import eu.kanade.tachiyomi.data.sync.UpdateMangaSyncService;
import eu.kanade.tachiyomi.data.updater.UpdateDownloader;
import eu.kanade.tachiyomi.injection.module.AppModule; import eu.kanade.tachiyomi.injection.module.AppModule;
import eu.kanade.tachiyomi.injection.module.DataModule; import eu.kanade.tachiyomi.injection.module.DataModule;
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter; import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter;
@ -50,6 +51,7 @@ public interface AppComponent {
void inject(ReaderActivity readerActivity); void inject(ReaderActivity readerActivity);
void inject(MangaActivity mangaActivity); void inject(MangaActivity mangaActivity);
void inject(SettingsAccountsFragment settingsAccountsFragment); void inject(SettingsAccountsFragment settingsAccountsFragment);
void inject(SettingsActivity settingsActivity); void inject(SettingsActivity settingsActivity);
void inject(Source source); void inject(Source source);
@ -60,6 +62,7 @@ public interface AppComponent {
void inject(DownloadService downloadService); void inject(DownloadService downloadService);
void inject(UpdateMangaSyncService updateMangaSyncService); void inject(UpdateMangaSyncService updateMangaSyncService);
void inject(UpdateDownloader updateDownloader);
Application application(); Application application();
} }

View File

@ -47,8 +47,8 @@ public class DataModule {
@Provides @Provides
@Singleton @Singleton
NetworkHelper provideNetworkHelper() { NetworkHelper provideNetworkHelper(Application app) {
return new NetworkHelper(); return new NetworkHelper(app);
} }
@Provides @Provides

View File

@ -5,7 +5,8 @@ import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.view.MenuItem; import android.view.MenuItem;
import de.greenrobot.event.EventBus; import org.greenrobot.eventbus.EventBus;
import icepick.Icepick; import icepick.Icepick;
public class BaseActivity extends AppCompatActivity { public class BaseActivity extends AppCompatActivity {
@ -58,20 +59,8 @@ public class BaseActivity extends AppCompatActivity {
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
public void registerForStickyEvents() {
registerForStickyEvents(0);
}
public void registerForStickyEvents(int priority) {
EventBus.getDefault().registerSticky(this, priority);
}
public void registerForEvents() { public void registerForEvents() {
registerForEvents(0); EventBus.getDefault().register(this);
}
public void registerForEvents(int priority) {
EventBus.getDefault().register(this, priority);
} }
public void unregisterForEvents() { public void unregisterForEvents() {

View File

@ -19,10 +19,19 @@ import android.content.Context;
import android.support.design.widget.CoordinatorLayout; import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton; import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.ViewCompat; import android.support.v4.view.ViewCompat;
import android.support.v4.view.animation.FastOutSlowInInterpolator;
import android.util.AttributeSet; import android.util.AttributeSet;
import android.view.View; import android.view.View;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.view.animation.Interpolator;
import eu.kanade.tachiyomi.R;
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior { public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
private boolean mIsAnimatingOut = false;
public ScrollAwareFABBehavior(Context context, AttributeSet attrs) { public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
super(); super();
} }
@ -40,12 +49,43 @@ public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
final View target, final int dxConsumed, final int dyConsumed, final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) { final int dxUnconsumed, final int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed); super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) { if (dyConsumed > 0 && !this.mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
// User scrolled down and the FAB is currently visible -> hide the FAB // User scrolled down and the FAB is currently visible -> hide the FAB
child.hide(); animateOut(child);
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) { } else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
// User scrolled up and the FAB is currently not visible -> show the FAB // User scrolled up and the FAB is currently not visible -> show the FAB
child.show(); animateIn(child);
} }
} }
}
// Same animation that FloatingActionButton.Behavior uses to hide the FAB when the AppBarLayout exits
private void animateOut(final FloatingActionButton button) {
Animation anim = AnimationUtils.loadAnimation(button.getContext(), R.anim.fab_hide_to_bottom);
anim.setInterpolator(INTERPOLATOR);
anim.setDuration(200L);
anim.setAnimationListener(new Animation.AnimationListener() {
public void onAnimationStart(Animation animation) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
}
public void onAnimationEnd(Animation animation) {
ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
button.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(final Animation animation) {
}
});
button.startAnimation(anim);
}
// Same animation that FloatingActionButton.Behavior uses to show the FAB when the AppBarLayout enters
private void animateIn(FloatingActionButton button) {
button.setVisibility(View.VISIBLE);
Animation anim = AnimationUtils.loadAnimation(button.getContext(), R.anim.fab_show_from_bottom);
anim.setDuration(200L);
anim.setInterpolator(INTERPOLATOR);
button.startAnimation(anim);
}
}

View File

@ -3,7 +3,8 @@ package eu.kanade.tachiyomi.ui.base.fragment;
import android.os.Bundle; import android.os.Bundle;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import de.greenrobot.event.EventBus; import org.greenrobot.eventbus.EventBus;
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity; import eu.kanade.tachiyomi.ui.base.activity.BaseActivity;
import icepick.Icepick; import icepick.Icepick;
@ -33,20 +34,8 @@ public class BaseFragment extends Fragment {
return (BaseActivity) getActivity(); return (BaseActivity) getActivity();
} }
public void registerForStickyEvents() {
registerForStickyEvents(0);
}
public void registerForStickyEvents(int priority) {
EventBus.getDefault().registerSticky(this, priority);
}
public void registerForEvents() { public void registerForEvents() {
registerForEvents(0); EventBus.getDefault().register(this);
}
public void registerForEvents(int priority) {
EventBus.getDefault().register(this, priority);
} }
public void unregisterForEvents() { public void unregisterForEvents() {

View File

@ -4,7 +4,8 @@ import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import de.greenrobot.event.EventBus; import org.greenrobot.eventbus.EventBus;
import icepick.Icepick; import icepick.Icepick;
import nucleus.view.ViewWithPresenter; import nucleus.view.ViewWithPresenter;
@ -24,10 +25,6 @@ public class BasePresenter<V extends ViewWithPresenter> extends RxPresenter<V> {
Icepick.saveInstanceState(this, state); Icepick.saveInstanceState(this, state);
} }
public void registerForStickyEvents() {
EventBus.getDefault().registerSticky(this);
}
public void registerForEvents() { public void registerForEvents() {
EventBus.getDefault().register(this); EventBus.getDefault().register(this);
} }

View File

@ -107,14 +107,14 @@ public class RxPresenter<View> extends Presenter<View> {
} }
/** /**
* Checks if a restartable is subscribed. * Checks if a restartable is unsubscribed.
* *
* @param restartableId id of a restartable. * @param restartableId id of the restartable.
* @return True if the restartable is subscribed, false otherwise. * @return true if the subscription is null or unsubscribed, false otherwise.
*/ */
public boolean isSubscribed(int restartableId) { public boolean isUnsubscribed(int restartableId) {
Subscription s = restartableSubscriptions.get(restartableId); Subscription subscription = restartableSubscriptions.get(restartableId);
return s != null && !s.isUnsubscribed(); return subscription == null || subscription.isUnsubscribed();
} }
/** /**
@ -213,6 +213,137 @@ public class RxPresenter<View> extends Presenter<View> {
restartableReplay(restartableId, observableFactory, onNext, null); restartableReplay(restartableId, observableFactory, onNext, null);
} }
/**
* A startable behaves the same as a restartable but it does not resubscribe on process restart
*
* @param startableId an id of the restartable.
* @param observableFactory a factory that should return an Observable when the startable should run.
*/
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory) {
restartables.put(startableId, () -> observableFactory.call().subscribe());
}
/**
* A startable behaves the same as a restartable but it does not resubscribe on process restart
*
* @param startableId an id of the restartable.
* @param observableFactory a factory that should return an Observable when the startable should run.
* @param onNext a callback that will be called when received data should be delivered to view.
* @param onError a callback that will be called if the source observable emits onError.
*/
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory,
final Action1<T> onNext, final Action1<Throwable> onError) {
restartables.put(startableId, () -> observableFactory.call().subscribe(onNext, onError));
}
/**
* A startable behaves the same as a restartable but it does not resubscribe on process restart
*
* @param startableId an id of the restartable.
* @param observableFactory a factory that should return an Observable when the startable should run.
* @param onNext a callback that will be called when received data should be delivered to view.
*/
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory, final Action1<T> onNext) {
restartables.put(startableId, () -> observableFactory.call().subscribe(onNext));
}
/**
* This is a shortcut that can be used instead of combining together
* {@link #startable(int, Func0)},
* {@link #deliverFirst()},
* {@link #split(Action2, Action2)}.
*
* @param startableId an id of the startable.
* @param observableFactory a factory that should return an Observable when the startable should run.
* @param onNext a callback that will be called when received data should be delivered to view.
* @param onError a callback that will be called if the source observable emits onError.
* @param <T> the type of the observable.
*/
public <T> void startableFirst(int startableId, final Func0<Observable<T>> observableFactory,
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
restartables.put(startableId, new Func0<Subscription>() {
@Override
public Subscription call() {
return observableFactory.call()
.compose(RxPresenter.this.<T>deliverFirst())
.subscribe(split(onNext, onError));
}
});
}
/**
* This is a shortcut for calling {@link #startableFirst(int, Func0, Action2, Action2)} with the last parameter = null.
*/
public <T> void startableFirst(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
startableFirst(startableId, observableFactory, onNext, null);
}
/**
* This is a shortcut that can be used instead of combining together
* {@link #startable(int, Func0)},
* {@link #deliverLatestCache()},
* {@link #split(Action2, Action2)}.
*
* @param startableId an id of the startable.
* @param observableFactory a factory that should return an Observable when the startable should run.
* @param onNext a callback that will be called when received data should be delivered to view.
* @param onError a callback that will be called if the source observable emits onError.
* @param <T> the type of the observable.
*/
public <T> void startableLatestCache(int startableId, final Func0<Observable<T>> observableFactory,
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
restartables.put(startableId, new Func0<Subscription>() {
@Override
public Subscription call() {
return observableFactory.call()
.compose(RxPresenter.this.<T>deliverLatestCache())
.subscribe(split(onNext, onError));
}
});
}
/**
* This is a shortcut for calling {@link #startableLatestCache(int, Func0, Action2, Action2)} with the last parameter = null.
*/
public <T> void startableLatestCache(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
startableLatestCache(startableId, observableFactory, onNext, null);
}
/**
* This is a shortcut that can be used instead of combining together
* {@link #startable(int, Func0)},
* {@link #deliverReplay()},
* {@link #split(Action2, Action2)}.
*
* @param startableId an id of the startable.
* @param observableFactory a factory that should return an Observable when the startable should run.
* @param onNext a callback that will be called when received data should be delivered to view.
* @param onError a callback that will be called if the source observable emits onError.
* @param <T> the type of the observable.
*/
public <T> void startableReplay(int startableId, final Func0<Observable<T>> observableFactory,
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
restartables.put(startableId, new Func0<Subscription>() {
@Override
public Subscription call() {
return observableFactory.call()
.compose(RxPresenter.this.<T>deliverReplay())
.subscribe(split(onNext, onError));
}
});
}
/**
* This is a shortcut for calling {@link #startableReplay(int, Func0, Action2, Action2)} with the last parameter = null.
*/
public <T> void startableReplay(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
startableReplay(startableId, observableFactory, onNext, null);
}
/** /**
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by * Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
* the source {@link rx.Observable}. * the source {@link rx.Observable}.

View File

@ -31,6 +31,10 @@ public class CatalogueAdapter extends FlexibleAdapter<CatalogueHolder, Manga> {
notifyDataSetChanged(); notifyDataSetChanged();
} }
public List<Manga> getItems() {
return mItems;
}
@Override @Override
public long getItemId(int position) { public long getItemId(int position) {
return mItems.get(position).id; return mItems.get(position).id;
@ -44,8 +48,13 @@ public class CatalogueAdapter extends FlexibleAdapter<CatalogueHolder, Manga> {
@Override @Override
public CatalogueHolder onCreateViewHolder(ViewGroup parent, int viewType) { public CatalogueHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = fragment.getActivity().getLayoutInflater(); LayoutInflater inflater = fragment.getActivity().getLayoutInflater();
View v = inflater.inflate(R.layout.item_catalogue, parent, false); if (parent.getId() == R.id.catalogue_grid) {
return new CatalogueHolder(v, this, fragment); View v = inflater.inflate(R.layout.item_catalogue_grid, parent, false);
return new CatalogueGridHolder(v, this, fragment);
} else {
View v = inflater.inflate(R.layout.item_catalogue_list, parent, false);
return new CatalogueListHolder(v, this, fragment);
}
} }
@Override @Override

View File

@ -4,7 +4,10 @@ import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.GridLayoutManager; import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.SearchView; import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.text.TextUtils; import android.text.TextUtils;
@ -14,9 +17,13 @@ import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView; import android.widget.AdapterView;
import android.widget.ArrayAdapter;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.Spinner; import android.widget.Spinner;
import android.widget.ViewSwitcher;
import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.MaterialDialog;
@ -30,11 +37,13 @@ import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.base.Source; import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder; import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment; import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
import eu.kanade.tachiyomi.ui.decoration.DividerItemDecoration;
import eu.kanade.tachiyomi.ui.main.MainActivity; import eu.kanade.tachiyomi.ui.main.MainActivity;
import eu.kanade.tachiyomi.ui.manga.MangaActivity; import eu.kanade.tachiyomi.ui.manga.MangaActivity;
import eu.kanade.tachiyomi.util.ToastUtil; import eu.kanade.tachiyomi.util.ToastUtil;
import eu.kanade.tachiyomi.widget.AutofitRecyclerView; import eu.kanade.tachiyomi.widget.AutofitRecyclerView;
import eu.kanade.tachiyomi.widget.EndlessRecyclerScrollListener; import eu.kanade.tachiyomi.widget.EndlessGridScrollListener;
import eu.kanade.tachiyomi.widget.EndlessListScrollListener;
import icepick.State; import icepick.State;
import nucleus.factory.RequiresPresenter; import nucleus.factory.RequiresPresenter;
import rx.Subscription; import rx.Subscription;
@ -45,22 +54,28 @@ import rx.subjects.PublishSubject;
public class CatalogueFragment extends BaseRxFragment<CataloguePresenter> public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
implements FlexibleViewHolder.OnListItemClickListener { implements FlexibleViewHolder.OnListItemClickListener {
@Bind(R.id.recycler) AutofitRecyclerView recycler; @Bind(R.id.switcher) ViewSwitcher switcher;
@Bind(R.id.catalogue_grid) AutofitRecyclerView catalogueGrid;
@Bind(R.id.catalogue_list) RecyclerView catalogueList;
@Bind(R.id.progress) ProgressBar progress; @Bind(R.id.progress) ProgressBar progress;
@Bind(R.id.progress_grid) ProgressBar progressGrid; @Bind(R.id.progress_grid) ProgressBar progressGrid;
private Toolbar toolbar; private Toolbar toolbar;
private Spinner spinner; private Spinner spinner;
private CatalogueAdapter adapter; private CatalogueAdapter adapter;
private EndlessRecyclerScrollListener scrollListener; private EndlessGridScrollListener gridScrollListener;
private EndlessListScrollListener listScrollListener;
@State String query = ""; @State String query = "";
@State int selectedIndex = -1; @State int selectedIndex;
private final int SEARCH_TIMEOUT = 1000; private final int SEARCH_TIMEOUT = 1000;
private PublishSubject<String> queryDebouncerSubject; private PublishSubject<String> queryDebouncerSubject;
private Subscription queryDebouncerSubscription; private Subscription queryDebouncerSubscription;
private MenuItem displayMode;
private MenuItem searchItem;
public static CatalogueFragment newInstance() { public static CatalogueFragment newInstance() {
return new CatalogueFragment(); return new CatalogueFragment();
} }
@ -77,38 +92,61 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
View view = inflater.inflate(R.layout.fragment_catalogue, container, false); View view = inflater.inflate(R.layout.fragment_catalogue, container, false);
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
// Initialize adapter and scroll listener // Initialize adapter, scroll listener and recycler views
GridLayoutManager layoutManager = (GridLayoutManager) recycler.getLayoutManager();
adapter = new CatalogueAdapter(this); adapter = new CatalogueAdapter(this);
scrollListener = new EndlessRecyclerScrollListener(layoutManager, this::requestNextPage);
recycler.setHasFixedSize(true); GridLayoutManager glm = (GridLayoutManager) catalogueGrid.getLayoutManager();
recycler.setAdapter(adapter); gridScrollListener = new EndlessGridScrollListener(glm, this::requestNextPage);
recycler.addOnScrollListener(scrollListener); catalogueGrid.setHasFixedSize(true);
catalogueGrid.setAdapter(adapter);
catalogueGrid.addOnScrollListener(gridScrollListener);
LinearLayoutManager llm = new LinearLayoutManager(getActivity());
listScrollListener = new EndlessListScrollListener(llm, this::requestNextPage);
catalogueList.setHasFixedSize(true);
catalogueList.setAdapter(adapter);
catalogueList.setLayoutManager(llm);
catalogueList.addOnScrollListener(listScrollListener);
catalogueList.addItemDecoration(new DividerItemDecoration(
ContextCompat.getDrawable(getContext(), R.drawable.line_divider)));
if (getPresenter().isListMode()) {
switcher.showNext();
}
Animation inAnim = AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in);
Animation outAnim = AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_out);
switcher.setInAnimation(inAnim);
switcher.setOutAnimation(outAnim);
// Create toolbar spinner // Create toolbar spinner
Context themedContext = getBaseActivity().getSupportActionBar() != null ? Context themedContext = getBaseActivity().getSupportActionBar() != null ?
getBaseActivity().getSupportActionBar().getThemedContext() : getActivity(); getBaseActivity().getSupportActionBar().getThemedContext() : getActivity();
spinner = new Spinner(themedContext); spinner = new Spinner(themedContext);
CatalogueSpinnerAdapter spinnerAdapter = new CatalogueSpinnerAdapter(themedContext, ArrayAdapter<Source> spinnerAdapter = new ArrayAdapter<>(themedContext,
android.R.layout.simple_spinner_item, getPresenter().getEnabledSources()); android.R.layout.simple_spinner_item, getPresenter().getEnabledSources());
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item); spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
if (savedState == null) selectedIndex = spinnerAdapter.getEmptyIndex();
if (savedState == null) {
selectedIndex = getPresenter().getLastUsedSourceIndex();
}
spinner.setAdapter(spinnerAdapter); spinner.setAdapter(spinnerAdapter);
spinner.setSelection(selectedIndex); spinner.setSelection(selectedIndex);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() { spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override @Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) { public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Source source = spinnerAdapter.getItem(position); Source source = spinnerAdapter.getItem(position);
// We add an empty source with id -1 that acts as a placeholder to show a hint if (selectedIndex != position || adapter.isEmpty()) {
// that asks to select a source
if (source.getId() != -1 && (selectedIndex != position || adapter.isEmpty())) {
// Set previous selection if it's not a valid source and notify the user // Set previous selection if it's not a valid source and notify the user
if (!getPresenter().isValidSource(source)) { if (!getPresenter().isValidSource(source)) {
spinner.setSelection(spinnerAdapter.getEmptyIndex()); spinner.setSelection(getPresenter().findFirstValidSource());
ToastUtil.showShort(getActivity(), R.string.source_requires_login); ToastUtil.showShort(getActivity(), R.string.source_requires_login);
} else { } else {
selectedIndex = position; selectedIndex = position;
getPresenter().setEnabledSource(selectedIndex);
showProgressBar(); showProgressBar();
glm.scrollToPositionWithOffset(0, 0);
llm.scrollToPositionWithOffset(0, 0);
getPresenter().startRequesting(source); getPresenter().startRequesting(source);
} }
} }
@ -130,7 +168,7 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
inflater.inflate(R.menu.catalogue_list, menu); inflater.inflate(R.menu.catalogue_list, menu);
// Initialize search menu // Initialize search menu
MenuItem searchItem = menu.findItem(R.id.action_search); searchItem = menu.findItem(R.id.action_search);
final SearchView searchView = (SearchView) searchItem.getActionView(); final SearchView searchView = (SearchView) searchItem.getActionView();
if (!TextUtils.isEmpty(query)) { if (!TextUtils.isEmpty(query)) {
@ -151,6 +189,22 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
return true; return true;
} }
}); });
// Show next display mode
displayMode = menu.findItem(R.id.action_display_mode);
int icon = getPresenter().isListMode() ?
R.drawable.ic_view_module_white_24dp : R.drawable.ic_view_list_white_24dp;
displayMode.setIcon(icon);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_display_mode:
swapDisplayMode();
break;
}
return super.onOptionsItemSelected(item);
} }
@Override @Override
@ -167,6 +221,9 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
@Override @Override
public void onDestroyView() { public void onDestroyView() {
if (searchItem != null && searchItem.isActionViewExpanded()) {
searchItem.collapseActionView();
}
toolbar.removeView(spinner); toolbar.removeView(spinner);
super.onDestroyView(); super.onDestroyView();
} }
@ -193,11 +250,13 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
private void restartRequest(String newQuery) { private void restartRequest(String newQuery) {
// If text didn't change, do nothing // If text didn't change, do nothing
if (query.equals(newQuery)) return; if (query.equals(newQuery) || getPresenter().getSource() == null)
return;
query = newQuery; query = newQuery;
showProgressBar(); showProgressBar();
recycler.getLayoutManager().scrollToPosition(0); catalogueGrid.getLayoutManager().scrollToPosition(0);
catalogueList.getLayoutManager().scrollToPosition(0);
getPresenter().restartRequest(query); getPresenter().restartRequest(query);
} }
@ -211,9 +270,10 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
public void onAddPage(int page, List<Manga> mangas) { public void onAddPage(int page, List<Manga> mangas) {
hideProgressBar(); hideProgressBar();
if (page == 1) { if (page == 0) {
adapter.clear(); adapter.clear();
scrollListener.resetScroll(); gridScrollListener.resetScroll();
listScrollListener.resetScroll();
} }
adapter.addItems(mangas); adapter.addItems(mangas);
} }
@ -223,15 +283,28 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
} }
public void updateImage(Manga manga) { public void updateImage(Manga manga) {
CatalogueHolder holder = getHolder(manga); CatalogueGridHolder holder = getHolder(manga);
if (holder != null) { if (holder != null) {
holder.setImage(manga, getPresenter()); holder.setImage(manga, getPresenter());
} }
} }
public void swapDisplayMode() {
getPresenter().swapDisplayMode();
boolean isListMode = getPresenter().isListMode();
int icon = isListMode ?
R.drawable.ic_view_module_white_24dp : R.drawable.ic_view_list_white_24dp;
displayMode.setIcon(icon);
switcher.showNext();
if (!isListMode) {
// Initialize mangas if going to grid view
getPresenter().initializeMangas(adapter.getItems());
}
}
@Nullable @Nullable
private CatalogueHolder getHolder(Manga manga) { private CatalogueGridHolder getHolder(Manga manga) {
return (CatalogueHolder) recycler.findViewHolderForItemId(manga.id); return (CatalogueGridHolder) catalogueGrid.findViewHolderForItemId(manga.id);
} }
private void showProgressBar() { private void showProgressBar() {
@ -261,12 +334,15 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
public void onListItemLongClick(int position) { public void onListItemLongClick(int position) {
final Manga selectedManga = adapter.getItem(position); final Manga selectedManga = adapter.getItem(position);
int textRes = selectedManga.favorite ? R.string.remove_from_library : R.string.add_to_library;
new MaterialDialog.Builder(getActivity()) new MaterialDialog.Builder(getActivity())
.items(getString(R.string.add_to_library)) .items(getString(textRes))
.itemsCallback((dialog, itemView, which, text) -> { .itemsCallback((dialog, itemView, which, text) -> {
switch (which) { switch (which) {
case 0: case 0:
getPresenter().addMangaToLibrary(selectedManga); getPresenter().changeMangaFavorite(selectedManga);
adapter.notifyItemChanged(position);
break; break;
} }
}) })

View File

@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.catalogue;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.mikepenz.iconics.view.IconicsImageView;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Manga;
public class CatalogueGridHolder extends CatalogueHolder {
@Bind(R.id.title) TextView title;
@Bind(R.id.thumbnail) ImageView thumbnail;
@Bind(R.id.favorite_sticker) IconicsImageView favoriteSticker;
public CatalogueGridHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) {
super(view, adapter, listener);
ButterKnife.bind(this, view);
}
@Override
public void onSetValues(Manga manga, CataloguePresenter presenter) {
title.setText(manga.title);
// Set visibility of in library icon.
favoriteSticker.setVisibility(manga.favorite ? View.VISIBLE : View.GONE);
// Set alpha of thumbnail.
thumbnail.setAlpha(manga.favorite ? 0.3f : 1.0f);
setImage(manga, presenter);
}
public void setImage(Manga manga, CataloguePresenter presenter) {
if (manga.thumbnail_url != null) {
presenter.coverCache.loadFromNetwork(thumbnail, manga.thumbnail_url,
presenter.getSource().getGlideHeaders());
} else {
thumbnail.setImageResource(android.R.color.transparent);
}
}
}

View File

@ -1,38 +1,15 @@
package eu.kanade.tachiyomi.ui.catalogue; package eu.kanade.tachiyomi.ui.catalogue;
import android.view.View; import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder; import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
public class CatalogueHolder extends FlexibleViewHolder { public abstract class CatalogueHolder extends FlexibleViewHolder {
@Bind(R.id.title) TextView title;
@Bind(R.id.thumbnail) ImageView thumbnail;
@Bind(R.id.favorite_sticker) ImageView favoriteSticker;
public CatalogueHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) { public CatalogueHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) {
super(view, adapter, listener); super(view, adapter, listener);
ButterKnife.bind(this, view);
} }
public void onSetValues(Manga manga, CataloguePresenter presenter) { abstract void onSetValues(Manga manga, CataloguePresenter presenter);
title.setText(manga.title); }
favoriteSticker.setVisibility(manga.favorite ? View.VISIBLE : View.GONE);
setImage(manga, presenter);
}
public void setImage(Manga manga, CataloguePresenter presenter) {
if (manga.thumbnail_url != null) {
presenter.coverCache.loadFromNetwork(thumbnail, manga.thumbnail_url,
presenter.getSource().getGlideHeaders());
} else {
thumbnail.setImageResource(android.R.color.transparent);
}
}
}

View File

@ -0,0 +1,32 @@
package eu.kanade.tachiyomi.ui.catalogue;
import android.support.v4.content.ContextCompat;
import android.view.View;
import android.widget.TextView;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Manga;
public class CatalogueListHolder extends CatalogueHolder {
@Bind(R.id.title) TextView title;
private final int favoriteColor;
private final int unfavoriteColor;
public CatalogueListHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) {
super(view, adapter, listener);
ButterKnife.bind(this, view);
favoriteColor = ContextCompat.getColor(view.getContext(), R.color.hint_text);
unfavoriteColor = ContextCompat.getColor(view.getContext(), R.color.primary_text);
}
@Override
public void onSetValues(Manga manga, CataloguePresenter presenter) {
title.setText(manga.title);
title.setTextColor(manga.favorite ? favoriteColor : unfavoriteColor);
}
}

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.catalogue;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import android.util.Pair;
import com.pushtorefresh.storio.sqlite.operations.put.PutResult; import com.pushtorefresh.storio.sqlite.operations.put.PutResult;
@ -33,55 +32,68 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
@Inject CoverCache coverCache; @Inject CoverCache coverCache;
@Inject PreferencesHelper prefs; @Inject PreferencesHelper prefs;
private List<Source> sources;
private Source source; private Source source;
@State int sourceId; @State int sourceId;
private String query; private String query;
private int currentPage; private RxPager<Manga> pager;
private RxPager pager;
private MangasPage lastMangasPage; private MangasPage lastMangasPage;
private PublishSubject<List<Manga>> mangaDetailSubject; private PublishSubject<List<Manga>> mangaDetailSubject;
private boolean isListMode;
private static final int GET_MANGA_LIST = 1; private static final int GET_MANGA_LIST = 1;
private static final int GET_MANGA_DETAIL = 2; private static final int GET_MANGA_DETAIL = 2;
private static final int GET_MANGA_PAGE = 3;
@Override @Override
protected void onCreate(Bundle savedState) { protected void onCreate(Bundle savedState) {
super.onCreate(savedState); super.onCreate(savedState);
if (savedState != null) { if (savedState != null) {
onProcessRestart(); source = sourceManager.get(sourceId);
} }
sources = sourceManager.getSources();
mangaDetailSubject = PublishSubject.create(); mangaDetailSubject = PublishSubject.create();
restartableReplay(GET_MANGA_LIST, pager = new RxPager<>();
() -> pager.pages().concatMap(page -> getMangasPageObservable(page + 1)),
(view, pair) -> view.onAddPage(pair.first, pair.second),
(view, error) -> {
view.onAddPageError();
Timber.e(error.getMessage());
});
restartableLatestCache(GET_MANGA_DETAIL, startableReplay(GET_MANGA_LIST,
pager::results,
(view, pair) -> view.onAddPage(pair.first, pair.second));
startableFirst(GET_MANGA_PAGE,
() -> pager.request(page -> getMangasPageObservable(page + 1)),
(view, next) -> {},
(view, error) -> view.onAddPageError());
startableLatestCache(GET_MANGA_DETAIL,
() -> mangaDetailSubject () -> mangaDetailSubject
.observeOn(Schedulers.io()) .observeOn(Schedulers.io())
.flatMap(Observable::from) .flatMap(Observable::from)
.filter(manga -> !manga.initialized) .filter(manga -> !manga.initialized)
.window(3) .concatMap(this::getMangaDetails)
.concatMap(pack -> pack.concatMap(this::getMangaDetails))
.onBackpressureBuffer() .onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread()), .observeOn(AndroidSchedulers.mainThread()),
CatalogueFragment::updateImage, CatalogueFragment::updateImage,
(view, error) -> Timber.e(error.getMessage())); (view, error) -> Timber.e(error.getMessage()));
add(prefs.catalogueAsList().asObservable()
.subscribe(this::setDisplayMode));
} }
private void onProcessRestart() { private void setDisplayMode(boolean asList) {
source = sourceManager.get(sourceId); this.isListMode = asList;
stop(GET_MANGA_LIST); if (asList) {
stop(GET_MANGA_DETAIL); stop(GET_MANGA_DETAIL);
} else {
start(GET_MANGA_DETAIL);
}
} }
public void startRequesting(Source source) { public void startRequesting(Source source) {
@ -92,20 +104,23 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
public void restartRequest(String query) { public void restartRequest(String query) {
this.query = query; this.query = query;
stop(GET_MANGA_LIST); stop(GET_MANGA_PAGE);
currentPage = 1; lastMangasPage = null;
pager = new RxPager();
start(GET_MANGA_DETAIL); if (!isListMode) {
start(GET_MANGA_DETAIL);
}
start(GET_MANGA_LIST); start(GET_MANGA_LIST);
start(GET_MANGA_PAGE);
} }
public void requestNext() { public void requestNext() {
if (hasNextPage()) if (hasNextPage()) {
pager.requestNext(++currentPage); start(GET_MANGA_PAGE);
}
} }
private Observable<Pair<Integer, List<Manga>>> getMangasPageObservable(int page) { private Observable<List<Manga>> getMangasPageObservable(int page) {
MangasPage nextMangasPage = new MangasPage(page); MangasPage nextMangasPage = new MangasPage(page);
if (page != 1) { if (page != 1) {
nextMangasPage.url = lastMangasPage.nextPageUrl; nextMangasPage.url = lastMangasPage.nextPageUrl;
@ -120,11 +135,7 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
.flatMap(mangasPage -> Observable.from(mangasPage.mangas)) .flatMap(mangasPage -> Observable.from(mangasPage.mangas))
.map(this::networkToLocalManga) .map(this::networkToLocalManga)
.toList() .toList()
.map(mangas -> Pair.create(page, mangas)) .doOnNext(this::initializeMangas)
.doOnNext(pair -> {
if (mangaDetailSubject != null)
mangaDetailSubject.onNext(pair.second);
})
.observeOn(AndroidSchedulers.mainThread()); .observeOn(AndroidSchedulers.mainThread());
} }
@ -138,9 +149,12 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
return localManga; return localManga;
} }
public void initializeMangas(List<Manga> mangas) {
mangaDetailSubject.onNext(mangas);
}
private Observable<Manga> getMangaDetails(final Manga manga) { private Observable<Manga> getMangaDetails(final Manga manga) {
return source.pullMangaFromNetwork(manga.url) return source.pullMangaFromNetwork(manga.url)
.subscribeOn(Schedulers.io())
.flatMap(networkManga -> { .flatMap(networkManga -> {
manga.copyFrom(networkManga); manga.copyFrom(networkManga);
db.insertManga(manga).executeAsBlocking(); db.insertManga(manga).executeAsBlocking();
@ -157,6 +171,14 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
return lastMangasPage != null && lastMangasPage.nextPageUrl != null; return lastMangasPage != null && lastMangasPage.nextPageUrl != null;
} }
public int getLastUsedSourceIndex() {
int index = prefs.lastUsedCatalogueSource().get();
if (index < 0 || index >= sources.size() || !isValidSource(sources.get(index))) {
return findFirstValidSource();
}
return index;
}
public boolean isValidSource(Source source) { public boolean isValidSource(Source source) {
if (!source.isLoginRequired() || source.isLogged()) if (!source.isLoginRequired() || source.isLogged())
return true; return true;
@ -165,13 +187,35 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
|| prefs.getSourcePassword(source).equals("")); || prefs.getSourcePassword(source).equals(""));
} }
public int findFirstValidSource() {
for (int i = 0; i < sources.size(); i++) {
if (isValidSource(sources.get(i))) {
return i;
}
}
return 0;
}
public void setEnabledSource(int index) {
prefs.lastUsedCatalogueSource().set(index);
}
public List<Source> getEnabledSources() { public List<Source> getEnabledSources() {
// TODO filter by enabled source // TODO filter by enabled source
return sourceManager.getSources(); return sourceManager.getSources();
} }
public void addMangaToLibrary(Manga manga) { public void changeMangaFavorite(Manga manga) {
manga.favorite = true; manga.favorite = !manga.favorite;
db.insertManga(manga).executeAsBlocking(); db.insertManga(manga).executeAsBlocking();
} }
public boolean isListMode() {
return isListMode;
}
public void swapDisplayMode() {
prefs.catalogueAsList().set(!isListMode);
}
} }

View File

@ -1,120 +0,0 @@
package eu.kanade.tachiyomi.ui.catalogue;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.jsoup.nodes.Document;
import java.util.List;
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.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
public class CatalogueSpinnerAdapter extends ArrayAdapter<Source> {
public CatalogueSpinnerAdapter(Context context, int resource, List<Source> sources) {
super(context, resource, sources);
sources.add(new SimpleSource());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = super.getView(position, convertView, parent);
if (position == getCount()) {
((TextView)v.findViewById(android.R.id.text1)).setText("");
((TextView)v.findViewById(android.R.id.text1)).setHint(getItem(getCount()).getName());
}
return v;
}
@Override
public int getCount() {
return super.getCount()-1; // you dont display last item. It is used as hint.
}
public int getEmptyIndex() {
return getCount();
}
private class SimpleSource extends Source {
@Override
public String getName() {
return getContext().getString(R.string.select_source);
}
@Override
public int getId() {
return -1;
}
@Override
public String getBaseUrl() {
return null;
}
@Override
public boolean isLoginRequired() {
return false;
}
@Override
protected String getInitialPopularMangasUrl() {
return null;
}
@Override
protected String getInitialSearchUrl(String query) {
return null;
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
return null;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
return null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return null;
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
return null;
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
return null;
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
return null;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
return null;
}
}
}

View File

@ -29,7 +29,8 @@ public class DownloadFragment extends BaseRxFragment<DownloadPresenter> {
private DownloadAdapter adapter; private DownloadAdapter adapter;
private MenuItem startButton; private MenuItem startButton;
private MenuItem stopButton; private MenuItem pauseButton;
private MenuItem clearButton;
private Subscription queueStatusSubscription; private Subscription queueStatusSubscription;
private boolean isRunning; private boolean isRunning;
@ -64,11 +65,16 @@ public class DownloadFragment extends BaseRxFragment<DownloadPresenter> {
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.download_queue, menu); inflater.inflate(R.menu.download_queue, menu);
startButton = menu.findItem(R.id.start_queue); startButton = menu.findItem(R.id.start_queue);
stopButton = menu.findItem(R.id.stop_queue); pauseButton = menu.findItem(R.id.pause_queue);
clearButton = menu.findItem(R.id.clear_queue);
if(adapter.getItemCount() > 0) {
clearButton.setVisible(true);
}
// Menu seems to be inflated after onResume in fragments, so we initialize them here // Menu seems to be inflated after onResume in fragments, so we initialize them here
startButton.setVisible(!isRunning && !getPresenter().downloadManager.getQueue().isEmpty()); startButton.setVisible(!isRunning && !getPresenter().downloadManager.getQueue().isEmpty());
stopButton.setVisible(isRunning); pauseButton.setVisible(isRunning);
} }
@Override @Override
@ -77,9 +83,14 @@ public class DownloadFragment extends BaseRxFragment<DownloadPresenter> {
case R.id.start_queue: case R.id.start_queue:
DownloadService.start(getActivity()); DownloadService.start(getActivity());
break; break;
case R.id.stop_queue: case R.id.pause_queue:
DownloadService.stop(getActivity()); DownloadService.stop(getActivity());
break; break;
case R.id.clear_queue:
DownloadService.stop(getActivity());
getPresenter().clearQueue();
clearButton.setVisible(false);
break;
} }
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
@ -101,8 +112,8 @@ public class DownloadFragment extends BaseRxFragment<DownloadPresenter> {
isRunning = running; isRunning = running;
if (startButton != null) if (startButton != null)
startButton.setVisible(!running && !getPresenter().downloadManager.getQueue().isEmpty()); startButton.setVisible(!running && !getPresenter().downloadManager.getQueue().isEmpty());
if (stopButton != null) if (pauseButton != null)
stopButton.setVisible(running); pauseButton.setVisible(running);
} }
private void createAdapter() { private void createAdapter() {

View File

@ -20,15 +20,13 @@ import timber.log.Timber;
public class DownloadPresenter extends BasePresenter<DownloadFragment> { public class DownloadPresenter extends BasePresenter<DownloadFragment> {
public final static int GET_DOWNLOAD_QUEUE = 1;
@Inject DownloadManager downloadManager; @Inject DownloadManager downloadManager;
private DownloadQueue downloadQueue; private DownloadQueue downloadQueue;
private Subscription statusSubscription; private Subscription statusSubscription;
private Subscription pageProgressSubscription; private Subscription pageProgressSubscription;
private HashMap<Download, Subscription> progressSubscriptions; private HashMap<Download, Subscription> progressSubscriptions;
public final static int GET_DOWNLOAD_QUEUE = 1;
@Override @Override
protected void onCreate(Bundle savedState) { protected void onCreate(Bundle savedState) {
super.onCreate(savedState); super.onCreate(savedState);
@ -57,6 +55,7 @@ public class DownloadPresenter extends BasePresenter<DownloadFragment> {
})); }));
add(pageProgressSubscription = downloadQueue.getProgressObservable() add(pageProgressSubscription = downloadQueue.getProgressObservable()
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(view::updateDownloadedPages)); .subscribe(view::updateDownloadedPages));
} }
@ -90,6 +89,7 @@ public class DownloadPresenter extends BasePresenter<DownloadFragment> {
.flatMap(tick -> Observable.from(download.pages) .flatMap(tick -> Observable.from(download.pages)
.map(Page::getProgress) .map(Page::getProgress)
.reduce((x, y) -> x + y)) .reduce((x, y) -> x + y))
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(progress -> { .subscribe(progress -> {
if (download.totalProgress != progress) { if (download.totalProgress != progress) {
@ -121,4 +121,8 @@ public class DownloadPresenter extends BasePresenter<DownloadFragment> {
remove(statusSubscription); remove(statusSubscription);
} }
public void clearQueue() {
downloadQueue.clear();
start(GET_DOWNLOAD_QUEUE);
}
} }

View File

@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.ui.library;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -12,28 +10,24 @@ import java.util.List;
import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.Manga;
import rx.Observable;
public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga> public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga> {
implements Filterable {
List<Manga> mangas; private List<Manga> mangas;
Filter filter;
private LibraryCategoryFragment fragment; private LibraryCategoryFragment fragment;
public LibraryCategoryAdapter(LibraryCategoryFragment fragment) { public LibraryCategoryAdapter(LibraryCategoryFragment fragment) {
this.fragment = fragment; this.fragment = fragment;
mItems = new ArrayList<>(); mItems = new ArrayList<>();
filter = new LibraryFilter();
setHasStableIds(true); setHasStableIds(true);
} }
public void setItems(List<Manga> list) { public void setItems(List<Manga> list) {
mItems = list; mItems = list;
notifyDataSetChanged();
// TODO needed for filtering? // A copy of manga that it's always unfiltered
mangas = list; mangas = new ArrayList<>(list);
updateDataSet(null);
} }
public void clear() { public void clear() {
@ -47,12 +41,21 @@ public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga
@Override @Override
public void updateDataSet(String param) { public void updateDataSet(String param) {
if (mangas != null) {
filterItems(mangas);
notifyDataSetChanged();
}
}
@Override
protected boolean filterObject(Manga manga, String query) {
return (manga.title != null && manga.title.toLowerCase().contains(query)) ||
(manga.author != null && manga.author.toLowerCase().contains(query));
} }
@Override @Override
public LibraryHolder onCreateViewHolder(ViewGroup parent, int viewType) { public LibraryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(fragment.getActivity()).inflate(R.layout.item_catalogue, parent, false); View v = LayoutInflater.from(fragment.getActivity()).inflate(R.layout.item_catalogue_grid, parent, false);
return new LibraryHolder(v, this, fragment); return new LibraryHolder(v, this, fragment);
} }
@ -61,49 +64,12 @@ public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga
final LibraryPresenter presenter = ((LibraryFragment) fragment.getParentFragment()).getPresenter(); final LibraryPresenter presenter = ((LibraryFragment) fragment.getParentFragment()).getPresenter();
final Manga manga = getItem(position); final Manga manga = getItem(position);
holder.onSetValues(manga, presenter); holder.onSetValues(manga, presenter);
//When user scrolls this bind the correct selection status //When user scrolls this bind the correct selection status
holder.itemView.setActivated(isSelected(position)); holder.itemView.setActivated(isSelected(position));
} }
public int getCoverHeight() { public int getCoverHeight() {
return fragment.recycler.getItemWidth() / 9 * 12; return fragment.recycler.getItemWidth() / 3 * 4;
}
@Override
public Filter getFilter() {
return filter;
}
private class LibraryFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence charSequence) {
FilterResults results = new FilterResults();
String query = charSequence.toString().toLowerCase();
if (query.length() == 0) {
results.values = mangas;
results.count = mangas.size();
} else {
List<Manga> filteredMangas = Observable.from(mangas)
.filter(x ->
(x.title != null && x.title.toLowerCase().contains(query)) ||
(x.author != null && x.author.toLowerCase().contains(query)) ||
(x.artist != null && x.artist.toLowerCase().contains(query)))
.toList()
.toBlocking()
.single();
results.values = filteredMangas;
results.count = filteredMangas.size();
}
return results;
}
@Override
public void publishResults(CharSequence constraint, FilterResults results) {
setItems((List<Manga>) results.values);
}
} }
} }

View File

@ -9,6 +9,9 @@ import android.view.ViewGroup;
import com.f2prateek.rx.preferences.Preference; import com.f2prateek.rx.preferences.Preference;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
@ -22,7 +25,6 @@ import eu.kanade.tachiyomi.event.LibraryMangasEvent;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder; import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment; import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
import eu.kanade.tachiyomi.ui.manga.MangaActivity; import eu.kanade.tachiyomi.ui.manga.MangaActivity;
import eu.kanade.tachiyomi.util.EventBusHook;
import eu.kanade.tachiyomi.widget.AutofitRecyclerView; import eu.kanade.tachiyomi.widget.AutofitRecyclerView;
import icepick.State; import icepick.State;
import rx.Subscription; import rx.Subscription;
@ -37,6 +39,7 @@ public class LibraryCategoryFragment extends BaseFragment
private List<Manga> mangas; private List<Manga> mangas;
private Subscription numColumnsSubscription; private Subscription numColumnsSubscription;
private Subscription searchSubscription;
public static LibraryCategoryFragment newInstance(int position) { public static LibraryCategoryFragment newInstance(int position) {
LibraryCategoryFragment fragment = new LibraryCategoryFragment(); LibraryCategoryFragment fragment = new LibraryCategoryFragment();
@ -44,6 +47,8 @@ public class LibraryCategoryFragment extends BaseFragment
return fragment; return fragment;
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
// Inflate the layout for this fragment // Inflate the layout for this fragment
@ -77,19 +82,29 @@ public class LibraryCategoryFragment extends BaseFragment
} }
} }
searchSubscription = getLibraryPresenter().searchSubject
.subscribe(text -> {
adapter.setSearchText(text);
adapter.updateDataSet();
});
return view; return view;
} }
@Override @Override
public void onDestroyView() { public void onDestroyView() {
numColumnsSubscription.unsubscribe(); numColumnsSubscription.unsubscribe();
searchSubscription.unsubscribe();
super.onDestroyView(); super.onDestroyView();
} }
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
registerForStickyEvents(); registerForEvents();
} }
@Override @Override
@ -104,8 +119,8 @@ public class LibraryCategoryFragment extends BaseFragment
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
} }
@EventBusHook @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(LibraryMangasEvent event) { public void onEvent(LibraryMangasEvent event) {
List<Category> categories = getLibraryFragment().getAdapter().categories; List<Category> categories = getLibraryFragment().getAdapter().categories;
// When a category is deleted, the index can be greater than the number of categories // When a category is deleted, the index can be greater than the number of categories
if (position >= categories.size()) if (position >= categories.size())
@ -155,15 +170,16 @@ public class LibraryCategoryFragment extends BaseFragment
private void toggleSelection(int position) { private void toggleSelection(int position) {
LibraryFragment f = getLibraryFragment(); LibraryFragment f = getLibraryFragment();
adapter.toggleSelection(position, false); adapter.toggleSelection(position, false);
f.getPresenter().setSelection(adapter.getItem(position), adapter.isSelected(position)); f.getPresenter().setSelection(adapter.getItem(position), adapter.isSelected(position));
int count = f.getPresenter().selectedMangas.size(); int count = f.getPresenter().selectedMangas.size();
if (count == 0) { if (count == 0) {
f.destroyActionModeIfNeeded(); f.destroyActionModeIfNeeded();
} else { }
else {
f.setContextTitle(count); f.setContextTitle(count);
f.setVisibilityOfCoverEdit(count);
f.invalidateActionMode(); f.invalidateActionMode();
} }
} }

View File

@ -1,12 +1,16 @@
package eu.kanade.tachiyomi.ui.library; package eu.kanade.tachiyomi.ui.library;
import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.net.Uri;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout; import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout; import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.support.v7.view.ActionMode; import android.support.v7.view.ActionMode;
import android.support.v7.widget.SearchView;
import android.text.TextUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater; import android.view.MenuInflater;
@ -16,22 +20,27 @@ import android.view.ViewGroup;
import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.MaterialDialog;
import org.greenrobot.eventbus.EventBus;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import de.greenrobot.event.EventBus;
import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Category; import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.io.IOHandler;
import eu.kanade.tachiyomi.data.sync.LibraryUpdateService; import eu.kanade.tachiyomi.data.sync.LibraryUpdateService;
import eu.kanade.tachiyomi.event.LibraryMangasEvent; import eu.kanade.tachiyomi.event.LibraryMangasEvent;
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment; import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
import eu.kanade.tachiyomi.ui.library.category.CategoryActivity; import eu.kanade.tachiyomi.ui.library.category.CategoryActivity;
import eu.kanade.tachiyomi.ui.main.MainActivity; import eu.kanade.tachiyomi.ui.main.MainActivity;
import eu.kanade.tachiyomi.util.ToastUtil;
import icepick.State; import icepick.State;
import nucleus.factory.RequiresPresenter; import nucleus.factory.RequiresPresenter;
@ -39,16 +48,25 @@ import nucleus.factory.RequiresPresenter;
public class LibraryFragment extends BaseRxFragment<LibraryPresenter> public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
implements ActionMode.Callback { implements ActionMode.Callback {
@Bind(R.id.view_pager) ViewPager viewPager;
private TabLayout tabs; private static final int REQUEST_IMAGE_OPEN = 101;
private AppBarLayout appBar;
protected LibraryAdapter adapter; protected LibraryAdapter adapter;
private ActionMode actionMode; @Bind(R.id.view_pager) ViewPager viewPager;
@State int activeCategory; @State int activeCategory;
@State String query = "";
private TabLayout tabs;
private AppBarLayout appBar;
private ActionMode actionMode;
private Manga selectedCoverManga;
public static LibraryFragment newInstance() { public static LibraryFragment newInstance() {
return new LibraryFragment(); return new LibraryFragment();
} }
@ -60,8 +78,7 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Bundle savedInstanceState) {
// Inflate the layout for this fragment // Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_library, container, false); View view = inflater.inflate(R.layout.fragment_library, container, false);
setToolbarTitle(getString(R.string.label_library)); setToolbarTitle(getString(R.string.label_library));
@ -75,6 +92,10 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
viewPager.setAdapter(adapter); viewPager.setAdapter(adapter);
tabs.setupWithViewPager(viewPager); tabs.setupWithViewPager(viewPager);
if (savedState != null) {
getPresenter().searchSubject.onNext(query);
}
return view; return view;
} }
@ -84,12 +105,6 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
super.onDestroyView(); super.onDestroyView();
} }
@Override
public void onPause() {
EventBus.getDefault().removeStickyEvent(LibraryMangasEvent.class);
super.onPause();
}
@Override @Override
public void onSaveInstanceState(Bundle bundle) { public void onSaveInstanceState(Bundle bundle) {
activeCategory = viewPager.getCurrentItem(); activeCategory = viewPager.getCurrentItem();
@ -99,6 +114,29 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
@Override @Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.library, menu); inflater.inflate(R.menu.library, menu);
// Initialize search menu
MenuItem searchItem = menu.findItem(R.id.action_search);
final SearchView searchView = (SearchView) searchItem.getActionView();
if (!TextUtils.isEmpty(query)) {
searchItem.expandActionView();
searchView.setQuery(query, true);
searchView.clearFocus();
}
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
onSearchTextChange(query);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
onSearchTextChange(newText);
return true;
}
});
} }
@Override @Override
@ -115,6 +153,11 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
return super.onOptionsItemSelected(item); return super.onOptionsItemSelected(item);
} }
private void onSearchTextChange(String query) {
this.query = query;
getPresenter().searchSubject.onNext(query);
}
private void onEditCategories() { private void onEditCategories() {
Intent intent = CategoryActivity.newIntent(getActivity()); Intent intent = CategoryActivity.newIntent(getActivity());
startActivity(intent); startActivity(intent);
@ -158,6 +201,11 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
actionMode.setTitle(getString(R.string.label_selected, count)); actionMode.setTitle(getString(R.string.label_selected, count));
} }
public void setVisibilityOfCoverEdit(int count) {
// If count = 1 display edit button
actionMode.getMenu().findItem(R.id.action_edit_cover).setVisible((count == 1));
}
@Override @Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) { public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.library_selection, menu); mode.getMenuInflater().inflate(R.menu.library_selection, menu);
@ -173,6 +221,11 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
@Override @Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) { public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) { switch (item.getItemId()) {
case R.id.action_edit_cover:
changeSelectedCover(getPresenter().selectedMangas);
rebuildAdapter();
destroyActionModeIfNeeded();
return true;
case R.id.action_move_to_category: case R.id.action_move_to_category:
moveMangasToCategories(getPresenter().selectedMangas); moveMangasToCategories(getPresenter().selectedMangas);
return true; return true;
@ -184,6 +237,15 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
return false; return false;
} }
/**
* TODO workaround. Covers won't refresh any other way.
*/
public void rebuildAdapter() {
adapter = new LibraryAdapter(getChildFragmentManager());
viewPager.setAdapter(adapter);
tabs.setupWithViewPager(viewPager);
}
@Override @Override
public void onDestroyActionMode(ActionMode mode) { public void onDestroyActionMode(ActionMode mode) {
adapter.setSelectionMode(FlexibleAdapter.MODE_SINGLE); adapter.setSelectionMode(FlexibleAdapter.MODE_SINGLE);
@ -197,6 +259,53 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
} }
} }
private void changeSelectedCover(List<Manga> mangas) {
if (mangas.size() == 1) {
selectedCoverManga = mangas.get(0);
if (selectedCoverManga.favorite) {
Intent intent = new Intent();
intent.setType("image/*");
intent.setAction(Intent.ACTION_GET_CONTENT);
startActivityForResult(Intent.createChooser(intent,
getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN);
} else {
ToastUtil.showShort(getContext(), R.string.notification_first_add_to_library);
}
}
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (resultCode == Activity.RESULT_OK) {
switch (requestCode) {
case (REQUEST_IMAGE_OPEN):
if (selectedCoverManga != null) {
// Get the file's content URI from the incoming Intent
Uri selectedImageUri = data.getData();
// Convert to absolute path to prevent FileNotFoundException
String result = IOHandler.getFilePath(selectedImageUri,
getContext().getContentResolver(), getContext());
// Get file from filepath
File picture = new File(result != null ? result : "");
try {
// Update cover to selected file, show error if something went wrong
if (!getPresenter().editCoverWithLocalFile(picture, selectedCoverManga))
ToastUtil.showShort(getContext(), R.string.notification_manga_update_failed);
} catch (IOException e) {
e.printStackTrace();
}
}
break;
}
}
}
private void moveMangasToCategories(List<Manga> mangas) { private void moveMangasToCategories(List<Manga> mangas) {
new MaterialDialog.Builder(getActivity()) new MaterialDialog.Builder(getActivity())
.title(R.string.action_move_category) .title(R.string.action_move_category)

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.ui.library; package eu.kanade.tachiyomi.ui.library;
import android.view.View; import android.view.View;
import android.widget.FrameLayout;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
@ -17,6 +18,7 @@ import static android.widget.RelativeLayout.LayoutParams;
public class LibraryHolder extends FlexibleViewHolder { public class LibraryHolder extends FlexibleViewHolder {
@Bind(R.id.image_container) FrameLayout container;
@Bind(R.id.thumbnail) ImageView thumbnail; @Bind(R.id.thumbnail) ImageView thumbnail;
@Bind(R.id.title) TextView title; @Bind(R.id.title) TextView title;
@Bind(R.id.unreadText) TextView unreadText; @Bind(R.id.unreadText) TextView unreadText;
@ -24,7 +26,7 @@ public class LibraryHolder extends FlexibleViewHolder {
public LibraryHolder(View view, LibraryCategoryAdapter adapter, OnListItemClickListener listener) { public LibraryHolder(View view, LibraryCategoryAdapter adapter, OnListItemClickListener listener) {
super(view, adapter, listener); super(view, adapter, listener);
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
thumbnail.setLayoutParams(new LayoutParams(MATCH_PARENT, adapter.getCoverHeight())); container.setLayoutParams(new LayoutParams(MATCH_PARENT, adapter.getCoverHeight()));
} }
public void onSetValues(Manga manga, LibraryPresenter presenter) { public void onSetValues(Manga manga, LibraryPresenter presenter) {
@ -42,10 +44,12 @@ public class LibraryHolder extends FlexibleViewHolder {
private void loadCover(Manga manga, Source source, CoverCache coverCache) { private void loadCover(Manga manga, Source source, CoverCache coverCache) {
if (manga.thumbnail_url != null) { if (manga.thumbnail_url != null) {
coverCache.saveAndLoadFromCache(thumbnail, manga.thumbnail_url, source.getGlideHeaders()); coverCache.saveOrLoadFromCache(thumbnail, manga.thumbnail_url, source.getGlideHeaders());
} else { } else {
thumbnail.setImageResource(android.R.color.transparent); thumbnail.setImageResource(android.R.color.transparent);
} }
} }
} }

View File

@ -3,13 +3,16 @@ package eu.kanade.tachiyomi.ui.library;
import android.os.Bundle; import android.os.Bundle;
import android.util.Pair; import android.util.Pair;
import org.greenrobot.eventbus.EventBus;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.data.cache.CoverCache; import eu.kanade.tachiyomi.data.cache.CoverCache;
import eu.kanade.tachiyomi.data.database.DatabaseHelper; import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.Category; import eu.kanade.tachiyomi.data.database.models.Category;
@ -21,25 +24,27 @@ import eu.kanade.tachiyomi.event.LibraryMangasEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import rx.Observable; import rx.Observable;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
import rx.subjects.BehaviorSubject;
public class LibraryPresenter extends BasePresenter<LibraryFragment> { public class LibraryPresenter extends BasePresenter<LibraryFragment> {
private static final int GET_LIBRARY = 1;
protected List<Category> categories;
protected List<Manga> selectedMangas;
protected BehaviorSubject<String> searchSubject;
@Inject DatabaseHelper db; @Inject DatabaseHelper db;
@Inject PreferencesHelper preferences; @Inject PreferencesHelper preferences;
@Inject CoverCache coverCache; @Inject CoverCache coverCache;
@Inject SourceManager sourceManager; @Inject SourceManager sourceManager;
protected List<Category> categories;
protected List<Manga> selectedMangas;
private static final int GET_LIBRARY = 1;
@Override @Override
protected void onCreate(Bundle savedState) { protected void onCreate(Bundle savedState) {
super.onCreate(savedState); super.onCreate(savedState);
selectedMangas = new ArrayList<>(); selectedMangas = new ArrayList<>();
searchSubject = BehaviorSubject.create();
restartableLatestCache(GET_LIBRARY, restartableLatestCache(GET_LIBRARY,
this::getLibraryObservable, this::getLibraryObservable,
(view, pair) -> view.onNextLibraryUpdate(pair.first, pair.second)); (view, pair) -> view.onNextLibraryUpdate(pair.first, pair.second));
@ -50,15 +55,15 @@ public class LibraryPresenter extends BasePresenter<LibraryFragment> {
} }
@Override @Override
protected void onDestroy() { protected void onDropView() {
EventBus.getDefault().removeStickyEvent(LibraryMangasEvent.class); EventBus.getDefault().removeStickyEvent(LibraryMangasEvent.class);
super.onDestroy(); super.onDropView();
} }
@Override @Override
protected void onTakeView(LibraryFragment libraryFragment) { protected void onTakeView(LibraryFragment libraryFragment) {
super.onTakeView(libraryFragment); super.onTakeView(libraryFragment);
if (!isSubscribed(GET_LIBRARY)) { if (isUnsubscribed(GET_LIBRARY)) {
start(GET_LIBRARY); start(GET_LIBRARY);
} }
} }
@ -135,4 +140,18 @@ public class LibraryPresenter extends BasePresenter<LibraryFragment> {
db.setMangaCategories(mc, mangas); db.setMangaCategories(mc, mangas);
} }
/**
* Update cover with local file
*/
public boolean editCoverWithLocalFile(File file, Manga manga) throws IOException {
if (!manga.initialized)
return false;
if (manga.favorite) {
coverCache.copyToLocalCache(manga.thumbnail_url, file);
return true;
}
return false;
}
} }

View File

@ -8,8 +8,10 @@ import android.support.v4.widget.DrawerLayout;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.widget.FrameLayout; import android.widget.FrameLayout;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.materialdrawer.Drawer; import com.mikepenz.materialdrawer.Drawer;
import com.mikepenz.materialdrawer.DrawerBuilder; import com.mikepenz.materialdrawer.DrawerBuilder;
import com.mikepenz.materialdrawer.model.DividerDrawerItem;
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem; import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
import butterknife.Bind; import butterknife.Bind;
@ -29,12 +31,11 @@ public class MainActivity extends BaseActivity {
@Bind(R.id.appbar) AppBarLayout appBar; @Bind(R.id.appbar) AppBarLayout appBar;
@Bind(R.id.toolbar) Toolbar toolbar; @Bind(R.id.toolbar) Toolbar toolbar;
@Bind(R.id.drawer_container) FrameLayout container; @Bind(R.id.drawer_container) FrameLayout container;
@State
int selectedItem;
private Drawer drawer; private Drawer drawer;
private FragmentStack fragmentStack; private FragmentStack fragmentStack;
@State int selectedItem;
@Override @Override
protected void onCreate(Bundle savedState) { protected void onCreate(Bundle savedState) {
super.onCreate(savedState); super.onCreate(savedState);
@ -53,7 +54,7 @@ public class MainActivity extends BaseActivity {
fragmentStack = new FragmentStack(this, getSupportFragmentManager(), R.id.content_layout, fragmentStack = new FragmentStack(this, getSupportFragmentManager(), R.id.content_layout,
fragment -> { fragment -> {
if (fragment instanceof ViewWithPresenter) if (fragment instanceof ViewWithPresenter)
((ViewWithPresenter)fragment).getPresenter().destroy(); ((ViewWithPresenter) fragment).getPresenter().destroy();
}); });
drawer = new DrawerBuilder() drawer = new DrawerBuilder()
@ -71,20 +72,27 @@ public class MainActivity extends BaseActivity {
.addDrawerItems( .addDrawerItems(
new PrimaryDrawerItem() new PrimaryDrawerItem()
.withName(R.string.label_library) .withName(R.string.label_library)
.withIdentifier(R.id.nav_drawer_library), .withIdentifier(R.id.nav_drawer_library)
// new PrimaryDrawerItem() .withIcon(GoogleMaterial.Icon.gmd_book),
// .withName(R.string.label_recent_updates) new PrimaryDrawerItem()
// .withIdentifier(R.id.nav_drawer_recent_updates), .withName(R.string.label_recent_updates)
.withIdentifier(R.id.nav_drawer_recent_updates)
.withIcon(GoogleMaterial.Icon.gmd_update),
new PrimaryDrawerItem() new PrimaryDrawerItem()
.withName(R.string.label_catalogues) .withName(R.string.label_catalogues)
.withIdentifier(R.id.nav_drawer_catalogues), .withIdentifier(R.id.nav_drawer_catalogues)
.withIcon(GoogleMaterial.Icon.gmd_explore),
new PrimaryDrawerItem() new PrimaryDrawerItem()
.withName(R.string.label_download_queue) .withName(R.string.label_download_queue)
.withIdentifier(R.id.nav_drawer_downloads), .withIdentifier(R.id.nav_drawer_downloads)
.withIcon(GoogleMaterial.Icon.gmd_file_download),
new DividerDrawerItem(),
new PrimaryDrawerItem() new PrimaryDrawerItem()
.withName(R.string.label_settings) .withName(R.string.label_settings)
.withIdentifier(R.id.nav_drawer_settings) .withIdentifier(R.id.nav_drawer_settings)
.withSelectable(false) .withSelectable(false)
.withIcon(GoogleMaterial.Icon.gmd_settings)
) )
.withSavedInstance(savedState) .withSavedInstance(savedState)
.withOnDrawerItemClickListener( .withOnDrawerItemClickListener(
@ -179,4 +187,4 @@ public class MainActivity extends BaseActivity {
return appBar; return appBar;
} }
} }

View File

@ -1,15 +1,22 @@
package eu.kanade.tachiyomi.ui.manga; package eu.kanade.tachiyomi.ui.manga;
import android.Manifest;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.design.widget.TabLayout; import android.support.design.widget.TabLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter; import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import org.greenrobot.eventbus.EventBus;
import javax.inject.Inject; import javax.inject.Inject;
import butterknife.Bind; import butterknife.Bind;
@ -30,21 +37,21 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
@Bind(R.id.toolbar) Toolbar toolbar; @Bind(R.id.toolbar) Toolbar toolbar;
@Bind(R.id.tabs) TabLayout tabs; @Bind(R.id.tabs) TabLayout tabs;
@Bind(R.id.view_pager) ViewPager view_pager; @Bind(R.id.view_pager) ViewPager viewPager;
@Inject PreferencesHelper preferences; @Inject PreferencesHelper preferences;
@Inject MangaSyncManager mangaSyncManager; @Inject MangaSyncManager mangaSyncManager;
private MangaDetailAdapter adapter; private MangaDetailAdapter adapter;
private long manga_id; private boolean isOnline;
private boolean is_online;
public final static String MANGA_ID = "manga_id";
public final static String MANGA_ONLINE = "manga_online"; public final static String MANGA_ONLINE = "manga_online";
public static Intent newIntent(Context context, Manga manga) { public static Intent newIntent(Context context, Manga manga) {
Intent intent = new Intent(context, MangaActivity.class); Intent intent = new Intent(context, MangaActivity.class);
intent.putExtra(MANGA_ID, manga.id); if (manga != null) {
EventBus.getDefault().postSticky(manga);
}
return intent; return intent;
} }
@ -59,23 +66,21 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
Intent intent = getIntent(); Intent intent = getIntent();
manga_id = intent.getLongExtra(MANGA_ID, -1); isOnline = intent.getBooleanExtra(MANGA_ONLINE, false);
is_online = intent.getBooleanExtra(MANGA_ONLINE, false);
setupViewPager(); setupViewPager();
if (savedState == null) requestPermissionsOnMarshmallow();
getPresenter().queryManga(manga_id);
} }
private void setupViewPager() { private void setupViewPager() {
adapter = new MangaDetailAdapter(getSupportFragmentManager(), this); adapter = new MangaDetailAdapter(getSupportFragmentManager(), this);
view_pager.setAdapter(adapter); viewPager.setAdapter(adapter);
tabs.setupWithViewPager(view_pager); tabs.setupWithViewPager(viewPager);
if (!is_online) if (!isOnline)
view_pager.setCurrentItem(MangaDetailAdapter.CHAPTERS_FRAGMENT); viewPager.setCurrentItem(MangaDetailAdapter.CHAPTERS_FRAGMENT);
} }
public void setManga(Manga manga) { public void setManga(Manga manga) {
@ -83,7 +88,22 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
} }
public boolean isCatalogueManga() { public boolean isCatalogueManga() {
return is_online; return isOnline;
}
private void requestPermissionsOnMarshmallow() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
if (ContextCompat.checkSelfPermission(this,
Manifest.permission.WRITE_EXTERNAL_STORAGE)
!= PackageManager.PERMISSION_GRANTED) {
ActivityCompat.requestPermissions(this,
new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE,
Manifest.permission.READ_EXTERNAL_STORAGE},
1);
}
}
} }
class MangaDetailAdapter extends FragmentPagerAdapter { class MangaDetailAdapter extends FragmentPagerAdapter {
@ -104,7 +124,7 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
}; };
pageCount = 2; pageCount = 2;
if (!is_online && mangaSyncManager.getMyAnimeList().isLogged()) if (!isOnline && mangaSyncManager.getMyAnimeList().isLogged())
pageCount++; pageCount++;
} }

View File

@ -2,49 +2,55 @@ package eu.kanade.tachiyomi.ui.manga;
import android.os.Bundle; import android.os.Bundle;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import javax.inject.Inject; import javax.inject.Inject;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.data.database.DatabaseHelper; import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.event.MangaEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import icepick.State; import icepick.State;
import rx.Observable; import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class MangaPresenter extends BasePresenter<MangaActivity> { public class MangaPresenter extends BasePresenter<MangaActivity> {
@Inject DatabaseHelper db; @Inject DatabaseHelper db;
@State long mangaId; @State Manga manga;
private static final int DB_MANGA = 1; private static final int GET_MANGA = 1;
@Override @Override
protected void onCreate(Bundle savedState) { protected void onCreate(Bundle savedState) {
super.onCreate(savedState); super.onCreate(savedState);
restartableLatestCache(DB_MANGA, this::getDbMangaObservable, MangaActivity::setManga); restartableLatestCache(GET_MANGA, this::getMangaObservable, MangaActivity::setManga);
if (savedState == null)
registerForEvents();
} }
@Override @Override
protected void onDestroy() { protected void onDestroy() {
super.onDestroy(); super.onDestroy();
// Avoid new instances receiving wrong manga // Avoid new instances receiving wrong manga
EventBus.getDefault().removeStickyEvent(Manga.class); EventBus.getDefault().removeStickyEvent(MangaEvent.class);
} }
private Observable<Manga> getDbMangaObservable() { private Observable<Manga> getMangaObservable() {
return db.getManga(mangaId).asRxObservable() return Observable.just(manga)
.subscribeOn(Schedulers.io()) .doOnNext(manga -> EventBus.getDefault().postSticky(new MangaEvent(manga)));
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(manga -> EventBus.getDefault().postSticky(manga));
} }
public void queryManga(long mangaId) { @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
this.mangaId = mangaId; public void onEvent(Manga manga) {
start(DB_MANGA); EventBus.getDefault().removeStickyEvent(manga);
unregisterForEvents();
this.manga = manga;
start(GET_MANGA);
} }
} }

View File

@ -10,6 +10,7 @@ import java.util.List;
import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter; import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
public class ChaptersAdapter extends FlexibleAdapter<ChaptersHolder, Chapter> { public class ChaptersAdapter extends FlexibleAdapter<ChaptersHolder, Chapter> {
@ -33,7 +34,8 @@ public class ChaptersAdapter extends FlexibleAdapter<ChaptersHolder, Chapter> {
@Override @Override
public void onBindViewHolder(ChaptersHolder holder, int position) { public void onBindViewHolder(ChaptersHolder holder, int position) {
final Chapter chapter = getItem(position); final Chapter chapter = getItem(position);
holder.onSetValues(fragment.getActivity(), chapter); final Manga manga = fragment.getPresenter().getManga();
holder.onSetValues(chapter, manga);
//When user scrolls this bind the correct selection status //When user scrolls this bind the correct selection status
holder.itemView.setActivated(isSelected(position)); holder.itemView.setActivated(isSelected(position));

View File

@ -10,6 +10,7 @@ import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.Menu; import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -18,12 +19,14 @@ import android.widget.ImageView;
import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.MaterialDialog;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter; import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.download.DownloadService; import eu.kanade.tachiyomi.data.download.DownloadService;
import eu.kanade.tachiyomi.data.download.model.Download; import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder; import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
@ -61,6 +64,12 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
return new ChaptersFragment(); return new ChaptersFragment();
} }
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setHasOptionsMenu(true);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
@ -71,26 +80,14 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
// Init RecyclerView and adapter // Init RecyclerView and adapter
linearLayout = new LinearLayoutManager(getActivity()); linearLayout = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(linearLayout); recyclerView.setLayoutManager(linearLayout);
recyclerView.addItemDecoration(new DividerItemDecoration(ContextCompat.getDrawable(getContext(), R.drawable.line_divider))); recyclerView.addItemDecoration(new DividerItemDecoration(
ContextCompat.getDrawable(getContext(), R.drawable.line_divider)));
recyclerView.setHasFixedSize(true); recyclerView.setHasFixedSize(true);
adapter = new ChaptersAdapter(this); adapter = new ChaptersAdapter(this);
recyclerView.setAdapter(adapter); recyclerView.setAdapter(adapter);
// Set initial values
setReadFilter();
setDownloadedFilter();
setSortIcon();
// Init listeners
swipeRefresh.setOnRefreshListener(this::fetchChapters); swipeRefresh.setOnRefreshListener(this::fetchChapters);
readCb.setOnCheckedChangeListener((arg, isChecked) ->
getPresenter().setReadFilter(isChecked));
downloadedCb.setOnCheckedChangeListener((v, isChecked) ->
getPresenter().setDownloadedFilter(isChecked));
sortBtn.setOnClickListener(v -> {
getPresenter().revertSortOrder();
setSortIcon();
});
nextUnreadBtn.setOnClickListener(v -> { nextUnreadBtn.setOnClickListener(v -> {
Chapter chapter = getPresenter().getNextUnreadChapter(); Chapter chapter = getPresenter().getNextUnreadChapter();
if (chapter != null) { if (chapter != null) {
@ -104,15 +101,52 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
} }
@Override @Override
public void onResume() { public void onPause() {
super.onResume(); // Stop recycler's scrolling when onPause is called. If the activity is finishing
observeChapterDownloadProgress(); // the presenter will be destroyed, and it could cause NPE
// https://github.com/inorichi/tachiyomi/issues/159
recyclerView.stopScroll();
super.onPause();
} }
@Override @Override
public void onPause() { public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
unsubscribeChapterDownloadProgress(); inflater.inflate(R.menu.chapters, menu);
super.onPause(); }
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_display_mode:
showDisplayModeDialog();
return true;
case R.id.manga_download:
showDownloadDialog();
return true;
}
return false;
}
public void onNextManga(Manga manga) {
// Remove listeners before setting the values
readCb.setOnCheckedChangeListener(null);
downloadedCb.setOnCheckedChangeListener(null);
sortBtn.setOnClickListener(null);
// Set initial values
setReadFilter();
setDownloadedFilter();
setSortIcon();
// Init listeners
readCb.setOnCheckedChangeListener((arg, isChecked) ->
getPresenter().setReadFilter(isChecked));
downloadedCb.setOnCheckedChangeListener((v, isChecked) ->
getPresenter().setDownloadedFilter(isChecked));
sortBtn.setOnClickListener(v -> {
getPresenter().revertSortOrder();
setSortIcon();
});
} }
public void onNextChapters(List<Chapter> chapters) { public void onNextChapters(List<Chapter> chapters) {
@ -143,9 +177,9 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
swipeRefresh.setRefreshing(false); swipeRefresh.setRefreshing(false);
} }
public void onFetchChaptersError() { public void onFetchChaptersError(Throwable error) {
swipeRefresh.setRefreshing(false); swipeRefresh.setRefreshing(false);
ToastUtil.showShort(getContext(), R.string.fetch_chapters_error); ToastUtil.showShort(getContext(), error.getMessage());
} }
public boolean isCatalogueManga() { public boolean isCatalogueManga() {
@ -158,6 +192,56 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
startActivity(intent); startActivity(intent);
} }
private void showDisplayModeDialog() {
final Manga manga = getPresenter().getManga();
if (manga == null)
return;
// Get available modes, ids and the selected mode
String[] modes = {getString(R.string.show_title), getString(R.string.show_chapter_number)};
int[] ids = {Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER};
int selectedIndex = manga.getDisplayMode() == Manga.DISPLAY_NAME ? 0 : 1;
new MaterialDialog.Builder(getActivity())
.title(R.string.action_display_mode)
.items(modes)
.itemsIds(ids)
.itemsCallbackSingleChoice(selectedIndex, (dialog, itemView, which, text) -> {
// Save the new display mode
getPresenter().setDisplayMode(itemView.getId());
// Refresh ui
adapter.notifyDataSetChanged();
return true;
})
.show();
}
private void showDownloadDialog() {
// Get available modes
String[] modes = {getString(R.string.download_all), getString(R.string.download_unread)};
new MaterialDialog.Builder(getActivity())
.title(R.string.manga_download)
.items(modes)
.itemsCallback((dialog, view, i, charSequence) -> {
List<Chapter> chapters = new ArrayList<>();
for(Chapter chapter : getPresenter().getChapters()) {
if(!chapter.isDownloaded()) {
if(i == 0 || (i == 1 && !chapter.read)) {
chapters.add(chapter);
}
}
}
if(chapters.size() > 0) {
onDownload(Observable.from(chapters));
}
})
.negativeText(R.string.button_cancel)
.show();
}
private void observeChapterDownloadProgress() { private void observeChapterDownloadProgress() {
downloadProgressSubscription = getPresenter().getDownloadProgressObs() downloadProgressSubscription = getPresenter().getDownloadProgressObs()
.subscribe(this::onDownloadProgressChange, .subscribe(this::onDownloadProgressChange,
@ -175,10 +259,10 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
holder.onProgressChange(getContext(), download.downloadedImages, download.pages.size()); holder.onProgressChange(getContext(), download.downloadedImages, download.pages.size());
} }
public void onChapterStatusChange(Chapter chapter) { public void onChapterStatusChange(Download download) {
ChaptersHolder holder = getHolder(chapter); ChaptersHolder holder = getHolder(download.chapter);
if (holder != null) if (holder != null)
holder.onStatusChange(chapter.status); holder.onStatusChange(download.getStatus());
} }
@Nullable @Nullable
@ -342,13 +426,13 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
public void setReadFilter() { public void setReadFilter() {
if (readCb != null) { if (readCb != null) {
readCb.setChecked(getPresenter().getReadFilter()); readCb.setChecked(getPresenter().onlyUnread());
} }
} }
public void setDownloadedFilter() { public void setDownloadedFilter() {
if (downloadedCb != null) { if (downloadedCb != null) {
downloadedCb.setChecked(getPresenter().getDownloadedFilter()); downloadedCb.setChecked(getPresenter().onlyDownloaded());
} }
} }

View File

@ -2,18 +2,22 @@ package eu.kanade.tachiyomi.ui.manga.chapter;
import android.content.Context; import android.content.Context;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.view.Menu;
import android.view.View; import android.view.View;
import android.widget.PopupMenu; import android.widget.PopupMenu;
import android.widget.RelativeLayout; import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import java.text.SimpleDateFormat; import java.text.DateFormat;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.util.Date; import java.util.Date;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter; import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.download.model.Download; import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder; import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import rx.Observable; import rx.Observable;
@ -21,33 +25,50 @@ import rx.Observable;
public class ChaptersHolder extends FlexibleViewHolder { public class ChaptersHolder extends FlexibleViewHolder {
private final ChaptersAdapter adapter; private final ChaptersAdapter adapter;
private Chapter item; private final int readColor;
private final int unreadColor;
private final DecimalFormat decimalFormat;
private final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
@Bind(R.id.chapter_title) TextView title; @Bind(R.id.chapter_title) TextView title;
@Bind(R.id.download_text) TextView downloadText; @Bind(R.id.download_text) TextView downloadText;
@Bind(R.id.chapter_menu) RelativeLayout chapterMenu; @Bind(R.id.chapter_menu) RelativeLayout chapterMenu;
@Bind(R.id.chapter_pages) TextView pages; @Bind(R.id.chapter_pages) TextView pages;
@Bind(R.id.chapter_date) TextView date; @Bind(R.id.chapter_date) TextView date;
private Context context;
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy"); private Chapter item;
public ChaptersHolder(View view, ChaptersAdapter adapter, OnListItemClickListener listener) { public ChaptersHolder(View view, ChaptersAdapter adapter, OnListItemClickListener listener) {
super(view, adapter, listener); super(view, adapter, listener);
this.adapter = adapter; this.adapter = adapter;
context = view.getContext();
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
readColor = ContextCompat.getColor(view.getContext(), R.color.hint_text);
unreadColor = ContextCompat.getColor(view.getContext(), R.color.primary_text);
DecimalFormatSymbols symbols = new DecimalFormatSymbols();
symbols.setDecimalSeparator('.');
decimalFormat = new DecimalFormat("#.###", symbols);
chapterMenu.setOnClickListener(v -> v.post(() -> showPopupMenu(v))); chapterMenu.setOnClickListener(v -> v.post(() -> showPopupMenu(v)));
} }
public void onSetValues(Context context, Chapter chapter) { public void onSetValues(Chapter chapter, Manga manga) {
this.item = chapter; this.item = chapter;
title.setText(chapter.name); String name;
switch (manga.getDisplayMode()) {
if (chapter.read) { case Manga.DISPLAY_NAME:
title.setTextColor(ContextCompat.getColor(context, R.color.hint_text)); default:
} else { name = chapter.name;
title.setTextColor(ContextCompat.getColor(context, R.color.primary_text)); break;
case Manga.DISPLAY_NUMBER:
String formattedNumber = decimalFormat.format(chapter.chapter_number);
name = context.getString(R.string.display_mode_chapter, formattedNumber);
break;
} }
title.setText(name);
title.setTextColor(chapter.read ? readColor : unreadColor);
date.setTextColor(chapter.read ? readColor : unreadColor);
if (!chapter.read && chapter.last_page_read > 0) { if (!chapter.read && chapter.last_page_read > 0) {
pages.setText(context.getString(R.string.chapter_progress, chapter.last_page_read + 1)); pages.setText(context.getString(R.string.chapter_progress, chapter.last_page_read + 1));
@ -56,7 +77,7 @@ public class ChaptersHolder extends FlexibleViewHolder {
} }
onStatusChange(chapter.status); onStatusChange(chapter.status);
date.setText(sdf.format(new Date(chapter.date_upload))); date.setText(df.format(new Date(chapter.date_upload)));
} }
public void onStatusChange(int status) { public void onStatusChange(int status) {
@ -86,19 +107,36 @@ public class ChaptersHolder extends FlexibleViewHolder {
// Inflate our menu resource into the PopupMenu's Menu // Inflate our menu resource into the PopupMenu's Menu
popup.getMenuInflater().inflate(R.menu.chapter_single, popup.getMenu()); popup.getMenuInflater().inflate(R.menu.chapter_single, popup.getMenu());
// Hide download and show delete if the chapter is downloaded and
if(item.isDownloaded()) {
Menu menu = popup.getMenu();
menu.findItem(R.id.action_download).setVisible(false);
menu.findItem(R.id.action_delete).setVisible(true);
}
// Hide mark as unread when the chapter is unread
if(!item.read && item.last_page_read == 0) {
popup.getMenu().findItem(R.id.action_mark_as_unread).setVisible(false);
}
// Hide mark as read when the chapter is read
if(item.read) {
popup.getMenu().findItem(R.id.action_mark_as_read).setVisible(false);
}
// Set a listener so we are notified if a menu item is clicked // Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener(menuItem -> { popup.setOnMenuItemClickListener(menuItem -> {
Observable<Chapter> chapter = Observable.just(item); Observable<Chapter> chapter = Observable.just(item);
switch (menuItem.getItemId()) { switch (menuItem.getItemId()) {
case R.id.action_mark_as_read:
return adapter.getFragment().onMarkAsRead(chapter);
case R.id.action_mark_as_unread:
return adapter.getFragment().onMarkAsUnread(chapter);
case R.id.action_download: case R.id.action_download:
return adapter.getFragment().onDownload(chapter); return adapter.getFragment().onDownload(chapter);
case R.id.action_delete: case R.id.action_delete:
return adapter.getFragment().onDelete(chapter); return adapter.getFragment().onDelete(chapter);
case R.id.action_mark_as_read:
return adapter.getFragment().onMarkAsRead(chapter);
case R.id.action_mark_as_unread:
return adapter.getFragment().onMarkAsUnread(chapter);
case R.id.action_mark_previous_as_read: case R.id.action_mark_previous_as_read:
return adapter.getFragment().onMarkPreviousAsRead(item); return adapter.getFragment().onMarkPreviousAsRead(item);
} }

View File

@ -3,11 +3,14 @@ package eu.kanade.tachiyomi.ui.manga.chapter;
import android.os.Bundle; import android.os.Bundle;
import android.util.Pair; import android.util.Pair;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.data.database.DatabaseHelper; import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.Chapter; import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.Manga;
@ -18,9 +21,9 @@ import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.base.Source; import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.event.ChapterCountEvent; import eu.kanade.tachiyomi.event.ChapterCountEvent;
import eu.kanade.tachiyomi.event.DownloadChaptersEvent; import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
import eu.kanade.tachiyomi.event.MangaEvent;
import eu.kanade.tachiyomi.event.ReaderEvent; import eu.kanade.tachiyomi.event.ReaderEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import eu.kanade.tachiyomi.util.EventBusHook;
import icepick.State; import icepick.State;
import rx.Observable; import rx.Observable;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
@ -38,48 +41,40 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
private Manga manga; private Manga manga;
private Source source; private Source source;
private List<Chapter> chapters; private List<Chapter> chapters;
private boolean sortOrderAToZ = true;
private boolean onlyUnread = true;
private boolean onlyDownloaded;
@State boolean hasRequested; @State boolean hasRequested;
private PublishSubject<List<Chapter>> chaptersSubject; private PublishSubject<List<Chapter>> chaptersSubject;
private static final int DB_CHAPTERS = 1; private static final int GET_MANGA = 1;
private static final int FETCH_CHAPTERS = 2; private static final int DB_CHAPTERS = 2;
private static final int CHAPTER_STATUS_CHANGES = 3; private static final int FETCH_CHAPTERS = 3;
private static final int CHAPTER_STATUS_CHANGES = 4;
@Override @Override
protected void onCreate(Bundle savedState) { protected void onCreate(Bundle savedState) {
super.onCreate(savedState); super.onCreate(savedState);
if (savedState != null) {
onProcessRestart();
}
chaptersSubject = PublishSubject.create(); chaptersSubject = PublishSubject.create();
restartableLatestCache(DB_CHAPTERS, startableLatestCache(GET_MANGA,
() -> Observable.just(manga),
ChaptersFragment::onNextManga);
startableLatestCache(DB_CHAPTERS,
this::getDbChaptersObs, this::getDbChaptersObs,
ChaptersFragment::onNextChapters); ChaptersFragment::onNextChapters);
restartableFirst(FETCH_CHAPTERS, startableFirst(FETCH_CHAPTERS,
this::getOnlineChaptersObs, this::getOnlineChaptersObs,
(view, result) -> view.onFetchChaptersDone(), (view, result) -> view.onFetchChaptersDone(),
(view, error) -> view.onFetchChaptersError()); (view, error) -> view.onFetchChaptersError(error));
restartableLatestCache(CHAPTER_STATUS_CHANGES, startableLatestCache(CHAPTER_STATUS_CHANGES,
this::getChapterStatusObs, this::getChapterStatusObs,
(view, download) -> view.onChapterStatusChange(download.chapter), (view, download) -> view.onChapterStatusChange(download),
(view, error) -> Timber.e(error.getCause(), error.getMessage())); (view, error) -> Timber.e(error.getCause(), error.getMessage()));
registerForStickyEvents(); registerForEvents();
}
private void onProcessRestart() {
stop(DB_CHAPTERS);
stop(FETCH_CHAPTERS);
stop(CHAPTER_STATUS_CHANGES);
} }
@Override @Override
@ -89,11 +84,12 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
super.onDestroy(); super.onDestroy();
} }
@EventBusHook @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(Manga manga) { public void onEvent(MangaEvent event) {
this.manga = manga; this.manga = event.manga;
start(GET_MANGA);
if (!isSubscribed(DB_CHAPTERS)) { if (isUnsubscribed(DB_CHAPTERS)) {
source = sourceManager.get(manga.source); source = sourceManager.get(manga.source);
start(DB_CHAPTERS); start(DB_CHAPTERS);
@ -135,13 +131,13 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
private Observable<List<Chapter>> applyChapterFilters(List<Chapter> chapters) { private Observable<List<Chapter>> applyChapterFilters(List<Chapter> chapters) {
Observable<Chapter> observable = Observable.from(chapters) Observable<Chapter> observable = Observable.from(chapters)
.subscribeOn(Schedulers.io()); .subscribeOn(Schedulers.io());
if (onlyUnread) { if (onlyUnread()) {
observable = observable.filter(chapter -> !chapter.read); observable = observable.filter(chapter -> !chapter.read);
} }
if (onlyDownloaded) { if (onlyDownloaded()) {
observable = observable.filter(chapter -> chapter.status == Download.DOWNLOADED); observable = observable.filter(chapter -> chapter.status == Download.DOWNLOADED);
} }
return observable.toSortedList((chapter, chapter2) -> sortOrderAToZ ? return observable.toSortedList((chapter, chapter2) -> getSortOrder() ?
Float.compare(chapter2.chapter_number, chapter.chapter_number) : Float.compare(chapter2.chapter_number, chapter.chapter_number) :
Float.compare(chapter.chapter_number, chapter2.chapter_number)); Float.compare(chapter.chapter_number, chapter2.chapter_number));
} }
@ -175,7 +171,7 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
break; break;
} }
} }
if (onlyDownloaded && download.getStatus() == Download.DOWNLOADED) if (onlyDownloaded() && download.getStatus() == Download.DOWNLOADED)
refreshChapters(); refreshChapters();
} }
@ -231,7 +227,7 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
}, error -> { }, error -> {
Timber.e(error.getMessage()); Timber.e(error.getMessage());
}, () -> { }, () -> {
if (onlyDownloaded) if (onlyDownloaded())
refreshChapters(); refreshChapters();
})); }));
} }
@ -241,32 +237,38 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
} }
public void revertSortOrder() { public void revertSortOrder() {
//TODO manga.chapter_order manga.setChapterOrder(getSortOrder() ? Manga.SORT_ZA : Manga.SORT_AZ);
sortOrderAToZ = !sortOrderAToZ; db.insertManga(manga).executeAsBlocking();
refreshChapters(); refreshChapters();
} }
public void setReadFilter(boolean onlyUnread) { public void setReadFilter(boolean onlyUnread) {
//TODO do we need save filter for manga? manga.setReadFilter(onlyUnread ? Manga.SHOW_UNREAD : Manga.SHOW_ALL);
this.onlyUnread = onlyUnread; db.insertManga(manga).executeAsBlocking();
refreshChapters(); refreshChapters();
} }
public void setDownloadedFilter(boolean onlyDownloaded) { public void setDownloadedFilter(boolean onlyDownloaded) {
this.onlyDownloaded = onlyDownloaded; manga.setDownloadedFilter(onlyDownloaded ? Manga.SHOW_DOWNLOADED : Manga.SHOW_ALL);
db.insertManga(manga).executeAsBlocking();
refreshChapters(); refreshChapters();
} }
public void setDisplayMode(int mode) {
manga.setDisplayMode(mode);
db.insertManga(manga).executeAsBlocking();
}
public boolean onlyDownloaded() {
return manga.getDownloadedFilter() == Manga.SHOW_DOWNLOADED;
}
public boolean onlyUnread() {
return manga.getReadFilter() == Manga.SHOW_UNREAD;
}
public boolean getSortOrder() { public boolean getSortOrder() {
return sortOrderAToZ; return manga.sortChaptersAZ();
}
public boolean getReadFilter() {
return onlyUnread;
}
public boolean getDownloadedFilter() {
return onlyDownloaded;
} }
public Manga getManga() { public Manga getManga() {

View File

@ -1,11 +1,12 @@
package eu.kanade.tachiyomi.ui.manga.info; package eu.kanade.tachiyomi.ui.manga.info;
import android.os.Bundle; import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.SwipeRefreshLayout; import android.support.v4.widget.SwipeRefreshLayout;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
@ -16,100 +17,227 @@ import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.cache.CoverCache; import eu.kanade.tachiyomi.data.cache.CoverCache;
import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment; import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
import nucleus.factory.RequiresPresenter; import nucleus.factory.RequiresPresenter;
/**
* Fragment that shows manga information.
* Uses R.layout.fragment_manga_info.
* UI related actions should be called from here.
*/
@RequiresPresenter(MangaInfoPresenter.class) @RequiresPresenter(MangaInfoPresenter.class)
public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> { public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
/**
* SwipeRefreshLayout showing refresh status
*/
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh; @Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
/**
* TextView containing artist information.
*/
@Bind(R.id.manga_artist) TextView artist; @Bind(R.id.manga_artist) TextView artist;
/**
* TextView containing author information.
*/
@Bind(R.id.manga_author) TextView author; @Bind(R.id.manga_author) TextView author;
/**
* TextView containing chapter count.
*/
@Bind(R.id.manga_chapters) TextView chapterCount; @Bind(R.id.manga_chapters) TextView chapterCount;
/**
* TextView containing genres.
*/
@Bind(R.id.manga_genres) TextView genres; @Bind(R.id.manga_genres) TextView genres;
/**
* TextView containing status (ongoing, finished).
*/
@Bind(R.id.manga_status) TextView status; @Bind(R.id.manga_status) TextView status;
/**
* TextView containing source.
*/
@Bind(R.id.manga_source) TextView source;
/**
* TextView containing manga summary.
*/
@Bind(R.id.manga_summary) TextView description; @Bind(R.id.manga_summary) TextView description;
/**
* ImageView of cover.
*/
@Bind(R.id.manga_cover) ImageView cover; @Bind(R.id.manga_cover) ImageView cover;
@Bind(R.id.action_favorite) Button favoriteBtn; /**
* ImageView containing manga cover shown as blurred backdrop.
*/
@Bind(R.id.backdrop) ImageView backdrop;
/**
* FAB anchored to bottom of top view used to (add / remove) manga (to / from) library.
*/
@Bind(R.id.fab_favorite) FloatingActionButton fabFavorite;
/**
* Create new instance of MangaInfoFragment.
*
* @return MangaInfoFragment.
*/
public static MangaInfoFragment newInstance() { public static MangaInfoFragment newInstance() {
return new MangaInfoFragment(); return new MangaInfoFragment();
} }
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
setHasOptionsMenu(true);
}
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) { Bundle savedInstanceState) {
// Inflate the layout for this fragment // Inflate the layout for this fragment.
View view = inflater.inflate(R.layout.fragment_manga_info, container, false); View view = inflater.inflate(R.layout.fragment_manga_info, container, false);
// Bind layout objects.
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
favoriteBtn.setOnClickListener(v -> { // Set onclickListener to toggle favorite when FAB clicked.
getPresenter().toggleFavorite(); fabFavorite.setOnClickListener(v -> getPresenter().toggleFavorite());
});
// Set SwipeRefresh to refresh manga data.
swipeRefresh.setOnRefreshListener(this::fetchMangaFromSource); swipeRefresh.setOnRefreshListener(this::fetchMangaFromSource);
return view; return view;
} }
public void onNextManga(Manga manga) { /**
* Check if manga is initialized.
* If true update view with manga information,
* if false fetch manga information
*
* @param manga manga object containing information about manga.
* @param source the source of the manga.
*/
public void onNextManga(Manga manga, Source source) {
if (manga.initialized) { if (manga.initialized) {
setMangaInfo(manga); // Update view.
setMangaInfo(manga, source);
} else { } else {
// Initialize manga // Initialize manga.
fetchMangaFromSource(); fetchMangaFromSource();
} }
} }
private void setMangaInfo(Manga manga) { /**
* Update the view with manga information.
*
* @param manga manga object containing information about manga.
* @param mangaSource the source of the manga.
*/
private void setMangaInfo(Manga manga, Source mangaSource) {
// Update artist TextView.
artist.setText(manga.artist); artist.setText(manga.artist);
// Update author TextView.
author.setText(manga.author); author.setText(manga.author);
// If manga source is known update source TextView.
if (mangaSource != null) {
source.setText(mangaSource.getName());
}
// Update genres TextView.
genres.setText(manga.genre); genres.setText(manga.genre);
// Update status TextView.
status.setText(manga.getStatus(getActivity())); status.setText(manga.getStatus(getActivity()));
// Update description TextView.
description.setText(manga.description); description.setText(manga.description);
setFavoriteText(manga.favorite); // Set the favorite drawable to the correct one.
setFavoriteDrawable(manga.favorite);
// Initialize CoverCache and Glide headers to retrieve cover information.
CoverCache coverCache = getPresenter().coverCache; CoverCache coverCache = getPresenter().coverCache;
LazyHeaders headers = getPresenter().source.getGlideHeaders(); LazyHeaders headers = getPresenter().source.getGlideHeaders();
if (manga.thumbnail_url != null && cover.getDrawable() == null) {
if (manga.favorite) { // Check if thumbnail_url is given.
coverCache.saveAndLoadFromCache(cover, manga.thumbnail_url, headers); if (manga.thumbnail_url != null) {
} else { // Check if cover is already drawn.
coverCache.loadFromNetwork(cover, manga.thumbnail_url, headers); if (cover.getDrawable() == null) {
// If manga is in library then (download / save) (from / to) local cache if available,
// else download from network.
if (manga.favorite) {
coverCache.saveOrLoadFromCache(cover, manga.thumbnail_url, headers);
} else {
coverCache.loadFromNetwork(cover, manga.thumbnail_url, headers);
}
}
// Check if backdrop is already drawn.
if (backdrop.getDrawable() == null) {
// If manga is in library then (download / save) (from / to) local cache if available,
// else download from network.
if (manga.favorite) {
coverCache.saveOrLoadFromCache(backdrop, manga.thumbnail_url, headers);
} else {
coverCache.loadFromNetwork(backdrop, manga.thumbnail_url, headers);
}
} }
} }
} }
/**
* Update chapter count TextView.
*
* @param count number of chapters.
*/
public void setChapterCount(int count) { public void setChapterCount(int count) {
chapterCount.setText(String.valueOf(count)); chapterCount.setText(String.valueOf(count));
} }
public void setFavoriteText(boolean isFavorite) { /**
favoriteBtn.setText(!isFavorite ? R.string.add_to_library : R.string.remove_from_library); * Update FAB with correct drawable.
*
* @param isFavorite determines if manga is favorite or not.
*/
private void setFavoriteDrawable(boolean isFavorite) {
// Set the Favorite drawable to the correct one.
// Border drawable if false, filled drawable if true.
fabFavorite.setImageDrawable(ContextCompat.getDrawable(getContext(), isFavorite ?
R.drawable.ic_bookmark_white_24dp :
R.drawable.ic_bookmark_border_white_24dp));
} }
/**
* Start fetching manga information from source.
*/
private void fetchMangaFromSource() { private void fetchMangaFromSource() {
setRefreshing(true); setRefreshing(true);
// Call presenter and start fetching manga information
getPresenter().fetchMangaFromSource(); getPresenter().fetchMangaFromSource();
} }
/**
* Update swipeRefresh to stop showing refresh in progress spinner.
*/
public void onFetchMangaDone() { public void onFetchMangaDone() {
setRefreshing(false); setRefreshing(false);
} }
/**
* Update swipeRefresh to start showing refresh in progress spinner.
*/
public void onFetchMangaError() { public void onFetchMangaError() {
setRefreshing(false); setRefreshing(false);
} }
/**
* Set swipeRefresh status.
*
* @param value status of manga fetch.
*/
private void setRefreshing(boolean value) { private void setRefreshing(boolean value) {
swipeRefresh.setRefreshing(value); swipeRefresh.setRefreshing(value);
} }

View File

@ -2,6 +2,9 @@ package eu.kanade.tachiyomi.ui.manga.info;
import android.os.Bundle; import android.os.Bundle;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import javax.inject.Inject; import javax.inject.Inject;
import eu.kanade.tachiyomi.data.cache.CoverCache; import eu.kanade.tachiyomi.data.cache.CoverCache;
@ -10,56 +13,86 @@ import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.SourceManager; import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.base.Source; import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.event.ChapterCountEvent; import eu.kanade.tachiyomi.event.ChapterCountEvent;
import eu.kanade.tachiyomi.event.MangaEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import eu.kanade.tachiyomi.util.EventBusHook;
import rx.Observable; import rx.Observable;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers; import rx.schedulers.Schedulers;
/**
* Presenter of MangaInfoFragment.
* Contains information and data for fragment.
* Observable updates should be called from here.
*/
public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> { public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
/**
* The id of the restartable.
*/
private static final int GET_MANGA = 1;
/**
* The id of the restartable.
*/
private static final int GET_CHAPTER_COUNT = 2;
/**
* The id of the restartable.
*/
private static final int FETCH_MANGA_INFO = 3;
/**
* Source information.
*/
protected Source source;
/**
* Used to connect to database.
*/
@Inject DatabaseHelper db; @Inject DatabaseHelper db;
/**
* Used to connect to different manga sources.
*/
@Inject SourceManager sourceManager; @Inject SourceManager sourceManager;
/**
* Used to connect to cache.
*/
@Inject CoverCache coverCache; @Inject CoverCache coverCache;
/**
* Selected manga information.
*/
private Manga manga; private Manga manga;
protected Source source;
/**
* Count of chapters.
*/
private int count = -1; private int count = -1;
private boolean isFetching;
private static final int GET_MANGA = 1;
private static final int GET_CHAPTER_COUNT = 2;
private static final int FETCH_MANGA_INFO = 3;
@Override @Override
protected void onCreate(Bundle savedState) { protected void onCreate(Bundle savedState) {
super.onCreate(savedState); super.onCreate(savedState);
if (savedState != null) { // Notify the view a manga is available or has changed.
onProcessRestart(); startableLatestCache(GET_MANGA,
}
restartableLatestCache(GET_MANGA,
() -> Observable.just(manga), () -> Observable.just(manga),
MangaInfoFragment::onNextManga); (view, manga) -> view.onNextManga(manga, source));
restartableLatestCache(GET_CHAPTER_COUNT, // Update chapter count.
startableLatestCache(GET_CHAPTER_COUNT,
() -> Observable.just(count), () -> Observable.just(count),
MangaInfoFragment::setChapterCount); MangaInfoFragment::setChapterCount);
restartableFirst(FETCH_MANGA_INFO, // Fetch manga info from source.
startableFirst(FETCH_MANGA_INFO,
this::fetchMangaObs, this::fetchMangaObs,
(view, manga) -> view.onFetchMangaDone(), (view, manga) -> view.onFetchMangaDone(),
(view, error) -> view.onFetchMangaError()); (view, error) -> view.onFetchMangaError());
registerForStickyEvents(); // Listen for events.
} registerForEvents();
private void onProcessRestart() {
stop(GET_MANGA);
stop(GET_CHAPTER_COUNT);
stop(FETCH_MANGA_INFO);
} }
@Override @Override
@ -68,28 +101,36 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
super.onDestroy(); super.onDestroy();
} }
@EventBusHook @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(Manga manga) { public void onEvent(MangaEvent event) {
this.manga = manga; this.manga = event.manga;
source = sourceManager.get(manga.source); source = sourceManager.get(manga.source);
start(GET_MANGA); refreshManga();
} }
@EventBusHook @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(ChapterCountEvent event) { public void onEvent(ChapterCountEvent event) {
if (count != event.getCount()) { if (count != event.getCount()) {
count = event.getCount(); count = event.getCount();
// Update chapter count
start(GET_CHAPTER_COUNT); start(GET_CHAPTER_COUNT);
} }
} }
/**
* Fetch manga information from source.
*/
public void fetchMangaFromSource() { public void fetchMangaFromSource() {
if (!isFetching) { if (isUnsubscribed(FETCH_MANGA_INFO)) {
isFetching = true;
start(FETCH_MANGA_INFO); start(FETCH_MANGA_INFO);
} }
} }
/**
* Fetch manga information from source.
*
* @return manga information.
*/
private Observable<Manga> fetchMangaObs() { private Observable<Manga> fetchMangaObs() {
return source.pullMangaFromNetwork(manga.url) return source.pullMangaFromNetwork(manga.url)
.flatMap(networkManga -> { .flatMap(networkManga -> {
@ -97,23 +138,40 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
db.insertManga(manga).executeAsBlocking(); db.insertManga(manga).executeAsBlocking();
return Observable.just(manga); return Observable.just(manga);
}) })
.finallyDo(() -> isFetching = false)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()); .observeOn(AndroidSchedulers.mainThread())
.doOnNext(manga -> refreshManga());
} }
/**
* Update favorite status of manga, (removes / adds) manga (to / from) library.
*/
public void toggleFavorite() { public void toggleFavorite() {
manga.favorite = !manga.favorite; manga.favorite = !manga.favorite;
onMangaFavoriteChange(manga.favorite); onMangaFavoriteChange(manga.favorite);
db.insertManga(manga).executeAsBlocking(); db.insertManga(manga).executeAsBlocking();
refreshManga();
} }
/**
* (Removes / Saves) cover depending on favorite status.
*
* @param isFavorite determines if manga is favorite or not.
*/
private void onMangaFavoriteChange(boolean isFavorite) { private void onMangaFavoriteChange(boolean isFavorite) {
if (isFavorite) { if (isFavorite) {
coverCache.save(manga.thumbnail_url, source.getGlideHeaders()); coverCache.save(manga.thumbnail_url, source.getGlideHeaders());
} else { } else {
coverCache.delete(manga.thumbnail_url); coverCache.deleteCoverFromCache(manga.thumbnail_url);
} }
} }
/**
* Refresh MangaInfo view.
*/
private void refreshManga() {
start(GET_MANGA);
}
} }

View File

@ -11,7 +11,6 @@ import android.view.View;
import android.widget.EditText; import android.widget.EditText;
import android.widget.ListView; import android.widget.ListView;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import android.widget.TextView;
import com.afollestad.materialdialogs.MaterialDialog; import com.afollestad.materialdialogs.MaterialDialog;
@ -25,11 +24,6 @@ import eu.kanade.tachiyomi.data.database.models.MangaSync;
import rx.Subscription; import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
import rx.subjects.PublishSubject; import rx.subjects.PublishSubject;
import uk.co.ribot.easyadapter.EasyAdapter;
import uk.co.ribot.easyadapter.ItemViewHolder;
import uk.co.ribot.easyadapter.PositionInfo;
import uk.co.ribot.easyadapter.annotations.LayoutId;
import uk.co.ribot.easyadapter.annotations.ViewId;
public class MyAnimeListDialogFragment extends DialogFragment { public class MyAnimeListDialogFragment extends DialogFragment {
@ -37,7 +31,7 @@ public class MyAnimeListDialogFragment extends DialogFragment {
@Bind(R.id.myanimelist_search_results) ListView searchResults; @Bind(R.id.myanimelist_search_results) ListView searchResults;
@Bind(R.id.progress) ProgressBar progressBar; @Bind(R.id.progress) ProgressBar progressBar;
private EasyAdapter<MangaSync> adapter; private MyAnimeListSearchAdapter adapter;
private MangaSync selectedItem; private MangaSync selectedItem;
private Subscription searchSubscription; private Subscription searchSubscription;
@ -59,7 +53,7 @@ public class MyAnimeListDialogFragment extends DialogFragment {
ButterKnife.bind(this, dialog.getView()); ButterKnife.bind(this, dialog.getView());
// Create adapter // Create adapter
adapter = new EasyAdapter<>(getActivity(), ResultViewHolder.class); adapter = new MyAnimeListSearchAdapter(getActivity());
searchResults.setAdapter(adapter); searchResults.setAdapter(adapter);
// Set listeners // Set listeners
@ -125,7 +119,7 @@ public class MyAnimeListDialogFragment extends DialogFragment {
public void onSearchResultsError() { public void onSearchResultsError() {
progressBar.setVisibility(View.GONE); progressBar.setVisibility(View.GONE);
searchResults.setVisibility(View.VISIBLE); searchResults.setVisibility(View.VISIBLE);
adapter.getItems().clear(); adapter.clear();
} }
public MyAnimeListFragment getMALFragment() { public MyAnimeListFragment getMALFragment() {
@ -136,21 +130,6 @@ public class MyAnimeListDialogFragment extends DialogFragment {
return getMALFragment().getPresenter(); return getMALFragment().getPresenter();
} }
@LayoutId(R.layout.dialog_myanimelist_search_item)
public static class ResultViewHolder extends ItemViewHolder<MangaSync> {
@ViewId(R.id.myanimelist_result_title) TextView title;
public ResultViewHolder(View view) {
super(view);
}
@Override
public void onSetValues(MangaSync chapter, PositionInfo positionInfo) {
title.setText(chapter.title);
}
}
private static class SimpleTextChangeListener implements TextWatcher { private static class SimpleTextChangeListener implements TextWatcher {
@Override @Override

View File

@ -4,6 +4,11 @@ import android.content.Context;
import android.os.Bundle; import android.os.Bundle;
import android.text.TextUtils; import android.text.TextUtils;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
@ -12,8 +17,8 @@ import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.database.models.MangaSync; import eu.kanade.tachiyomi.data.database.models.MangaSync;
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager; import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList; import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
import eu.kanade.tachiyomi.event.MangaEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import eu.kanade.tachiyomi.util.EventBusHook;
import eu.kanade.tachiyomi.util.ToastUtil; import eu.kanade.tachiyomi.util.ToastUtil;
import rx.Observable; import rx.Observable;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
@ -35,27 +40,23 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
private static final int GET_SEARCH_RESULTS = 2; private static final int GET_SEARCH_RESULTS = 2;
private static final int REFRESH = 3; private static final int REFRESH = 3;
private static final String PREFIX_MY = "my:";
@Override @Override
protected void onCreate(Bundle savedState) { protected void onCreate(Bundle savedState) {
super.onCreate(savedState); super.onCreate(savedState);
if (savedState != null) {
onProcessRestart();
}
myAnimeList = syncManager.getMyAnimeList(); myAnimeList = syncManager.getMyAnimeList();
restartableLatestCache(GET_MANGA_SYNC, startableLatestCache(GET_MANGA_SYNC,
() -> db.getMangaSync(manga, myAnimeList).asRxObservable() () -> db.getMangaSync(manga, myAnimeList).asRxObservable()
.doOnNext(mangaSync -> this.mangaSync = mangaSync) .doOnNext(mangaSync -> this.mangaSync = mangaSync)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()), .observeOn(AndroidSchedulers.mainThread()),
MyAnimeListFragment::setMangaSync); MyAnimeListFragment::setMangaSync);
restartableLatestCache(GET_SEARCH_RESULTS, startableLatestCache(GET_SEARCH_RESULTS,
() -> myAnimeList.search(query) this::getSearchResultsObservable,
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()),
(view, results) -> { (view, results) -> {
view.setSearchResults(results); view.setSearchResults(results);
}, (view, error) -> { }, (view, error) -> {
@ -63,7 +64,7 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
view.setSearchResultsError(); view.setSearchResultsError();
}); });
restartableFirst(REFRESH, startableFirst(REFRESH,
() -> myAnimeList.getList() () -> myAnimeList.getList()
.flatMap(myList -> { .flatMap(myList -> {
for (MangaSync myManga : myList) { for (MangaSync myManga : myList) {
@ -83,16 +84,10 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
} }
private void onProcessRestart() {
stop(GET_MANGA_SYNC);
stop(GET_SEARCH_RESULTS);
stop(REFRESH);
}
@Override @Override
protected void onTakeView(MyAnimeListFragment view) { protected void onTakeView(MyAnimeListFragment view) {
super.onTakeView(view); super.onTakeView(view);
registerForStickyEvents(); registerForEvents();
} }
@Override @Override
@ -101,12 +96,28 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
super.onDropView(); super.onDropView();
} }
@EventBusHook @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
public void onEventMainThread(Manga manga) { public void onEvent(MangaEvent event) {
this.manga = manga; this.manga = event.manga;
start(GET_MANGA_SYNC); start(GET_MANGA_SYNC);
} }
private Observable<List<MangaSync>> getSearchResultsObservable() {
Observable<List<MangaSync>> observable;
if (query.startsWith(PREFIX_MY)) {
String realQuery = query.substring(PREFIX_MY.length()).toLowerCase().trim();
observable = myAnimeList.getList()
.flatMap(Observable::from)
.filter(manga -> manga.title.toLowerCase().contains(realQuery))
.toList();
} else {
observable = myAnimeList.search(query);
}
return observable
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
private void updateRemote() { private void updateRemote() {
add(myAnimeList.update(mangaSync) add(myAnimeList.update(mangaSync)
.flatMap(response -> db.insertMangaSync(mangaSync).asRxObservable()) .flatMap(response -> db.insertMangaSync(mangaSync).asRxObservable())

View File

@ -0,0 +1,61 @@
package eu.kanade.tachiyomi.ui.manga.myanimelist;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import java.util.ArrayList;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.MangaSync;
public class MyAnimeListSearchAdapter extends ArrayAdapter<MangaSync> {
public MyAnimeListSearchAdapter(Context context) {
super(context, R.layout.dialog_myanimelist_search_item, new ArrayList<>());
}
@Override
public View getView(int position, View view, ViewGroup parent) {
// Get the data item for this position
MangaSync sync = getItem(position);
// Check if an existing view is being reused, otherwise inflate the view
SearchViewHolder holder; // view lookup cache stored in tag
if (view == null) {
LayoutInflater inflater = LayoutInflater.from(getContext());
view = inflater.inflate(R.layout.dialog_myanimelist_search_item, parent, false);
holder = new SearchViewHolder(view);
view.setTag(holder);
} else {
holder = (SearchViewHolder) view.getTag();
}
holder.onSetValues(sync);
return view;
}
public void setItems(List<MangaSync> syncs) {
setNotifyOnChange(false);
clear();
addAll(syncs);
notifyDataSetChanged();
}
public static class SearchViewHolder {
@Bind(R.id.myanimelist_result_title) TextView title;
public SearchViewHolder(View view) {
ButterKnife.bind(this, view);
}
public void onSetValues(MangaSync sync) {
title.setText(sync.title);
}
}
}

View File

@ -3,15 +3,17 @@ package eu.kanade.tachiyomi.ui.reader;
import android.content.Context; import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.pm.ActivityInfo; import android.content.pm.ActivityInfo;
import android.content.res.Configuration;
import android.graphics.Color; import android.graphics.Color;
import android.os.Build; import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.view.KeyEvent;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.Surface;
import android.view.View; import android.view.View;
import android.view.WindowManager; import android.view.WindowManager;
import android.widget.TextView; import android.widget.TextView;
@ -20,11 +22,8 @@ import com.afollestad.materialdialogs.MaterialDialog;
import java.util.List; import java.util.List;
import javax.inject.Inject;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter; import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.Manga;
@ -49,8 +48,6 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
@Bind(R.id.page_number) TextView pageNumber; @Bind(R.id.page_number) TextView pageNumber;
@Bind(R.id.toolbar) Toolbar toolbar; @Bind(R.id.toolbar) Toolbar toolbar;
@Inject PreferencesHelper preferences;
private BaseReader viewer; private BaseReader viewer;
private ReaderMenu readerMenu; private ReaderMenu readerMenu;
@ -75,7 +72,6 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
@Override @Override
public void onCreate(Bundle savedState) { public void onCreate(Bundle savedState) {
super.onCreate(savedState); super.onCreate(savedState);
App.get(this).getComponent().inject(this);
setContentView(R.layout.activity_reader); setContentView(R.layout.activity_reader);
ButterKnife.bind(this); ButterKnife.bind(this);
@ -158,37 +154,90 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
} }
} }
@Override
public boolean dispatchKeyEvent(KeyEvent event) {
int action = event.getAction();
int keyCode = event.getKeyCode();
switch (keyCode) {
case KeyEvent.KEYCODE_VOLUME_DOWN:
if (action == KeyEvent.ACTION_UP && viewer != null)
viewer.moveToNext();
return true;
case KeyEvent.KEYCODE_VOLUME_UP:
if (action == KeyEvent.ACTION_UP && viewer != null)
viewer.moveToPrevious();
return true;
default:
return super.dispatchKeyEvent(event);
}
}
public void onChapterError() { public void onChapterError() {
finish(); finish();
ToastUtil.showShort(this, R.string.page_list_error); ToastUtil.showShort(this, R.string.page_list_error);
} }
public void onChapterReady(List<Page> pages, Manga manga, Chapter chapter, int currentPage) { public void onChapterAppendError() {
if (viewer == null) { // Ignore
viewer = createViewer(manga); }
getSupportFragmentManager().beginTransaction().replace(R.id.reader, viewer).commit();
public void onChapterReady(Manga manga, Chapter chapter, Page currentPage) {
List<Page> pages = chapter.getPages();
if (currentPage == null) {
currentPage = pages.get(pages.size() - 1);
} }
viewer.onPageListReady(pages, currentPage);
readerMenu.onChapterReady(pages.size(), manga, chapter, currentPage); if (viewer == null) {
viewer = getOrCreateViewer(manga);
}
viewer.onPageListReady(chapter, currentPage);
readerMenu.setActiveManga(manga);
readerMenu.setActiveChapter(chapter, currentPage.getPageNumber());
}
public void onEnterChapter(Chapter chapter, int currentPage) {
if (currentPage == -1) {
currentPage = chapter.getPages().size() - 1;
}
getPresenter().setActiveChapter(chapter);
readerMenu.setActiveChapter(chapter, currentPage);
}
public void onAppendChapter(Chapter chapter) {
viewer.onPageListAppendReady(chapter);
} }
public void onAdjacentChapters(Chapter previous, Chapter next) { public void onAdjacentChapters(Chapter previous, Chapter next) {
readerMenu.onAdjacentChapters(previous, next); readerMenu.onAdjacentChapters(previous, next);
} }
private BaseReader createViewer(Manga manga) { private BaseReader getOrCreateViewer(Manga manga) {
int mangaViewer = manga.viewer == 0 ? preferences.getDefaultViewer() : manga.viewer; int mangaViewer = manga.viewer == 0 ? getPreferences().getDefaultViewer() : manga.viewer;
switch (mangaViewer) { FragmentManager fm = getSupportFragmentManager();
case LEFT_TO_RIGHT: default:
return new LeftToRightReader(); // Try to reuse the viewer using its tag
case RIGHT_TO_LEFT: BaseReader fragment = (BaseReader) fm.findFragmentByTag(manga.viewer + "");
return new RightToLeftReader(); if (fragment == null) {
case VERTICAL: // Create a new viewer
return new VerticalReader(); switch (mangaViewer) {
case WEBTOON: case LEFT_TO_RIGHT: default:
return new WebtoonReader(); fragment = new LeftToRightReader();
break;
case RIGHT_TO_LEFT:
fragment = new RightToLeftReader();
break;
case VERTICAL:
fragment = new VerticalReader();
break;
case WEBTOON:
fragment = new WebtoonReader();
break;
}
fm.beginTransaction().replace(R.id.reader, fragment, manga.viewer + "").commit();
} }
return fragment;
} }
public void onPageChanged(int currentPageIndex, int totalPages) { public void onPageChanged(int currentPageIndex, int totalPages) {
@ -197,8 +246,9 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
readerMenu.onPageChanged(currentPageIndex); readerMenu.onPageChanged(currentPageIndex);
} }
public void setSelectedPage(int pageIndex) { public void gotoPageInCurrentChapter(int pageIndex) {
viewer.setSelectedPage(pageIndex); Page requestedPage = viewer.getCurrentPage().getChapter().getPages().get(pageIndex);
viewer.setSelectedPage(requestedPage);
} }
public void onCenterSingleTap() { public void onCenterSingleTap() {
@ -206,7 +256,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
} }
public void requestNextChapter() { public void requestNextChapter() {
getPresenter().setCurrentPage(viewer != null ? viewer.getCurrentPage() : 0); getPresenter().setCurrentPage(viewer.getCurrentPage());
if (!getPresenter().loadNextChapter()) { if (!getPresenter().loadNextChapter()) {
ToastUtil.showShort(this, R.string.no_next_chapter); ToastUtil.showShort(this, R.string.no_next_chapter);
} }
@ -214,20 +264,22 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
} }
public void requestPreviousChapter() { public void requestPreviousChapter() {
getPresenter().setCurrentPage(viewer != null ? viewer.getCurrentPage() : 0); getPresenter().setCurrentPage(viewer.getCurrentPage());
if (!getPresenter().loadPreviousChapter()) { if (!getPresenter().loadPreviousChapter()) {
ToastUtil.showShort(this, R.string.no_previous_chapter); ToastUtil.showShort(this, R.string.no_previous_chapter);
} }
} }
private void initializeSettings() { private void initializeSettings() {
PreferencesHelper preferences = getPreferences();
subscriptions.add(preferences.showPageNumber() subscriptions.add(preferences.showPageNumber()
.asObservable() .asObservable()
.subscribe(this::setPageNumberVisibility)); .subscribe(this::setPageNumberVisibility));
subscriptions.add(preferences.lockOrientation() subscriptions.add(preferences.rotation()
.asObservable() .asObservable()
.subscribe(this::setOrientation)); .subscribe(this::setRotation));
subscriptions.add(preferences.hideStatusBar() subscriptions.add(preferences.hideStatusBar()
.asObservable() .asObservable()
@ -247,28 +299,25 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
.subscribe(this::applyTheme)); .subscribe(this::applyTheme));
} }
private void setOrientation(boolean locked) { private void setRotation(int rotation) {
if (locked) { switch (rotation) {
int orientation; // Rotation free
int rotation = ((WindowManager) getSystemService( case 1:
Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation(); setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
switch (rotation) { break;
case Surface.ROTATION_0: // Lock in current rotation
orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT; case 2:
break; int currentOrientation = getResources().getConfiguration().orientation;
case Surface.ROTATION_90: setRotation(currentOrientation == Configuration.ORIENTATION_PORTRAIT ? 3 : 4);
orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE; break;
break; // Lock in portrait
case Surface.ROTATION_180: case 3:
orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT; setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
break; break;
default: // Lock in landscape
orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE; case 4:
break; setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
} break;
setRequestedOrientation(orientation);
} else {
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
} }
} }
@ -286,7 +335,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
private void setCustomBrightness(boolean enabled) { private void setCustomBrightness(boolean enabled) {
if (enabled) { if (enabled) {
subscriptions.add(customBrightnessSubscription = preferences.customBrightnessValue() subscriptions.add(customBrightnessSubscription = getPreferences().customBrightnessValue()
.asObservable() .asObservable()
.subscribe(this::setCustomBrightnessValue)); .subscribe(this::setCustomBrightnessValue));
} else { } else {
@ -344,7 +393,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
} }
public PreferencesHelper getPreferences() { public PreferencesHelper getPreferences() {
return preferences; return getPresenter().prefs;
} }
public BaseReader getViewer() { public BaseReader getViewer() {

View File

@ -2,11 +2,13 @@ package eu.kanade.tachiyomi.ui.reader;
import android.app.Dialog; import android.app.Dialog;
import android.content.Context; import android.content.Context;
import android.content.res.Configuration;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
import android.view.Gravity; import android.view.Gravity;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager; import android.view.WindowManager;
import android.view.WindowManager.LayoutParams; import android.view.WindowManager.LayoutParams;
import android.view.animation.Animation; import android.view.animation.Animation;
@ -42,9 +44,10 @@ public class ReaderMenu {
@Bind(R.id.page_seeker) SeekBar seekBar; @Bind(R.id.page_seeker) SeekBar seekBar;
@Bind(R.id.total_pages) TextView totalPages; @Bind(R.id.total_pages) TextView totalPages;
@Bind(R.id.lock_orientation) ImageButton lockOrientation; @Bind(R.id.lock_orientation) ImageButton lockOrientation;
@Bind(R.id.reader_zoom_selector) ImageButton zoomSelector;
@Bind(R.id.reader_scale_type_selector) ImageButton scaleTypeSelector;
@Bind(R.id.reader_selector) ImageButton readerSelector; @Bind(R.id.reader_selector) ImageButton readerSelector;
@Bind(R.id.reader_extra_settings) ImageButton extraSettings; @Bind(R.id.reader_extra_settings) ImageButton extraSettings;
@Bind(R.id.reader_brightness) ImageButton brightnessSettings;
private MenuItem nextChapterBtn; private MenuItem nextChapterBtn;
private MenuItem prevChapterBtn; private MenuItem prevChapterBtn;
@ -56,7 +59,6 @@ public class ReaderMenu {
@State boolean showing; @State boolean showing;
private PopupWindow settingsPopup; private PopupWindow settingsPopup;
private PopupWindow brightnessPopup;
private boolean inverted; private boolean inverted;
private DecimalFormat decimalFormat; private DecimalFormat decimalFormat;
@ -70,10 +72,10 @@ public class ReaderMenu {
bottomMenu.setOnTouchListener((v, event) -> true); bottomMenu.setOnTouchListener((v, event) -> true);
seekBar.setOnSeekBarChangeListener(new PageSeekBarChangeListener()); seekBar.setOnSeekBarChangeListener(new PageSeekBarChangeListener());
decimalFormat = new DecimalFormat("#.##"); decimalFormat = new DecimalFormat("#.###");
inverted = false; inverted = false;
initializeOptions(); initializeMenu();
} }
public void add(Subscription subscription) { public void add(Subscription subscription) {
@ -110,7 +112,6 @@ public class ReaderMenu {
bottomMenu.startAnimation(bottomMenuAnimation); bottomMenu.startAnimation(bottomMenuAnimation);
settingsPopup.dismiss(); settingsPopup.dismiss();
brightnessPopup.dismiss();
showing = false; showing = false;
} }
@ -134,7 +135,7 @@ public class ReaderMenu {
return true; return true;
} }
public void onChapterReady(int numPages, Manga manga, Chapter chapter, int currentPageIndex) { public void setActiveManga(Manga manga) {
if (manga.viewer == ReaderActivity.RIGHT_TO_LEFT && !inverted) { if (manga.viewer == ReaderActivity.RIGHT_TO_LEFT && !inverted) {
// Invert the seekbar and textview fields for the right to left reader // Invert the seekbar and textview fields for the right to left reader
seekBar.setRotation(180); seekBar.setRotation(180);
@ -144,14 +145,17 @@ public class ReaderMenu {
// Don't invert again on chapter change // Don't invert again on chapter change
inverted = true; inverted = true;
} }
activity.setToolbarTitle(manga.title);
}
public void setActiveChapter(Chapter chapter, int currentPageIndex) {
// Set initial values // Set initial values
int numPages = chapter.getPages().size();
totalPages.setText("" + numPages); totalPages.setText("" + numPages);
currentPage.setText("" + (currentPageIndex + 1)); currentPage.setText("" + (currentPageIndex + 1));
seekBar.setProgress(currentPageIndex);
seekBar.setMax(numPages - 1); seekBar.setMax(numPages - 1);
seekBar.setProgress(currentPageIndex);
activity.setToolbarTitle(manga.title);
activity.setToolbarSubtitle(chapter.chapter_number != -1 ? activity.setToolbarSubtitle(chapter.chapter_number != -1 ?
activity.getString(R.string.chapter_subtitle, activity.getString(R.string.chapter_subtitle,
decimalFormat.format(chapter.chapter_number)) : decimalFormat.format(chapter.chapter_number)) :
@ -175,25 +179,63 @@ public class ReaderMenu {
if (nextChapterBtn != null) nextChapterBtn.setVisible(nextChapter != null); if (nextChapterBtn != null) nextChapterBtn.setVisible(nextChapter != null);
} }
private void initializeOptions() { @SuppressWarnings("ConstantConditions")
// Orientation changes private void initializeMenu() {
add(preferences.lockOrientation().asObservable() // Orientation selector
.subscribe(locked -> { add(preferences.rotation().asObservable()
int resourceId = !locked ? R.drawable.ic_screen_rotation : .subscribe(value -> {
activity.getResources().getConfiguration().orientation == 1 ? boolean isPortrait = activity.getResources().getConfiguration()
.orientation == Configuration.ORIENTATION_PORTRAIT;
int resourceId = value == 1 ? R.drawable.ic_screen_rotation : isPortrait ?
R.drawable.ic_screen_lock_portrait : R.drawable.ic_screen_lock_portrait :
R.drawable.ic_screen_lock_landscape; R.drawable.ic_screen_lock_landscape;
lockOrientation.setImageResource(resourceId); lockOrientation.setImageResource(resourceId);
})); }));
lockOrientation.setOnClickListener(v -> lockOrientation.setOnClickListener(v -> {
preferences.lockOrientation().set(!preferences.lockOrientation().get())); showImmersiveDialog(new MaterialDialog.Builder(activity)
.title(R.string.pref_rotation_type)
.items(R.array.rotation_type)
.itemsCallbackSingleChoice(preferences.rotation().get() - 1,
(d, itemView, which, text) -> {
preferences.rotation().set(which + 1);
return true;
})
.build());
});
// Zoom selector
zoomSelector.setOnClickListener(v -> {
showImmersiveDialog(new MaterialDialog.Builder(activity)
.title(R.string.pref_zoom_start)
.items(R.array.zoom_start)
.itemsCallbackSingleChoice(preferences.zoomStart().get() - 1,
(d, itemView, which, text) -> {
preferences.zoomStart().set(which + 1);
return true;
})
.build());
});
// Scale type selector
scaleTypeSelector.setOnClickListener(v -> {
showImmersiveDialog(new MaterialDialog.Builder(activity)
.title(R.string.pref_image_scale_type)
.items(R.array.image_scale_type)
.itemsCallbackSingleChoice(preferences.imageScaleType().get() - 1,
(d, itemView, which, text) -> {
preferences.imageScaleType().set(which + 1);
return true;
})
.build());
});
// Reader selector // Reader selector
readerSelector.setOnClickListener(v -> { readerSelector.setOnClickListener(v -> {
final Manga manga = activity.getPresenter().getManga(); final Manga manga = activity.getPresenter().getManga();
showImmersiveDialog(new MaterialDialog.Builder(activity) showImmersiveDialog(new MaterialDialog.Builder(activity)
.title(R.string.pref_viewer_type)
.items(R.array.viewers_selector) .items(R.array.viewers_selector)
.itemsCallbackSingleChoice(manga.viewer, .itemsCallbackSingleChoice(manga.viewer,
(d, itemView, which, text) -> { (d, itemView, which, text) -> {
@ -215,17 +257,6 @@ public class ReaderMenu {
settingsPopup.dismiss(); settingsPopup.dismiss();
}); });
// Brightness popup
final View brightnessView = activity.getLayoutInflater().inflate(R.layout.reader_brightness, null);
brightnessPopup = new BrightnessPopupWindow(brightnessView);
brightnessSettings.setOnClickListener(v -> {
if (!brightnessPopup.isShowing())
brightnessPopup.showAtLocation(brightnessSettings,
Gravity.BOTTOM | Gravity.LEFT, 0, bottomMenu.getHeight());
else
brightnessPopup.dismiss();
});
} }
private void showImmersiveDialog(Dialog dialog) { private void showImmersiveDialog(Dialog dialog) {
@ -247,8 +278,11 @@ public class ReaderMenu {
@Bind(R.id.hide_status_bar) CheckBox hideStatusBar; @Bind(R.id.hide_status_bar) CheckBox hideStatusBar;
@Bind(R.id.keep_screen_on) CheckBox keepScreenOn; @Bind(R.id.keep_screen_on) CheckBox keepScreenOn;
@Bind(R.id.reader_theme) CheckBox readerTheme; @Bind(R.id.reader_theme) CheckBox readerTheme;
@Bind(R.id.image_decoder_container) ViewGroup imageDecoderContainer;
@Bind(R.id.image_decoder) TextView imageDecoder; @Bind(R.id.image_decoder) TextView imageDecoder;
@Bind(R.id.image_decoder_initial) TextView imageDecoderInitial; @Bind(R.id.image_decoder_initial) TextView imageDecoderInitial;
@Bind(R.id.custom_brightness) CheckBox customBrightness;
@Bind(R.id.brightness_seekbar) SeekBar brightnessSeekbar;
public SettingsPopupWindow(View view) { public SettingsPopupWindow(View view) {
super(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT); super(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
@ -257,6 +291,7 @@ public class ReaderMenu {
initializePopupMenu(); initializePopupMenu();
} }
@SuppressWarnings("ConstantConditions")
private void initializePopupMenu() { private void initializePopupMenu() {
// Load values from preferences // Load values from preferences
enableTransitions.setChecked(preferences.enableTransitions().get()); enableTransitions.setChecked(preferences.enableTransitions().get());
@ -282,7 +317,7 @@ public class ReaderMenu {
readerTheme.setOnCheckedChangeListener((view, isChecked) -> readerTheme.setOnCheckedChangeListener((view, isChecked) ->
preferences.readerTheme().set(isChecked ? 1 : 0)); preferences.readerTheme().set(isChecked ? 1 : 0));
imageDecoder.setOnClickListener(v -> { imageDecoderContainer.setOnClickListener(v -> {
showImmersiveDialog(new MaterialDialog.Builder(activity) showImmersiveDialog(new MaterialDialog.Builder(activity)
.title(R.string.pref_image_decoder) .title(R.string.pref_image_decoder)
.items(R.array.image_decoders) .items(R.array.image_decoders)
@ -294,6 +329,21 @@ public class ReaderMenu {
}) })
.build()); .build());
}); });
add(preferences.customBrightness()
.asObservable()
.subscribe(isEnabled -> {
customBrightness.setChecked(isEnabled);
brightnessSeekbar.setEnabled(isEnabled);
}));
customBrightness.setOnCheckedChangeListener((view, isChecked) ->
preferences.customBrightness().set(isChecked));
brightnessSeekbar.setMax(100);
brightnessSeekbar.setProgress(Math.round(
preferences.customBrightnessValue().get() * brightnessSeekbar.getMax()));
brightnessSeekbar.setOnSeekBarChangeListener(new BrightnessSeekBarChangeListener());
} }
private void setDecoderInitial(int decoder) { private void setDecoderInitial(int decoder) {
@ -314,43 +364,12 @@ public class ReaderMenu {
} }
class BrightnessPopupWindow extends PopupWindow {
@Bind(R.id.custom_brightness) CheckBox customBrightness;
@Bind(R.id.brightness_seekbar) SeekBar brightnessSeekbar;
public BrightnessPopupWindow(View view) {
super(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
setAnimationStyle(R.style.reader_brightness_popup_animation);
ButterKnife.bind(this, view);
initializePopupMenu();
}
private void initializePopupMenu() {
add(preferences.customBrightness()
.asObservable()
.subscribe(isEnabled -> {
customBrightness.setChecked(isEnabled);
brightnessSeekbar.setEnabled(isEnabled);
}));
customBrightness.setOnCheckedChangeListener((view, isChecked) ->
preferences.customBrightness().set(isChecked));
brightnessSeekbar.setMax(100);
brightnessSeekbar.setProgress(Math.round(
preferences.customBrightnessValue().get() * brightnessSeekbar.getMax()));
brightnessSeekbar.setOnSeekBarChangeListener(new BrightnessSeekBarChangeListener());
}
}
class PageSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener { class PageSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
@Override @Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) { public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) { if (fromUser) {
activity.setSelectedPage(progress); activity.gotoPageInCurrentChapter(progress);
} }
} }

View File

@ -4,17 +4,21 @@ import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.util.Pair; import android.util.Pair;
import org.greenrobot.eventbus.EventBus;
import org.greenrobot.eventbus.Subscribe;
import org.greenrobot.eventbus.ThreadMode;
import java.io.File; import java.io.File;
import java.util.List; import java.util.List;
import javax.inject.Inject; import javax.inject.Inject;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.data.database.DatabaseHelper; import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.Chapter; import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga; import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.database.models.MangaSync; import eu.kanade.tachiyomi.data.database.models.MangaSync;
import eu.kanade.tachiyomi.data.download.DownloadManager; import eu.kanade.tachiyomi.data.download.DownloadManager;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager; import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService; import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper; import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
@ -24,9 +28,9 @@ import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.data.sync.UpdateMangaSyncService; import eu.kanade.tachiyomi.data.sync.UpdateMangaSyncService;
import eu.kanade.tachiyomi.event.ReaderEvent; import eu.kanade.tachiyomi.event.ReaderEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import eu.kanade.tachiyomi.util.EventBusHook;
import icepick.State; import icepick.State;
import rx.Observable; import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers; import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject; import rx.subjects.PublishSubject;
@ -41,72 +45,55 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
@Inject SourceManager sourceManager; @Inject SourceManager sourceManager;
@State Manga manga; @State Manga manga;
@State Chapter chapter; @State Chapter activeChapter;
@State int sourceId; @State int sourceId;
@State boolean isDownloaded; @State int requestedPage;
@State int currentPage; private Page currentPage;
private Source source; private Source source;
private Chapter nextChapter; private Chapter nextChapter;
private Chapter previousChapter; private Chapter previousChapter;
private List<Page> pageList;
private List<Page> nextChapterPageList;
private List<MangaSync> mangaSyncList; private List<MangaSync> mangaSyncList;
private PublishSubject<Page> retryPageSubject; private PublishSubject<Page> retryPageSubject;
private PublishSubject<Chapter> pageInitializerSubject;
private boolean seamlessMode;
private Subscription appenderSubscription;
private static final int GET_PAGE_LIST = 1; private static final int GET_PAGE_LIST = 1;
private static final int GET_PAGE_IMAGES = 2; private static final int GET_ADJACENT_CHAPTERS = 2;
private static final int GET_ADJACENT_CHAPTERS = 3; private static final int GET_MANGA_SYNC = 3;
private static final int RETRY_IMAGES = 4; private static final int PRELOAD_NEXT_CHAPTER = 4;
private static final int PRELOAD_NEXT_CHAPTER = 5;
private static final int GET_MANGA_SYNC = 6;
@Override @Override
protected void onCreate(Bundle savedState) { protected void onCreate(Bundle savedState) {
super.onCreate(savedState); super.onCreate(savedState);
if (savedState != null) { if (savedState != null) {
onProcessRestart(); source = sourceManager.get(sourceId);
initializeSubjects();
} }
retryPageSubject = PublishSubject.create(); seamlessMode = prefs.seamlessMode();
restartableLatestCache(PRELOAD_NEXT_CHAPTER, startableLatestCache(GET_ADJACENT_CHAPTERS, this::getAdjacentChaptersObservable,
this::getPreloadNextChapterObservable, (view, pair) -> view.onAdjacentChapters(pair.first, pair.second));
(view, pages) -> {},
(view, error) -> Timber.e("An error occurred while preloading a chapter"));
restartableLatestCache(GET_PAGE_IMAGES, startable(PRELOAD_NEXT_CHAPTER, this::getPreloadNextChapterObservable,
this::getPageImagesObservable, next -> {},
(view, page) -> {}, error -> Timber.e("Error preloading chapter"));
(view, error) -> Timber.e("An error occurred while downloading an image"));
restartableLatestCache(GET_ADJACENT_CHAPTERS,
this::getAdjacentChaptersObservable,
(view, pair) -> view.onAdjacentChapters(pair.first, pair.second),
(view, error) -> Timber.e("An error occurred while getting adjacent chapters"));
restartableLatestCache(RETRY_IMAGES, restartable(GET_MANGA_SYNC, () -> getMangaSyncObservable().subscribe());
this::getRetryPageObservable,
(view, page) -> {},
(view, error) -> Timber.e("An error occurred while downloading an image"));
restartableLatestCache(GET_PAGE_LIST, restartableLatestCache(GET_PAGE_LIST,
() -> getPageListObservable() () -> getPageListObservable(activeChapter),
.doOnNext(pages -> pageList = pages) (view, chapter) -> view.onChapterReady(manga, activeChapter, currentPage),
.doOnCompleted(() -> {
start(GET_ADJACENT_CHAPTERS);
start(GET_PAGE_IMAGES);
start(RETRY_IMAGES);
}),
(view, pages) -> view.onChapterReady(pages, manga, chapter, currentPage),
(view, error) -> view.onChapterError()); (view, error) -> view.onChapterError());
restartableFirst(GET_MANGA_SYNC, this::getMangaSyncObservable, if (savedState == null) {
(view, mangaSync) -> {}, registerForEvents();
(view, error) -> {}); }
registerForStickyEvents();
} }
@Override @Override
@ -121,59 +108,85 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
super.onSave(state); super.onSave(state);
} }
private void onProcessRestart() { @Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
source = sourceManager.get(sourceId); public void onEvent(ReaderEvent event) {
// These are started by GET_PAGE_LIST, so we don't let them restart itselves
stop(GET_PAGE_IMAGES);
stop(GET_ADJACENT_CHAPTERS);
stop(RETRY_IMAGES);
stop(PRELOAD_NEXT_CHAPTER);
}
@EventBusHook
public void onEventMainThread(ReaderEvent event) {
EventBus.getDefault().removeStickyEvent(event); EventBus.getDefault().removeStickyEvent(event);
manga = event.getManga(); manga = event.getManga();
source = event.getSource(); source = event.getSource();
sourceId = source.getId(); sourceId = source.getId();
initializeSubjects();
loadChapter(event.getChapter()); loadChapter(event.getChapter());
if (prefs.autoUpdateMangaSync()) { if (prefs.autoUpdateMangaSync()) {
start(GET_MANGA_SYNC); start(GET_MANGA_SYNC);
} }
} }
private void initializeSubjects() {
// Listen for pages initialization events
pageInitializerSubject = PublishSubject.create();
add(pageInitializerSubject
.observeOn(Schedulers.io())
.concatMap(chapter -> {
Observable observable;
if (chapter.isDownloaded()) {
File chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter);
observable = Observable.from(chapter.getPages())
.flatMap(page -> downloadManager.getDownloadedImage(page, chapterDir));
} else {
observable = source.getAllImageUrlsFromPageList(chapter.getPages())
.flatMap(source::getCachedImage, 2)
.doOnCompleted(() -> source.savePageList(chapter.url, chapter.getPages()));
}
return observable.doOnCompleted(() -> {
if (!seamlessMode && activeChapter == chapter) {
preloadNextChapter();
}
});
})
.subscribe());
// Listen por retry events
retryPageSubject = PublishSubject.create();
add(retryPageSubject
.observeOn(Schedulers.io())
.flatMap(page -> page.getImageUrl() == null ?
source.getImageUrlFromPage(page) :
Observable.just(page))
.flatMap(source::getCachedImage)
.subscribe());
}
// Returns the page list of a chapter // Returns the page list of a chapter
private Observable<List<Page>> getPageListObservable() { private Observable<Chapter> getPageListObservable(Chapter chapter) {
return isDownloaded ? return (chapter.isDownloaded() ?
// Fetch the page list from disk // Fetch the page list from disk
Observable.just(downloadManager.getSavedPageList(source, manga, chapter)) : Observable.just(downloadManager.getSavedPageList(source, manga, chapter)) :
// Fetch the page list from cache or fallback to network // Fetch the page list from cache or fallback to network
source.getCachedPageListOrPullFromNetwork(chapter.url) source.getCachedPageListOrPullFromNetwork(chapter.url)
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()); .observeOn(AndroidSchedulers.mainThread())
} ).map(pages -> {
for (Page page : pages) {
// Get the chapter images from network or disk page.setChapter(chapter);
private Observable<Page> getPageImagesObservable() { }
Observable<Page> pageObservable; chapter.setPages(pages);
if (requestedPage >= -1 || currentPage == null) {
if (!isDownloaded) { if (requestedPage == -1) {
pageObservable = source.getAllImageUrlsFromPageList(pageList) currentPage = pages.get(pages.size() - 1);
.flatMap(source::getCachedImage, 2); } else {
} else { currentPage = pages.get(requestedPage);
File chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter); }
pageObservable = Observable.from(pageList) }
.flatMap(page -> downloadManager.getDownloadedImage(page, chapterDir)); requestedPage = -2;
} pageInitializerSubject.onNext(chapter);
return pageObservable.subscribeOn(Schedulers.io()) return chapter;
.doOnCompleted(this::preloadNextChapter); });
} }
private Observable<Pair<Chapter, Chapter>> getAdjacentChaptersObservable() { private Observable<Pair<Chapter, Chapter>> getAdjacentChaptersObservable() {
return Observable.zip( return Observable.zip(
db.getPreviousChapter(chapter).asRxObservable().take(1), db.getPreviousChapter(activeChapter).asRxObservable().take(1),
db.getNextChapter(chapter).asRxObservable().take(1), db.getNextChapter(activeChapter).asRxObservable().take(1),
Pair::create) Pair::create)
.doOnNext(pair -> { .doOnNext(pair -> {
previousChapter = pair.first; previousChapter = pair.first;
@ -182,29 +195,22 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
.observeOn(AndroidSchedulers.mainThread()); .observeOn(AndroidSchedulers.mainThread());
} }
// Listen for retry page events // Preload the first pages of the next chapter. Only for non seamless mode
private Observable<Page> getRetryPageObservable() {
return retryPageSubject
.observeOn(Schedulers.io())
.flatMap(page -> page.getImageUrl() == null ?
source.getImageUrlFromPage(page) :
Observable.just(page))
.flatMap(source::getCachedImage)
.observeOn(AndroidSchedulers.mainThread());
}
// Preload the first pages of the next chapter
private Observable<Page> getPreloadNextChapterObservable() { private Observable<Page> getPreloadNextChapterObservable() {
return source.getCachedPageListOrPullFromNetwork(nextChapter.url) return source.getCachedPageListOrPullFromNetwork(nextChapter.url)
.flatMap(pages -> { .flatMap(pages -> {
nextChapterPageList = pages; nextChapter.setPages(pages);
// Preload at most 5 pages
int pagesToPreload = Math.min(pages.size(), 5); int pagesToPreload = Math.min(pages.size(), 5);
return Observable.from(pages).take(pagesToPreload); return Observable.from(pages).take(pagesToPreload);
}) })
// Preload up to 5 images
.concatMap(page -> page.getImageUrl() == null ? .concatMap(page -> page.getImageUrl() == null ?
source.getImageUrlFromPage(page) : source.getImageUrlFromPage(page) :
Observable.just(page)) Observable.just(page))
// Download the first image
.concatMap(page -> page.getPageNumber() == 0 ?
source.getCachedImage(page) :
Observable.just(page))
.subscribeOn(Schedulers.io()) .subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.doOnCompleted(this::stopPreloadingNextChapter); .doOnCompleted(this::stopPreloadingNextChapter);
@ -212,29 +218,68 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
private Observable<List<MangaSync>> getMangaSyncObservable() { private Observable<List<MangaSync>> getMangaSyncObservable() {
return db.getMangasSync(manga).asRxObservable() return db.getMangasSync(manga).asRxObservable()
.take(1)
.doOnNext(mangaSync -> this.mangaSyncList = mangaSync); .doOnNext(mangaSync -> this.mangaSyncList = mangaSync);
} }
// Loads the given chapter
private void loadChapter(Chapter chapter) { private void loadChapter(Chapter chapter) {
// Before loading the chapter, stop preloading (if it's working) and save current progress loadChapter(chapter, 0);
stopPreloadingNextChapter(); }
this.chapter = chapter; // Loads the given chapter
isDownloaded = isChapterDownloaded(chapter); private void loadChapter(Chapter chapter, int requestedPage) {
if (seamlessMode) {
if (appenderSubscription != null)
remove(appenderSubscription);
} else {
stopPreloadingNextChapter();
}
this.activeChapter = chapter;
chapter.status = isChapterDownloaded(chapter) ? Download.DOWNLOADED : Download.NOT_DOWNLOADED;
// If the chapter is partially read, set the starting page to the last the user read // If the chapter is partially read, set the starting page to the last the user read
if (!chapter.read && chapter.last_page_read != 0) if (!chapter.read && chapter.last_page_read != 0)
currentPage = chapter.last_page_read; this.requestedPage = chapter.last_page_read;
else else
currentPage = 0; this.requestedPage = requestedPage;
// Reset next and previous chapter. They have to be fetched again // Reset next and previous chapter. They have to be fetched again
nextChapter = null; nextChapter = null;
previousChapter = null; previousChapter = null;
nextChapterPageList = null;
start(GET_PAGE_LIST); start(GET_PAGE_LIST);
start(GET_ADJACENT_CHAPTERS);
}
public void setActiveChapter(Chapter chapter) {
onChapterLeft();
this.activeChapter = chapter;
nextChapter = null;
previousChapter = null;
start(GET_ADJACENT_CHAPTERS);
}
public void appendNextChapter() {
if (nextChapter == null)
return;
if (appenderSubscription != null)
remove(appenderSubscription);
nextChapter.status = isChapterDownloaded(nextChapter) ? Download.DOWNLOADED : Download.NOT_DOWNLOADED;
appenderSubscription = getPageListObservable(nextChapter)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.compose(deliverLatestCache())
.subscribe(split((view, chapter) -> {
view.onAppendChapter(chapter);
}, (view, error) -> {
view.onChapterAppendError();
}));
add(appenderSubscription);
} }
// Check whether the given chapter is downloaded // Check whether the given chapter is downloaded
@ -250,34 +295,39 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
// Called before loading another chapter or leaving the reader. It allows to do operations // Called before loading another chapter or leaving the reader. It allows to do operations
// over the chapter read like saving progress // over the chapter read like saving progress
public void onChapterLeft() { public void onChapterLeft() {
if (pageList == null) List<Page> pages = activeChapter.getPages();
if (pages == null)
return; return;
// Get the last page read
int activePageNumber = activeChapter.last_page_read;
// Just in case, avoid out of index exceptions
if (activePageNumber >= pages.size()) {
activePageNumber = pages.size() - 1;
}
Page activePage = pages.get(activePageNumber);
// Cache current page list progress for online chapters to allow a faster reopen // Cache current page list progress for online chapters to allow a faster reopen
if (!isDownloaded) if (!activeChapter.isDownloaded()) {
source.savePageList(chapter.url, pageList); source.savePageList(activeChapter.url, pages);
}
// Save current progress of the chapter. Mark as read if the chapter is finished // Save current progress of the chapter. Mark as read if the chapter is finished
chapter.last_page_read = currentPage; if (activePage.isLastPage()) {
if (isChapterFinished()) { activeChapter.read = true;
chapter.read = true;
} }
db.insertChapter(chapter).asRxObservable().subscribe(); db.insertChapter(activeChapter).asRxObservable().subscribe();
}
// Check whether the chapter has been read
private boolean isChapterFinished() {
return !chapter.read && currentPage == pageList.size() - 1;
} }
public int getMangaSyncChapterToUpdate() { public int getMangaSyncChapterToUpdate() {
if (pageList == null || mangaSyncList == null || mangaSyncList.isEmpty()) if (activeChapter.getPages() == null || mangaSyncList == null || mangaSyncList.isEmpty())
return 0; return 0;
int lastChapterReadLocal = 0; int lastChapterReadLocal = 0;
// If the current chapter has been read, we check with this one // If the current chapter has been read, we check with this one
if (chapter.read) if (activeChapter.read)
lastChapterReadLocal = (int) Math.floor(chapter.chapter_number); lastChapterReadLocal = (int) Math.floor(activeChapter.chapter_number);
// If not, we check if the previous chapter has been read // If not, we check if the previous chapter has been read
else if (previousChapter != null && previousChapter.read) else if (previousChapter != null && previousChapter.read)
lastChapterReadLocal = (int) Math.floor(previousChapter.chapter_number); lastChapterReadLocal = (int) Math.floor(previousChapter.chapter_number);
@ -305,14 +355,14 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
} }
} }
public void setCurrentPage(int currentPage) { public void setCurrentPage(Page currentPage) {
this.currentPage = currentPage; this.currentPage = currentPage;
} }
public boolean loadNextChapter() { public boolean loadNextChapter() {
if (hasNextChapter()) { if (hasNextChapter()) {
onChapterLeft(); onChapterLeft();
loadChapter(nextChapter); loadChapter(nextChapter, 0);
return true; return true;
} }
return false; return false;
@ -321,7 +371,7 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
public boolean loadPreviousChapter() { public boolean loadPreviousChapter() {
if (hasPreviousChapter()) { if (hasPreviousChapter()) {
onChapterLeft(); onChapterLeft();
loadChapter(previousChapter); loadChapter(previousChapter, -1);
return true; return true;
} }
return false; return false;
@ -342,10 +392,10 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
} }
private void stopPreloadingNextChapter() { private void stopPreloadingNextChapter() {
if (isSubscribed(PRELOAD_NEXT_CHAPTER)) { if (!isUnsubscribed(PRELOAD_NEXT_CHAPTER)) {
stop(PRELOAD_NEXT_CHAPTER); stop(PRELOAD_NEXT_CHAPTER);
if (nextChapterPageList != null) if (nextChapter.getPages() != null)
source.savePageList(nextChapter.url, nextChapterPageList); source.savePageList(nextChapter.url, nextChapter.getPages());
} }
} }
@ -358,4 +408,11 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
return manga; return manga;
} }
public Page getCurrentPage() {
return currentPage;
}
public boolean isSeamlessMode() {
return seamlessMode;
}
} }

View File

@ -1,13 +1,15 @@
package eu.kanade.tachiyomi.ui.reader.viewer.base; package eu.kanade.tachiyomi.ui.reader.viewer.base;
import android.view.MotionEvent; import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder;
import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder; import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder;
import com.davemorrissey.labs.subscaleview.decoder.RapidImageRegionDecoder; import com.davemorrissey.labs.subscaleview.decoder.RapidImageRegionDecoder;
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageDecoder;
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder; import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder;
import java.util.ArrayList;
import java.util.List; import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.source.model.Page; import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment; import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity; import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
@ -16,48 +18,100 @@ public abstract class BaseReader extends BaseFragment {
protected int currentPage; protected int currentPage;
protected List<Page> pages; protected List<Page> pages;
protected List<Chapter> chapters;
protected Class<? extends ImageRegionDecoder> regionDecoderClass; protected Class<? extends ImageRegionDecoder> regionDecoderClass;
protected Class<? extends ImageDecoder> bitmapDecoderClass;
private boolean hasRequestedNextChapter;
public static final int RAPID_DECODER = 0; public static final int RAPID_DECODER = 0;
public static final int SKIA_DECODER = 1; public static final int SKIA_DECODER = 1;
public void updatePageNumber() { public void updatePageNumber() {
getReaderActivity().onPageChanged(getCurrentPage(), getTotalPages()); getReaderActivity().onPageChanged(getCurrentPage().getPageNumber(), getCurrentPage().getChapter().getPages().size());
} }
public int getCurrentPage() { public Page getCurrentPage() {
return currentPage; return pages.get(currentPage);
}
public int getPageForPosition(int position) {
return position;
}
public int getPositionForPage(int page) {
return page;
} }
public void onPageChanged(int position) { public void onPageChanged(int position) {
currentPage = getPageForPosition(position); Page oldPage = pages.get(currentPage);
Page newPage = pages.get(position);
newPage.getChapter().last_page_read = newPage.getPageNumber();
if (getReaderActivity().getPresenter().isSeamlessMode()) {
Chapter oldChapter = oldPage.getChapter();
Chapter newChapter = newPage.getChapter();
if (!hasRequestedNextChapter && position > pages.size() - 5) {
hasRequestedNextChapter = true;
getReaderActivity().getPresenter().appendNextChapter();
}
if (!oldChapter.id.equals(newChapter.id)) {
onChapterChanged(newPage.getChapter(), newPage);
}
}
currentPage = position;
updatePageNumber(); updatePageNumber();
} }
public int getTotalPages() { private void onChapterChanged(Chapter chapter, Page currentPage) {
return pages == null ? 0 : pages.size(); getReaderActivity().onEnterChapter(chapter, currentPage.getPageNumber());
}
public void setSelectedPage(Page page) {
setSelectedPage(getPageIndex(page));
}
public int getPageIndex(Page search) {
// search for the index of a page in the current list without requiring them to be the same object
for (Page page : pages) {
if (page.getPageNumber() == search.getPageNumber() &&
page.getChapter().id.equals(search.getChapter().id)) {
return pages.indexOf(page);
}
}
return 0;
}
public void onPageListReady(Chapter chapter, Page currentPage) {
if (chapters == null || !chapters.contains(chapter)) {
// if we reset the loaded page we also need to reset the loaded chapters
chapters = new ArrayList<>();
chapters.add(chapter);
onSetChapter(chapter, currentPage);
} else {
setSelectedPage(currentPage);
}
}
public void onPageListAppendReady(Chapter chapter) {
if (!chapters.contains(chapter)) {
hasRequestedNextChapter = false;
chapters.add(chapter);
onAppendChapter(chapter);
}
} }
public abstract void setSelectedPage(int pageNumber); public abstract void setSelectedPage(int pageNumber);
public abstract void onPageListReady(List<Page> pages, int currentPage); public abstract void onSetChapter(Chapter chapter, Page currentPage);
public abstract boolean onImageTouch(MotionEvent motionEvent); public abstract void onAppendChapter(Chapter chapter);
public abstract void moveToNext();
public abstract void moveToPrevious();
public void setRegionDecoderClass(int value) { public void setDecoderClass(int value) {
switch (value) { switch (value) {
case RAPID_DECODER: case RAPID_DECODER:
default: default:
regionDecoderClass = RapidImageRegionDecoder.class; regionDecoderClass = RapidImageRegionDecoder.class;
bitmapDecoderClass = SkiaImageDecoder.class;
// Using Skia because Rapid isn't stable. Rapid is still used for region decoding.
// https://github.com/inorichi/tachiyomi/issues/97
//bitmapDecoderClass = RapidImageDecoder.class;
break; break;
case SKIA_DECODER: case SKIA_DECODER:
regionDecoderClass = SkiaImageRegionDecoder.class; regionDecoderClass = SkiaImageRegionDecoder.class;
bitmapDecoderClass = SkiaImageDecoder.class;
break; break;
} }
} }
@ -66,6 +120,10 @@ public abstract class BaseReader extends BaseFragment {
return regionDecoderClass; return regionDecoderClass;
} }
public Class<? extends ImageDecoder> getBitmapDecoderClass() {
return bitmapDecoderClass;
}
public ReaderActivity getReaderActivity() { public ReaderActivity getReaderActivity() {
return (ReaderActivity) getActivity(); return (ReaderActivity) getActivity();
} }

View File

@ -1,7 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.viewer.base;
public interface OnChapterSingleTapListener {
void onCenterTap();
void onLeftSideTap();
void onRightSideTap();
}

View File

@ -1,4 +1,4 @@
package eu.kanade.tachiyomi.ui.reader.viewer.base; package eu.kanade.tachiyomi.ui.reader.viewer.pager;
public interface OnChapterBoundariesOutListener { public interface OnChapterBoundariesOutListener {
void onFirstPageOutEvent(); void onFirstPageOutEvent();

View File

@ -1,11 +1,8 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager; package eu.kanade.tachiyomi.ui.reader.viewer.pager;
import android.support.v4.view.PagerAdapter; import android.support.v4.view.PagerAdapter;
import android.view.MotionEvent;
import android.view.ViewGroup; import android.view.ViewGroup;
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener;
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener;
import rx.functions.Action1; import rx.functions.Action1;
public interface Pager { public interface Pager {
@ -24,13 +21,7 @@ public interface Pager {
PagerAdapter getAdapter(); PagerAdapter getAdapter();
void setAdapter(PagerAdapter adapter); void setAdapter(PagerAdapter adapter);
boolean onImageTouch(MotionEvent motionEvent);
void setOnChapterBoundariesOutListener(OnChapterBoundariesOutListener listener); void setOnChapterBoundariesOutListener(OnChapterBoundariesOutListener listener);
void setOnChapterSingleTapListener(OnChapterSingleTapListener listener);
OnChapterBoundariesOutListener getChapterBoundariesListener();
OnChapterSingleTapListener getChapterSingleTapListener();
void setOnPageChangeListener(Action1<Integer> onPageChanged); void setOnPageChangeListener(Action1<Integer> onPageChanged);
void clearOnPageChangeListeners(); void clearOnPageChangeListeners();

View File

@ -1,71 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager;
import android.view.GestureDetector;
import android.view.MotionEvent;
public class PagerGestureListener extends GestureDetector.SimpleOnGestureListener {
private Pager pager;
private static final float LEFT_REGION = 0.33f;
private static final float RIGHT_REGION = 0.66f;
public PagerGestureListener(Pager pager) {
this.pager = pager;
}
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
final int position = pager.getCurrentItem();
final float positionX = e.getX();
if (positionX < pager.getWidth() * LEFT_REGION) {
if (position != 0) {
onLeftSideTap();
} else {
onFirstPageOut();
}
} else if (positionX > pager.getWidth() * RIGHT_REGION) {
if (position != pager.getAdapter().getCount() - 1) {
onRightSideTap();
} else {
onLastPageOut();
}
} else {
onCenterTap();
}
return true;
}
private void onLeftSideTap() {
if (pager.getChapterSingleTapListener() != null) {
pager.getChapterSingleTapListener().onLeftSideTap();
}
}
private void onRightSideTap() {
if (pager.getChapterSingleTapListener() != null) {
pager.getChapterSingleTapListener().onRightSideTap();
}
}
private void onCenterTap() {
if (pager.getChapterSingleTapListener() != null) {
pager.getChapterSingleTapListener().onCenterTap();
}
}
private void onFirstPageOut() {
if (pager.getChapterBoundariesListener() != null) {
pager.getChapterBoundariesListener().onFirstPageOutEvent();
}
}
private void onLastPageOut() {
if (pager.getChapterBoundariesListener() != null) {
pager.getChapterBoundariesListener().onLastPageOutEvent();
}
}
}

View File

@ -1,15 +1,18 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager; package eu.kanade.tachiyomi.ui.reader.viewer.pager;
import android.view.GestureDetector;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.ViewGroup; import android.view.ViewGroup;
import java.util.List; import java.util.ArrayList;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.data.source.model.Page; import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader; import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener; import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader;
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener; import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader;
import rx.subscriptions.CompositeSubscription; import rx.subscriptions.CompositeSubscription;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT; import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
@ -18,10 +21,22 @@ public abstract class PagerReader extends BaseReader {
protected PagerReaderAdapter adapter; protected PagerReaderAdapter adapter;
protected Pager pager; protected Pager pager;
protected GestureDetector gestureDetector;
protected boolean transitions; protected boolean transitions;
protected CompositeSubscription subscriptions; protected CompositeSubscription subscriptions;
protected int scaleType = 1;
protected int zoomStart = 1;
public static final int ALIGN_AUTO = 1;
public static final int ALIGN_LEFT = 2;
public static final int ALIGN_RIGHT = 3;
public static final int ALIGN_CENTER = 4;
private static final float LEFT_REGION = 0.33f;
private static final float RIGHT_REGION = 0.66f;
protected void initializePager(Pager pager) { protected void initializePager(Pager pager) {
this.pager = pager; this.pager = pager;
pager.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); pager.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
@ -30,43 +45,43 @@ public abstract class PagerReader extends BaseReader {
pager.setOnChapterBoundariesOutListener(new OnChapterBoundariesOutListener() { pager.setOnChapterBoundariesOutListener(new OnChapterBoundariesOutListener() {
@Override @Override
public void onFirstPageOutEvent() { public void onFirstPageOutEvent() {
onFirstPageOut(); getReaderActivity().requestPreviousChapter();
} }
@Override @Override
public void onLastPageOutEvent() { public void onLastPageOutEvent() {
onLastPageOut(); getReaderActivity().requestNextChapter();
}
});
pager.setOnChapterSingleTapListener(new OnChapterSingleTapListener() {
@Override
public void onCenterTap() {
getReaderActivity().onCenterSingleTap();
}
@Override
public void onLeftSideTap() {
pager.setCurrentItem(pager.getCurrentItem() - 1, transitions);
}
@Override
public void onRightSideTap() {
pager.setCurrentItem(pager.getCurrentItem() + 1, transitions);
} }
}); });
gestureDetector = createGestureDetector();
adapter = new PagerReaderAdapter(getChildFragmentManager()); adapter = new PagerReaderAdapter(getChildFragmentManager());
pager.setAdapter(adapter); pager.setAdapter(adapter);
PreferencesHelper preferences = getReaderActivity().getPreferences();
subscriptions = new CompositeSubscription(); subscriptions = new CompositeSubscription();
subscriptions.add(getReaderActivity().getPreferences().imageDecoder() subscriptions.add(preferences.imageDecoder()
.asObservable() .asObservable()
.doOnNext(this::setRegionDecoderClass) .doOnNext(this::setDecoderClass)
.skip(1) .skip(1)
.distinctUntilChanged() .distinctUntilChanged()
.subscribe(v -> adapter.notifyDataSetChanged())); .subscribe(v -> pager.setAdapter(adapter)));
subscriptions.add(getReaderActivity().getPreferences().enableTransitions() subscriptions.add(preferences.imageScaleType()
.asObservable()
.doOnNext(this::setImageScaleType)
.skip(1)
.distinctUntilChanged()
.subscribe(v -> pager.setAdapter(adapter)));
subscriptions.add(preferences.zoomStart()
.asObservable()
.doOnNext(this::setZoomStart)
.skip(1)
.distinctUntilChanged()
.subscribe(v -> pager.setAdapter(adapter)));
subscriptions.add(preferences.enableTransitions()
.asObservable() .asObservable()
.subscribe(value -> transitions = value)); .subscribe(value -> transitions = value));
@ -79,14 +94,41 @@ public abstract class PagerReader extends BaseReader {
super.onDestroyView(); super.onDestroyView();
} }
@Override protected GestureDetector createGestureDetector() {
public void onPageListReady(List<Page> pages, int currentPage) { return new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
if (this.pages != pages) { @Override
this.pages = pages; public boolean onSingleTapConfirmed(MotionEvent e) {
this.currentPage = currentPage; final float positionX = e.getX();
if (isResumed()) {
setPages(); if (positionX < pager.getWidth() * LEFT_REGION) {
onLeftSideTap();
} else if (positionX > pager.getWidth() * RIGHT_REGION) {
onRightSideTap();
} else {
getReaderActivity().onCenterSingleTap();
}
return true;
} }
});
}
@Override
public void onSetChapter(Chapter chapter, Page currentPage) {
pages = new ArrayList<>(chapter.getPages());
this.currentPage = getPageIndex(currentPage); // we might have a new page object
// This method can be called before the view is created
if (pager != null) {
setPages();
}
}
public void onAppendChapter(Chapter chapter) {
pages.addAll(chapter.getPages());
// This method can be called before the view is created
if (pager != null) {
adapter.setPages(pages);
} }
} }
@ -102,15 +144,48 @@ public abstract class PagerReader extends BaseReader {
@Override @Override
public void setSelectedPage(int pageNumber) { public void setSelectedPage(int pageNumber) {
pager.setCurrentItem(getPositionForPage(pageNumber), false); pager.setCurrentItem(pageNumber, false);
} }
@Override protected void onLeftSideTap() {
public boolean onImageTouch(MotionEvent motionEvent) { moveToPrevious();
return pager.onImageTouch(motionEvent);
} }
public abstract void onFirstPageOut(); protected void onRightSideTap() {
public abstract void onLastPageOut(); moveToNext();
}
public void moveToNext() {
if (pager.getCurrentItem() != pager.getAdapter().getCount() - 1) {
pager.setCurrentItem(pager.getCurrentItem() + 1, transitions);
} else {
getReaderActivity().requestNextChapter();
}
}
public void moveToPrevious() {
if (pager.getCurrentItem() != 0) {
pager.setCurrentItem(pager.getCurrentItem() - 1, transitions);
} else {
getReaderActivity().requestPreviousChapter();
}
}
private void setImageScaleType(int scaleType) {
this.scaleType = scaleType;
}
private void setZoomStart(int zoomStart) {
if (zoomStart == ALIGN_AUTO) {
if (this instanceof LeftToRightReader)
setZoomStart(ALIGN_LEFT);
else if (this instanceof RightToLeftReader)
setZoomStart(ALIGN_RIGHT);
else
setZoomStart(ALIGN_CENTER);
} else {
this.zoomStart = zoomStart;
}
}
} }

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager;
import android.support.v4.app.Fragment; import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter; import android.support.v4.app.FragmentStatePagerAdapter;
import android.view.ViewGroup;
import java.util.List; import java.util.List;
@ -23,7 +24,15 @@ public class PagerReaderAdapter extends FragmentStatePagerAdapter {
@Override @Override
public Fragment getItem(int position) { public Fragment getItem(int position) {
return PagerReaderFragment.newInstance(pages.get(position)); return PagerReaderFragment.newInstance();
}
@Override
public Object instantiateItem(ViewGroup container, int position) {
PagerReaderFragment f = (PagerReaderFragment) super.instantiateItem(container, position);
f.setPage(pages.get(position));
f.setPosition(position);
return f;
} }
public List<Page> getPages() { public List<Page> getPages() {
@ -37,7 +46,15 @@ public class PagerReaderAdapter extends FragmentStatePagerAdapter {
@Override @Override
public int getItemPosition(Object object) { public int getItemPosition(Object object) {
return POSITION_NONE; PagerReaderFragment f = (PagerReaderFragment) object;
int position = f.getPosition();
if (position >= 0 && position < getCount()) {
if (pages.get(position) == f.getPage()) {
return POSITION_UNCHANGED;
} else {
return POSITION_NONE;
}
}
return super.getItemPosition(object);
} }
} }

View File

@ -1,5 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager; package eu.kanade.tachiyomi.ui.reader.viewer.pager;
import android.graphics.PointF;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable; import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
@ -16,6 +17,7 @@ import android.widget.TextView;
import com.davemorrissey.labs.subscaleview.ImageSource; import com.davemorrissey.labs.subscaleview.ImageSource;
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
import java.io.File;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicInteger;
@ -25,7 +27,8 @@ import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.source.model.Page; import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment; import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity; import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader; import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader;
import rx.Observable; import rx.Observable;
import rx.Subscription; import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
@ -43,11 +46,13 @@ public class PagerReaderFragment extends BaseFragment {
private Page page; private Page page;
private Subscription progressSubscription; private Subscription progressSubscription;
private Subscription statusSubscription; private Subscription statusSubscription;
private int position = -1;
public static PagerReaderFragment newInstance(Page page) { private int lightGreyColor;
PagerReaderFragment fragment = new PagerReaderFragment(); private int blackColor;
fragment.setPage(page);
return fragment; public static PagerReaderFragment newInstance() {
return new PagerReaderFragment();
} }
@Override @Override
@ -55,20 +60,47 @@ public class PagerReaderFragment extends BaseFragment {
View view = inflater.inflate(R.layout.item_pager_reader, container, false); View view = inflater.inflate(R.layout.item_pager_reader, container, false);
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
ReaderActivity activity = getReaderActivity(); ReaderActivity activity = getReaderActivity();
BaseReader parentFragment = (BaseReader) getParentFragment(); PagerReader parentFragment = (PagerReader) getParentFragment();
lightGreyColor = ContextCompat.getColor(getContext(), R.color.light_grey);
blackColor = ContextCompat.getColor(getContext(), R.color.primary_text);
if (activity.getReaderTheme() == ReaderActivity.BLACK_THEME) { if (activity.getReaderTheme() == ReaderActivity.BLACK_THEME) {
progressText.setTextColor(ContextCompat.getColor(getContext(), R.color.light_grey)); progressText.setTextColor(lightGreyColor);
}
if (parentFragment instanceof RightToLeftReader) {
view.setRotation(-180);
} }
imageView.setParallelLoadingEnabled(true); imageView.setParallelLoadingEnabled(true);
imageView.setMaxDimensions(activity.getMaxBitmapSize(), activity.getMaxBitmapSize()); imageView.setMaxBitmapDimensions(activity.getMaxBitmapSize());
imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED); imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED);
imageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE); imageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE);
imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE); imageView.setMinimumScaleType(parentFragment.scaleType);
imageView.setMinimumDpi(50);
imageView.setRegionDecoderClass(parentFragment.getRegionDecoderClass()); imageView.setRegionDecoderClass(parentFragment.getRegionDecoderClass());
imageView.setOnTouchListener((v, motionEvent) -> parentFragment.onImageTouch(motionEvent)); imageView.setBitmapDecoderClass(parentFragment.getBitmapDecoderClass());
imageView.setVerticalScrollingParent(parentFragment instanceof VerticalReader);
imageView.setOnTouchListener((v, motionEvent) -> parentFragment.gestureDetector.onTouchEvent(motionEvent));
imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() { imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() {
@Override
public void onReady() {
switch (parentFragment.zoomStart) {
case PagerReader.ALIGN_LEFT:
imageView.setScaleAndCenter(imageView.getScale(), new PointF(0, 0));
break;
case PagerReader.ALIGN_RIGHT:
imageView.setScaleAndCenter(imageView.getScale(), new PointF(imageView.getSWidth(), 0));
break;
case PagerReader.ALIGN_CENTER:
PointF center = imageView.getCenter();
center.y = 0;
imageView.setScaleAndCenter(imageView.getScale(), center);
break;
}
}
@Override @Override
public void onImageLoadError(Exception e) { public void onImageLoadError(Exception e) {
showImageLoadError(); showImageLoadError();
@ -91,20 +123,36 @@ public class PagerReaderFragment extends BaseFragment {
public void onDestroyView() { public void onDestroyView() {
unsubscribeProgress(); unsubscribeProgress();
unsubscribeStatus(); unsubscribeStatus();
imageView.setOnTouchListener(null);
imageView.setOnImageEventListener(null);
ButterKnife.unbind(this); ButterKnife.unbind(this);
super.onDestroyView(); super.onDestroyView();
} }
public void setPage(Page page) { public void setPage(Page page) {
this.page = page; this.page = page;
// This method can be called before the view is created
if (imageView != null) {
observeStatus();
}
}
public void setPosition(int position) {
this.position = position;
} }
private void showImage() { private void showImage() {
if (page == null || page.getImagePath() == null) if (page == null || page.getImagePath() == null)
return; return;
imageView.setImage(ImageSource.uri(page.getImagePath())); File imagePath = new File(page.getImagePath());
progressContainer.setVisibility(View.GONE); if (imagePath.exists()) {
imageView.setImage(ImageSource.uri(page.getImagePath()));
progressContainer.setVisibility(View.GONE);
} else {
page.setStatus(Page.ERROR);
}
} }
private void showDownloading() { private void showDownloading() {
@ -136,8 +184,7 @@ public class PagerReaderFragment extends BaseFragment {
errorText.setGravity(Gravity.CENTER); errorText.setGravity(Gravity.CENTER);
errorText.setText(R.string.decode_image_error); errorText.setText(R.string.decode_image_error);
errorText.setTextColor(getReaderActivity().getReaderTheme() == ReaderActivity.BLACK_THEME ? errorText.setTextColor(getReaderActivity().getReaderTheme() == ReaderActivity.BLACK_THEME ?
ContextCompat.getColor(getContext(), R.color.light_grey) : lightGreyColor : blackColor);
ContextCompat.getColor(getContext(), R.color.primary_text));
view.addView(errorText); view.addView(errorText);
} }
@ -157,7 +204,6 @@ public class PagerReaderFragment extends BaseFragment {
case Page.READY: case Page.READY:
showImage(); showImage();
unsubscribeProgress(); unsubscribeProgress();
unsubscribeStatus();
break; break;
case Page.ERROR: case Page.ERROR:
showError(); showError();
@ -185,8 +231,8 @@ public class PagerReaderFragment extends BaseFragment {
final AtomicInteger currentValue = new AtomicInteger(-1); final AtomicInteger currentValue = new AtomicInteger(-1);
progressSubscription = Observable.interval(75, TimeUnit.MILLISECONDS, Schedulers.newThread()) progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS, Schedulers.newThread())
.onBackpressureDrop() .onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread()) .observeOn(AndroidSchedulers.mainThread())
.subscribe(tick -> { .subscribe(tick -> {
// Refresh UI only if progress change // Refresh UI only if progress change
@ -212,6 +258,14 @@ public class PagerReaderFragment extends BaseFragment {
} }
} }
public Page getPage() {
return page;
}
public int getPosition() {
return position;
}
private ReaderActivity getReaderActivity() { private ReaderActivity getReaderActivity() {
return (ReaderActivity) getActivity(); return (ReaderActivity) getActivity();
} }

View File

@ -2,38 +2,21 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
import android.content.Context; import android.content.Context;
import android.support.v4.view.ViewPager; import android.support.v4.view.ViewPager;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent; import android.view.MotionEvent;
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener; import eu.kanade.tachiyomi.ui.reader.viewer.pager.OnChapterBoundariesOutListener;
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager; import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerGestureListener;
import rx.functions.Action1; import rx.functions.Action1;
public class HorizontalPager extends ViewPager implements Pager { public class HorizontalPager extends ViewPager implements Pager {
private GestureDetector gestureDetector;
private OnChapterBoundariesOutListener onChapterBoundariesOutListener; private OnChapterBoundariesOutListener onChapterBoundariesOutListener;
private OnChapterSingleTapListener onChapterSingleTapListener;
private static final float SWIPE_TOLERANCE = 0.25f; private static final float SWIPE_TOLERANCE = 0.25f;
private float startDragX; private float startDragX;
public HorizontalPager(Context context) { public HorizontalPager(Context context) {
super(context); super(context);
init(context);
}
public HorizontalPager(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
gestureDetector = new GestureDetector(context, new PagerGestureListener(this));
} }
@Override @Override
@ -47,7 +30,7 @@ public class HorizontalPager extends ViewPager implements Pager {
return super.onInterceptTouchEvent(ev); return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return true; return false;
} }
} }
@ -82,35 +65,15 @@ public class HorizontalPager extends ViewPager implements Pager {
return super.onTouchEvent(ev); return super.onTouchEvent(ev);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return true; return false;
} }
} }
@Override
public boolean onImageTouch(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
@Override @Override
public void setOnChapterBoundariesOutListener(OnChapterBoundariesOutListener listener) { public void setOnChapterBoundariesOutListener(OnChapterBoundariesOutListener listener) {
onChapterBoundariesOutListener = listener; onChapterBoundariesOutListener = listener;
} }
@Override
public void setOnChapterSingleTapListener(OnChapterSingleTapListener listener) {
onChapterSingleTapListener = listener;
}
@Override
public OnChapterBoundariesOutListener getChapterBoundariesListener() {
return onChapterBoundariesOutListener;
}
@Override
public OnChapterSingleTapListener getChapterSingleTapListener() {
return onChapterSingleTapListener;
}
@Override @Override
public void setOnPageChangeListener(Action1<Integer> function) { public void setOnPageChangeListener(Action1<Integer> function) {
addOnPageChangeListener(new SimpleOnPageChangeListener() { addOnPageChangeListener(new SimpleOnPageChangeListener() {

View File

@ -1,19 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader;
public abstract class HorizontalReader extends PagerReader {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
HorizontalPager pager = new HorizontalPager(getActivity());
initializePager(pager);
return pager;
}
}

View File

@ -1,15 +1,19 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal; package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
public class LeftToRightReader extends HorizontalReader { import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader;
public class LeftToRightReader extends PagerReader {
@Override @Override
public void onFirstPageOut() { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
getReaderActivity().requestPreviousChapter(); HorizontalPager pager = new HorizontalPager(getActivity());
} initializePager(pager);
return pager;
@Override
public void onLastPageOut() {
getReaderActivity().requestNextChapter();
} }
} }

View File

@ -1,38 +1,30 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal; package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
import java.util.ArrayList; import android.os.Bundle;
import java.util.Collections; import android.view.LayoutInflater;
import java.util.List; import android.view.View;
import android.view.ViewGroup;
import eu.kanade.tachiyomi.data.source.model.Page; import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader;
public class RightToLeftReader extends HorizontalReader { public class RightToLeftReader extends PagerReader {
@Override @Override
public void onPageListReady(List<Page> pages, int currentPage) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
ArrayList<Page> inversedPages = new ArrayList<>(pages); HorizontalPager pager = new HorizontalPager(getActivity());
Collections.reverse(inversedPages); pager.setRotation(180);
super.onPageListReady(inversedPages, currentPage); initializePager(pager);
return pager;
} }
@Override @Override
public int getPageForPosition(int position) { protected void onLeftSideTap() {
return (getTotalPages() - 1) - position; moveToNext();
} }
@Override @Override
public int getPositionForPage(int page) { protected void onRightSideTap() {
return (getTotalPages() - 1) - page; moveToPrevious();
}
@Override
public void onFirstPageOut() {
getReaderActivity().requestNextChapter();
}
@Override
public void onLastPageOut() {
getReaderActivity().requestPreviousChapter();
} }
} }

View File

@ -1,38 +1,21 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical; package eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical;
import android.content.Context; import android.content.Context;
import android.util.AttributeSet;
import android.view.GestureDetector;
import android.view.MotionEvent; import android.view.MotionEvent;
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener; import eu.kanade.tachiyomi.ui.reader.viewer.pager.OnChapterBoundariesOutListener;
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager; import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerGestureListener;
import rx.functions.Action1; import rx.functions.Action1;
public class VerticalPager extends VerticalViewPagerImpl implements Pager { public class VerticalPager extends VerticalViewPagerImpl implements Pager {
private GestureDetector gestureDetector;
private OnChapterBoundariesOutListener onChapterBoundariesOutListener; private OnChapterBoundariesOutListener onChapterBoundariesOutListener;
private OnChapterSingleTapListener onChapterSingleTapListener;
private static final float SWIPE_TOLERANCE = 0.25f; private static final float SWIPE_TOLERANCE = 0.25f;
private float startDragY; private float startDragY;
public VerticalPager(Context context) { public VerticalPager(Context context) {
super(context); super(context);
init(context);
}
public VerticalPager(Context context, AttributeSet attrs) {
super(context, attrs);
init(context);
}
private void init(Context context) {
gestureDetector = new GestureDetector(context, new VerticalPagerGestureListener(this));
} }
@Override @Override
@ -46,7 +29,7 @@ public class VerticalPager extends VerticalViewPagerImpl implements Pager {
return super.onInterceptTouchEvent(ev); return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return true; return false;
} }
} }
@ -81,35 +64,15 @@ public class VerticalPager extends VerticalViewPagerImpl implements Pager {
return super.onTouchEvent(ev); return super.onTouchEvent(ev);
} catch (IllegalArgumentException e) { } catch (IllegalArgumentException e) {
return true; return false;
} }
} }
@Override
public boolean onImageTouch(MotionEvent event) {
return gestureDetector.onTouchEvent(event);
}
@Override @Override
public void setOnChapterBoundariesOutListener(OnChapterBoundariesOutListener listener) { public void setOnChapterBoundariesOutListener(OnChapterBoundariesOutListener listener) {
onChapterBoundariesOutListener = listener; onChapterBoundariesOutListener = listener;
} }
@Override
public void setOnChapterSingleTapListener(OnChapterSingleTapListener listener) {
onChapterSingleTapListener = listener;
}
@Override
public OnChapterBoundariesOutListener getChapterBoundariesListener() {
return onChapterBoundariesOutListener;
}
@Override
public OnChapterSingleTapListener getChapterSingleTapListener() {
return onChapterSingleTapListener;
}
@Override @Override
public void setOnPageChangeListener(Action1<Integer> function) { public void setOnPageChangeListener(Action1<Integer> function) {
addOnPageChangeListener(new SimpleOnPageChangeListener() { addOnPageChangeListener(new SimpleOnPageChangeListener() {
@ -119,20 +82,5 @@ public class VerticalPager extends VerticalViewPagerImpl implements Pager {
} }
}); });
} }
private static class VerticalPagerGestureListener extends PagerGestureListener {
public VerticalPagerGestureListener(Pager pager) {
super(pager);
}
@Override
public boolean onDown(MotionEvent e) {
// Vertical view pager ignores scrolling events sometimes.
// Returning true here fixes it, but we lose touch events on the image like
// double tap to zoom
return true;
}
}
} }

View File

@ -16,14 +16,4 @@ public class VerticalReader extends PagerReader {
return pager; return pager;
} }
@Override
public void onFirstPageOut() {
getReaderActivity().requestPreviousChapter();
}
@Override
public void onLastPageOut() {
getReaderActivity().requestNextChapter();
}
} }

View File

@ -10,6 +10,7 @@ import java.util.List;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.source.model.Page; import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> { public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> {
@ -20,7 +21,7 @@ public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> {
public WebtoonAdapter(WebtoonReader fragment) { public WebtoonAdapter(WebtoonReader fragment) {
this.fragment = fragment; this.fragment = fragment;
pages = new ArrayList<>(); pages = new ArrayList<>();
touchListener = (v, event) -> fragment.onImageTouch(event); touchListener = (v, event) -> fragment.gestureDetector.onTouchEvent(event);
} }
public Page getItem(int position) { public Page getItem(int position) {
@ -64,4 +65,8 @@ public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> {
return fragment; return fragment;
} }
public ReaderActivity getReaderActivity() {
return (ReaderActivity) fragment.getActivity();
}
} }

View File

@ -4,14 +4,14 @@ import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent; import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button; import android.widget.Button;
import android.widget.ProgressBar; import android.widget.ProgressBar;
import com.davemorrissey.labs.subscaleview.ImageSource; import com.davemorrissey.labs.subscaleview.ImageSource;
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView; import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
import java.io.File;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
@ -24,7 +24,6 @@ public class WebtoonHolder extends RecyclerView.ViewHolder {
@Bind(R.id.progress) ProgressBar progressBar; @Bind(R.id.progress) ProgressBar progressBar;
@Bind(R.id.retry_button) Button retryButton; @Bind(R.id.retry_button) Button retryButton;
private Animation fadeInAnimation;
private Page page; private Page page;
private WebtoonAdapter adapter; private WebtoonAdapter adapter;
@ -33,27 +32,38 @@ public class WebtoonHolder extends RecyclerView.ViewHolder {
this.adapter = adapter; this.adapter = adapter;
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
fadeInAnimation = AnimationUtils.loadAnimation(view.getContext(), R.anim.fade_in);
imageView.setParallelLoadingEnabled(true); imageView.setParallelLoadingEnabled(true);
imageView.setMaxBitmapDimensions(adapter.getReaderActivity().getMaxBitmapSize());
imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED); imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED);
imageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE); imageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE);
imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE); imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_FIT_WIDTH);
imageView.setMaxScale(10);
imageView.setRegionDecoderClass(adapter.getReader().getRegionDecoderClass());
imageView.setBitmapDecoderClass(adapter.getReader().getBitmapDecoderClass());
imageView.setVerticalScrollingParent(true);
imageView.setOnTouchListener(touchListener); imageView.setOnTouchListener(touchListener);
imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() { imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() {
@Override @Override
public void onImageLoaded() { public void onImageLoaded() {
imageView.startAnimation(fadeInAnimation); // When the image is loaded, reset the minimum height to avoid gaps
container.setMinimumHeight(0);
} }
}); });
progressBar.setMinimumHeight(view.getResources().getDisplayMetrics().heightPixels);
// Avoid to create a lot of view holders taking twice the screen height,
// saving memory and a possible OOM. When the first image is loaded in this holder,
// the minimum size will be removed.
// Doing this we get sequential holder instantiation.
container.setMinimumHeight(view.getResources().getDisplayMetrics().heightPixels * 2);
// Leave some space between progress bars
progressBar.setMinimumHeight(300);
container.setOnTouchListener(touchListener); container.setOnTouchListener(touchListener);
retryButton.setOnTouchListener((v, event) -> { retryButton.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) { if (event.getAction() == MotionEvent.ACTION_UP) {
if (page != null) if (page != null)
adapter.retryPage(page); adapter.retryPage(page);
return true;
} }
return true; return true;
}); });
@ -90,8 +100,14 @@ public class WebtoonHolder extends RecyclerView.ViewHolder {
setErrorButtonVisible(false); setErrorButtonVisible(false);
setProgressVisible(false); setProgressVisible(false);
setImageVisible(true); setImageVisible(true);
imageView.setRegionDecoderClass(adapter.getReader().getRegionDecoderClass());
imageView.setImage(ImageSource.uri(page.getImagePath())); File imagePath = new File(page.getImagePath());
if (imagePath.exists()) {
imageView.setImage(ImageSource.uri(page.getImagePath()));
} else {
page.setStatus(Page.ERROR);
onError();
}
} }
private void onError() { private void onError() {

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon; package eu.kanade.tachiyomi.ui.reader.viewer.webtoon;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.view.GestureDetector; import android.view.GestureDetector;
import android.view.LayoutInflater; import android.view.LayoutInflater;
@ -9,8 +8,9 @@ import android.view.MotionEvent;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import java.util.List; import java.util.ArrayList;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.source.model.Page; import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader; import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
import eu.kanade.tachiyomi.widget.PreCachingLayoutManager; import eu.kanade.tachiyomi.widget.PreCachingLayoutManager;
@ -28,14 +28,27 @@ public class WebtoonReader extends BaseReader {
private PreCachingLayoutManager layoutManager; private PreCachingLayoutManager layoutManager;
private Subscription subscription; private Subscription subscription;
private Subscription decoderSubscription; private Subscription decoderSubscription;
private GestureDetector gestureDetector; protected GestureDetector gestureDetector;
private int scrollDistance;
private static final String SAVED_POSITION = "saved_position";
private static final float LEFT_REGION = 0.33f;
private static final float RIGHT_REGION = 0.66f;
@Nullable
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
adapter = new WebtoonAdapter(this); adapter = new WebtoonAdapter(this);
int screenHeight = getResources().getDisplayMetrics().heightPixels;
scrollDistance = screenHeight * 3 / 4;
layoutManager = new PreCachingLayoutManager(getActivity()); layoutManager = new PreCachingLayoutManager(getActivity());
layoutManager.setExtraLayoutSpace(getResources().getDisplayMetrics().heightPixels); layoutManager.setExtraLayoutSpace(screenHeight / 2);
if (savedState != null) {
layoutManager.scrollToPositionWithOffset(savedState.getInt(SAVED_POSITION), 0);
}
recycler = new RecyclerView(getActivity()); recycler = new RecyclerView(getActivity());
recycler.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT)); recycler.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
@ -45,29 +58,28 @@ public class WebtoonReader extends BaseReader {
decoderSubscription = getReaderActivity().getPreferences().imageDecoder() decoderSubscription = getReaderActivity().getPreferences().imageDecoder()
.asObservable() .asObservable()
.doOnNext(this::setRegionDecoderClass) .doOnNext(this::setDecoderClass)
.skip(1) .skip(1)
.distinctUntilChanged() .distinctUntilChanged()
.subscribe(v -> adapter.notifyDataSetChanged()); .subscribe(v -> recycler.setAdapter(adapter));
gestureDetector = new GestureDetector(getActivity(), new SimpleOnGestureListener() { gestureDetector = new GestureDetector(recycler.getContext(), new SimpleOnGestureListener() {
@Override @Override
public boolean onSingleTapConfirmed(MotionEvent e) { public boolean onSingleTapConfirmed(MotionEvent e) {
getReaderActivity().onCenterSingleTap(); final float positionX = e.getX();
if (positionX < recycler.getWidth() * LEFT_REGION) {
moveToPrevious();
} else if (positionX > recycler.getWidth() * RIGHT_REGION) {
moveToNext();
} else {
getReaderActivity().onCenterSingleTap();
}
return true; return true;
} }
@Override
public boolean onDown(MotionEvent e) {
// The only way I've found to allow panning. Double tap event (zoom) is lost
// but panning should be the most used one
return true;
}
}); });
setPages(); setPages();
return recycler; return recycler;
} }
@ -83,6 +95,14 @@ public class WebtoonReader extends BaseReader {
super.onPause(); super.onPause();
} }
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
int savedPosition = pages != null ?
pages.get(layoutManager.findFirstVisibleItemPosition()).getPageNumber() : 0;
outState.putInt(SAVED_POSITION, savedPosition);
}
private void unsubscribeStatus() { private void unsubscribeStatus() {
if (subscription != null && !subscription.isUnsubscribed()) if (subscription != null && !subscription.isUnsubscribed())
subscription.unsubscribe(); subscription.unsubscribe();
@ -90,15 +110,42 @@ public class WebtoonReader extends BaseReader {
@Override @Override
public void setSelectedPage(int pageNumber) { public void setSelectedPage(int pageNumber) {
recycler.scrollToPosition(getPositionForPage(pageNumber)); recycler.scrollToPosition(pageNumber);
} }
@Override @Override
public void onPageListReady(List<Page> pages, int currentPage) { public void moveToNext() {
if (this.pages != pages) { recycler.smoothScrollBy(0, scrollDistance);
this.pages = pages; }
if (isResumed()) {
setPages(); @Override
public void moveToPrevious() {
recycler.smoothScrollBy(0, -scrollDistance);
}
@Override
public void onSetChapter(Chapter chapter, Page currentPage) {
pages = new ArrayList<>(chapter.getPages());
// Restoring current page is not supported. It's getting weird scrolling jumps
// this.currentPage = currentPage;
// This method can be called before the view is created
if (recycler != null) {
setPages();
}
}
@Override
public void onAppendChapter(Chapter chapter) {
int insertStart = pages.size();
pages.addAll(chapter.getPages());
// This method can be called before the view is created
if (recycler != null) {
adapter.setPages(pages);
adapter.notifyItemRangeInserted(insertStart, chapter.getPages().size());
if (subscription != null && subscription.isUnsubscribed()) {
observeStatus(insertStart);
} }
} }
} }
@ -109,6 +156,7 @@ public class WebtoonReader extends BaseReader {
recycler.clearOnScrollListeners(); recycler.clearOnScrollListeners();
adapter.setPages(pages); adapter.setPages(pages);
recycler.setAdapter(adapter); recycler.setAdapter(adapter);
updatePageNumber();
setScrollListener(); setScrollListener();
observeStatus(0); observeStatus(0);
} }
@ -118,22 +166,19 @@ public class WebtoonReader extends BaseReader {
recycler.addOnScrollListener(new RecyclerView.OnScrollListener() { recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override @Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) { public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy); int page = layoutManager.findLastVisibleItemPosition();
if (page != currentPage) {
currentPage = layoutManager.findLastVisibleItemPosition(); onPageChanged(page);
updatePageNumber(); }
} }
}); });
} }
@Override
public boolean onImageTouch(MotionEvent motionEvent) {
return gestureDetector.onTouchEvent(motionEvent);
}
private void observeStatus(int position) { private void observeStatus(int position) {
if (position == pages.size()) if (position == pages.size()) {
unsubscribeStatus();
return; return;
}
final Page page = pages.get(position); final Page page = pages.get(position);

View File

@ -1,7 +1,7 @@
package eu.kanade.tachiyomi.ui.recent; package eu.kanade.tachiyomi.ui.recent;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.text.format.DateFormat; import android.text.format.DateUtils;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
@ -10,17 +10,39 @@ import android.widget.TextView;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.MangaChapter; import eu.kanade.tachiyomi.data.database.models.MangaChapter;
/**
* Adapter of RecentChaptersHolder.
* Connection between Fragment and Holder
* Holder updates should be called from here.
*/
public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHolder, Object> { public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHolder, Object> {
private RecentChaptersFragment fragment; /**
* Fragment of RecentChaptersFragment
*/
private final RecentChaptersFragment fragment;
private static final int CHAPTER = 0; /**
private static final int SECTION = 1; * The id of the view type
*/
private static final int VIEW_TYPE_CHAPTER = 0;
/**
* The id of the view type
*/
private static final int VIEW_TYPE_SECTION = 1;
/**
* Constructor
*
* @param fragment fragment
*/
public RecentChaptersAdapter(RecentChaptersFragment fragment) { public RecentChaptersAdapter(RecentChaptersFragment fragment) {
this.fragment = fragment; this.fragment = fragment;
setHasStableIds(true); setHasStableIds(true);
@ -35,6 +57,11 @@ public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHold
return item.hashCode(); return item.hashCode();
} }
/**
* Update items
*
* @param items items
*/
public void setItems(List<Object> items) { public void setItems(List<Object> items) {
mItems = items; mItems = items;
notifyDataSetChanged(); notifyDataSetChanged();
@ -47,18 +74,20 @@ public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHold
@Override @Override
public int getItemViewType(int position) { public int getItemViewType(int position) {
return getItem(position) instanceof MangaChapter ? CHAPTER : SECTION; return getItem(position) instanceof MangaChapter ? VIEW_TYPE_CHAPTER : VIEW_TYPE_SECTION;
} }
@Override @Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) { public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = LayoutInflater.from(parent.getContext()); LayoutInflater inflater = LayoutInflater.from(parent.getContext());
View v; View v;
// Check which view type and set correct values.
switch (viewType) { switch (viewType) {
case CHAPTER: case VIEW_TYPE_CHAPTER:
v = inflater.inflate(R.layout.item_recent_chapter, parent, false); v = inflater.inflate(R.layout.item_recent_chapter, parent, false);
return new RecentChaptersHolder(v, this, fragment); return new RecentChaptersHolder(v, this, fragment);
case SECTION: case VIEW_TYPE_SECTION:
v = inflater.inflate(R.layout.item_recent_chapter_section, parent, false); v = inflater.inflate(R.layout.item_recent_chapter_section, parent, false);
return new SectionViewHolder(v); return new SectionViewHolder(v);
} }
@ -67,12 +96,13 @@ public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHold
@Override @Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) { public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
// Check which view type and set correct values.
switch (holder.getItemViewType()) { switch (holder.getItemViewType()) {
case CHAPTER: case VIEW_TYPE_CHAPTER:
final MangaChapter chapter = (MangaChapter) getItem(position); final MangaChapter chapter = (MangaChapter) getItem(position);
((RecentChaptersHolder) holder).onSetValues(chapter); ((RecentChaptersHolder) holder).onSetValues(chapter);
break; break;
case SECTION: case VIEW_TYPE_SECTION:
final Date date = (Date) getItem(position); final Date date = (Date) getItem(position);
((SectionViewHolder) holder).onSetValues(date); ((SectionViewHolder) holder).onSetValues(date);
break; break;
@ -82,22 +112,29 @@ public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHold
holder.itemView.setActivated(isSelected(position)); holder.itemView.setActivated(isSelected(position));
} }
/**
* Returns fragment
* @return RecentChaptersFragment
*/
public RecentChaptersFragment getFragment() { public RecentChaptersFragment getFragment() {
return fragment; return fragment;
} }
private static class SectionViewHolder extends RecyclerView.ViewHolder { public static class SectionViewHolder extends RecyclerView.ViewHolder {
private TextView view; @Bind(R.id.section_text) TextView section;
private final long now = new Date().getTime();
public SectionViewHolder(View view) { public SectionViewHolder(View view) {
super(view); super(view);
this.view = (TextView) view; ButterKnife.bind(this, view);
} }
public void onSetValues(Date date) { public void onSetValues(Date date) {
String s = DateFormat.getDateFormat(view.getContext()).format(date); CharSequence s = DateUtils.getRelativeTimeSpanString(
view.setText(s); date.getTime(), now, DateUtils.DAY_IN_MILLIS);
section.setText(s);
} }
} }
} }

View File

@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.recent;
import android.content.Intent; import android.content.Intent;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
@ -9,18 +10,32 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.afollestad.materialdialogs.MaterialDialog;
import java.util.List; import java.util.List;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.database.models.MangaChapter; import eu.kanade.tachiyomi.data.database.models.MangaChapter;
import eu.kanade.tachiyomi.data.download.DownloadService;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder; import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment; import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
import eu.kanade.tachiyomi.ui.decoration.DividerItemDecoration; import eu.kanade.tachiyomi.ui.decoration.DividerItemDecoration;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity; import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
import nucleus.factory.RequiresPresenter; import nucleus.factory.RequiresPresenter;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
/**
* Fragment that shows recent chapters.
* Uses R.layout.fragment_recent_chapters.
* UI related actions should be called from here.
*/
@RequiresPresenter(RecentChaptersPresenter.class) @RequiresPresenter(RecentChaptersPresenter.class)
public class RecentChaptersFragment extends BaseRxFragment<RecentChaptersPresenter> implements FlexibleViewHolder.OnListItemClickListener { public class RecentChaptersFragment extends BaseRxFragment<RecentChaptersPresenter> implements FlexibleViewHolder.OnListItemClickListener {
@ -50,14 +65,21 @@ public class RecentChaptersFragment extends BaseRxFragment<RecentChaptersPresent
return view; return view;
} }
/**
* Populate adapter with chapters
*
* @param chapters list of chapters
*/
public void onNextMangaChapters(List<Object> chapters) { public void onNextMangaChapters(List<Object> chapters) {
adapter.setItems(chapters); adapter.setItems(chapters);
} }
@Override @Override
public boolean onListItemClick(int position) { public boolean onListItemClick(int position) {
// Get item from position
Object item = adapter.getItem(position); Object item = adapter.getItem(position);
if (item instanceof MangaChapter) { if (item instanceof MangaChapter) {
// Open chapter in reader
openChapter((MangaChapter) item); openChapter((MangaChapter) item);
} }
return false; return false;
@ -65,12 +87,114 @@ public class RecentChaptersFragment extends BaseRxFragment<RecentChaptersPresent
@Override @Override
public void onListItemLongClick(int position) { public void onListItemLongClick(int position) {
// Empty function
} }
protected void openChapter(MangaChapter chapter) { /**
* Open chapter in reader
*
* @param chapter selected chapter
*/
private void openChapter(MangaChapter chapter) {
getPresenter().onOpenChapter(chapter); getPresenter().onOpenChapter(chapter);
Intent intent = ReaderActivity.newIntent(getActivity()); Intent intent = ReaderActivity.newIntent(getActivity());
startActivity(intent); startActivity(intent);
} }
/**
* Update download status of chapter
*
* @param download download object containing download progress.
*/
public void onChapterStatusChange(Download download) {
RecentChaptersHolder holder = getHolder(download.chapter);
if (holder != null)
holder.onStatusChange(download.getStatus());
}
@Nullable
private RecentChaptersHolder getHolder(Chapter chapter) {
return (RecentChaptersHolder) recyclerView.findViewHolderForItemId(chapter.id);
}
/**
* Start downloading chapter
*
* @param chapters selected chapters
* @param manga manga that belongs to chapter
* @return true
*/
@SuppressWarnings("SameReturnValue")
protected boolean onDownload(Observable<Chapter> chapters, Manga manga) {
// Start the download service.
DownloadService.start(getActivity());
// Refresh data on download competition.
Observable<Chapter> observable = chapters
.doOnCompleted(adapter::notifyDataSetChanged);
// Download chapter.
getPresenter().downloadChapter(observable, manga);
return true;
}
/**
* Start deleting chapter
* @param chapters selected chapters
* @param manga manga that belongs to chapter
* @return success of deletion.
*/
protected boolean onDelete(Observable<Chapter> chapters, Manga manga) {
int size = adapter.getSelectedItemCount();
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
.title(R.string.deleting)
.progress(false, size, true)
.cancelable(false)
.show();
Observable<Chapter> observable = chapters
.concatMap(chapter -> {
getPresenter().deleteChapter(chapter, manga);
return Observable.just(chapter);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(chapter -> {
dialog.incrementProgress(1);
chapter.status = Download.NOT_DOWNLOADED;
})
.doOnCompleted(adapter::notifyDataSetChanged)
.finallyDo(dialog::dismiss);
getPresenter().deleteChapters(observable);
return true;
}
/**
* Mark chapter as read
*
* @param chapters selected chapter
* @return true
*/
@SuppressWarnings("SameReturnValue")
protected boolean onMarkAsRead(Observable<Chapter> chapters) {
getPresenter().markChaptersRead(chapters, true);
return true;
}
/**
* Mark chapter as unread
*
* @param chapters selected chapter
* @return true
*/
@SuppressWarnings("SameReturnValue")
protected boolean onMarkAsUnread(Observable<Chapter> chapters) {
getPresenter().markChaptersRead(chapters, false);
return true;
}
} }

View File

@ -1,35 +1,102 @@
package eu.kanade.tachiyomi.ui.recent; package eu.kanade.tachiyomi.ui.recent;
import android.support.v4.content.ContextCompat; import android.support.v4.content.ContextCompat;
import android.view.Menu;
import android.view.View; import android.view.View;
import android.widget.PopupMenu;
import android.widget.RelativeLayout;
import android.widget.TextView; import android.widget.TextView;
import butterknife.Bind; import butterknife.Bind;
import butterknife.ButterKnife; import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.MangaChapter; import eu.kanade.tachiyomi.data.database.models.MangaChapter;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder; import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import rx.Observable;
/**
* Holder that contains chapter item
* Uses R.layout.item_recent_chapter.
* UI related actions should be called from here.
*/
public class RecentChaptersHolder extends FlexibleViewHolder { public class RecentChaptersHolder extends FlexibleViewHolder {
/**
* Adapter for recent chapters
*/
private final RecentChaptersAdapter adapter;
/**
* TextView containing chapter title
*/
@Bind(R.id.chapter_title) TextView chapterTitle; @Bind(R.id.chapter_title) TextView chapterTitle;
/**
* TextView containing manga name
*/
@Bind(R.id.manga_title) TextView mangaTitle; @Bind(R.id.manga_title) TextView mangaTitle;
/**
* TextView containing download status
*/
@Bind(R.id.download_text) TextView downloadText;
/**
* RelativeLayout containing popup menu with download options
*/
@Bind(R.id.chapter_menu) RelativeLayout chapterMenu;
/**
* Color of read chapter
*/
private final int readColor; private final int readColor;
/**
* Color of unread chapter
*/
private final int unreadColor; private final int unreadColor;
/**
* Object containing chapter information
*/
private MangaChapter mangaChapter;
/**
* Constructor of RecentChaptersHolder
* @param view view of ChapterHolder
* @param adapter adapter of ChapterHolder
* @param onListItemClickListener ClickListener
*/
public RecentChaptersHolder(View view, RecentChaptersAdapter adapter, OnListItemClickListener onListItemClickListener) { public RecentChaptersHolder(View view, RecentChaptersAdapter adapter, OnListItemClickListener onListItemClickListener) {
super(view, adapter, onListItemClickListener); super(view, adapter, onListItemClickListener);
this.adapter = adapter;
ButterKnife.bind(this, view); ButterKnife.bind(this, view);
// Set colors.
readColor = ContextCompat.getColor(view.getContext(), R.color.hint_text); readColor = ContextCompat.getColor(view.getContext(), R.color.hint_text);
unreadColor = ContextCompat.getColor(view.getContext(), R.color.primary_text); unreadColor = ContextCompat.getColor(view.getContext(), R.color.primary_text);
//Set OnClickListener for download menu
chapterMenu.setOnClickListener(v -> v.post(() -> showPopupMenu(v)));
} }
/**
* Set values of view
*
* @param item item containing chapter information
*/
public void onSetValues(MangaChapter item) { public void onSetValues(MangaChapter item) {
this.mangaChapter = item;
// Set chapter title
chapterTitle.setText(item.chapter.name); chapterTitle.setText(item.chapter.name);
// Set manga title
mangaTitle.setText(item.manga.title); mangaTitle.setText(item.manga.title);
// Check if chapter is read and set correct color
if (item.chapter.read) { if (item.chapter.read) {
chapterTitle.setTextColor(readColor); chapterTitle.setTextColor(readColor);
mangaTitle.setTextColor(readColor); mangaTitle.setTextColor(readColor);
@ -37,6 +104,84 @@ public class RecentChaptersHolder extends FlexibleViewHolder {
chapterTitle.setTextColor(unreadColor); chapterTitle.setTextColor(unreadColor);
mangaTitle.setTextColor(unreadColor); mangaTitle.setTextColor(unreadColor);
} }
// Set chapter status
onStatusChange(item.chapter.status);
} }
/**
* Updates chapter status in view.
*
* @param status download status
*/
public void onStatusChange(int status) {
switch (status) {
case Download.QUEUE:
downloadText.setText(R.string.chapter_queued);
break;
case Download.DOWNLOADING:
downloadText.setText(R.string.chapter_downloading);
break;
case Download.DOWNLOADED:
downloadText.setText(R.string.chapter_downloaded);
break;
case Download.ERROR:
downloadText.setText(R.string.chapter_error);
break;
default:
downloadText.setText("");
break;
}
}
/**
* Show pop up menu
* @param view view containing popup menu.
*/
private void showPopupMenu(View view) {
// Create a PopupMenu, giving it the clicked view for an anchor
PopupMenu popup = new PopupMenu(adapter.getFragment().getActivity(), view);
// Inflate our menu resource into the PopupMenu's Menu
popup.getMenuInflater().inflate(R.menu.chapter_recent, popup.getMenu());
// Hide download and show delete if the chapter is downloaded and
if (mangaChapter.chapter.isDownloaded()) {
Menu menu = popup.getMenu();
menu.findItem(R.id.action_download).setVisible(false);
menu.findItem(R.id.action_delete).setVisible(true);
}
// Hide mark as unread when the chapter is unread
if (!mangaChapter.chapter.read /*&& mangaChapter.chapter.last_page_read == 0*/) {
popup.getMenu().findItem(R.id.action_mark_as_unread).setVisible(false);
}
// Hide mark as read when the chapter is read
if (mangaChapter.chapter.read) {
popup.getMenu().findItem(R.id.action_mark_as_read).setVisible(false);
}
// Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener(menuItem -> {
Observable<Chapter> chapterObservable = Observable.just(mangaChapter.chapter);
switch (menuItem.getItemId()) {
case R.id.action_download:
return adapter.getFragment().onDownload(chapterObservable, mangaChapter.manga);
case R.id.action_delete:
return adapter.getFragment().onDelete(chapterObservable, mangaChapter.manga);
case R.id.action_mark_as_read:
return adapter.getFragment().onMarkAsRead(chapterObservable);
case R.id.action_mark_as_unread:
return adapter.getFragment().onMarkAsUnread(chapterObservable);
}
return false;
});
// Finally show the PopupMenu
popup.show();
}
} }

View File

@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.ui.recent;
import android.os.Bundle; import android.os.Bundle;
import org.greenrobot.eventbus.EventBus;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Calendar; import java.util.Calendar;
import java.util.Collection; import java.util.Collection;
@ -12,44 +14,185 @@ import java.util.TreeMap;
import javax.inject.Inject; import javax.inject.Inject;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.data.database.DatabaseHelper; import eu.kanade.tachiyomi.data.database.DatabaseHelper;
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.models.MangaChapter;
import eu.kanade.tachiyomi.data.download.DownloadManager;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.data.source.SourceManager; import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.base.Source; import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
import eu.kanade.tachiyomi.event.ReaderEvent; import eu.kanade.tachiyomi.event.ReaderEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import rx.Observable; import rx.Observable;
import rx.android.schedulers.AndroidSchedulers; import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import timber.log.Timber;
/**
* Presenter of RecentChaptersFragment.
* Contains information and data for fragment.
* Observable updates should be called from here.
*/
public class RecentChaptersPresenter extends BasePresenter<RecentChaptersFragment> { public class RecentChaptersPresenter extends BasePresenter<RecentChaptersFragment> {
/**
* The id of the restartable.
*/
private static final int GET_RECENT_CHAPTERS = 1;
/**
* The id of the restartable.
*/
private static final int CHAPTER_STATUS_CHANGES = 2;
/**
* Used to connect to database
*/
@Inject DatabaseHelper db; @Inject DatabaseHelper db;
/**
* Used to get information from download manager
*/
@Inject DownloadManager downloadManager;
/**
* Used to get source from source id
*/
@Inject SourceManager sourceManager; @Inject SourceManager sourceManager;
private static final int GET_RECENT_CHAPTERS = 1; /**
* List containing chapter and manga information
*/
private List<MangaChapter> mangaChapters;
@Override @Override
protected void onCreate(Bundle savedState) { protected void onCreate(Bundle savedState) {
super.onCreate(savedState); super.onCreate(savedState);
// Used to get recent chapters
restartableLatestCache(GET_RECENT_CHAPTERS, restartableLatestCache(GET_RECENT_CHAPTERS,
this::getRecentChaptersObservable, this::getRecentChaptersObservable,
RecentChaptersFragment::onNextMangaChapters); (recentChaptersFragment, chapters) -> {
// Update adapter to show recent manga's
recentChaptersFragment.onNextMangaChapters(chapters);
// Update download status
updateChapterStatus(convertToMangaChaptersList(chapters));
});
if (savedState == null) // Used to update download status
startableLatestCache(CHAPTER_STATUS_CHANGES,
this::getChapterStatusObs,
RecentChaptersFragment::onChapterStatusChange,
(view, error) -> Timber.e(error.getCause(), error.getMessage()));
if (savedState == null) {
// Start fetching recent chapters
start(GET_RECENT_CHAPTERS); start(GET_RECENT_CHAPTERS);
}
} }
/**
* Returns a list only containing MangaChapter objects.
*
* @param input the list that will be converted.
* @return list containing MangaChapters objects.
*/
private List<MangaChapter> convertToMangaChaptersList(List<Object> input) {
// Create temp list
List<MangaChapter> tempMangaChapterList = new ArrayList<>();
// Only add MangaChapter objects
//noinspection Convert2streamapi
for (Object object : input) {
if (object instanceof MangaChapter) {
tempMangaChapterList.add((MangaChapter) object);
}
}
// Return temp list
return tempMangaChapterList;
}
/**
* Update status of chapters
*
* @param mangaChapters list containing recent chapters
*/
private void updateChapterStatus(List<MangaChapter> mangaChapters) {
// Set global list of chapters.
this.mangaChapters = mangaChapters;
// Update status.
//noinspection Convert2streamapi
for (MangaChapter mangaChapter : mangaChapters)
setChapterStatus(mangaChapter);
// Start onChapterStatusChange restartable.
start(CHAPTER_STATUS_CHANGES);
}
/**
* Returns observable containing chapter status.
*
* @return download object containing download progress.
*/
private Observable<Download> getChapterStatusObs() {
return downloadManager.getQueue().getStatusObservable()
.observeOn(AndroidSchedulers.mainThread())
.filter(download -> chapterIdEquals(download.chapter.id))
.doOnNext(this::updateChapterStatus);
}
/**
* Function to check if chapter is in recent list
* @param chaptersId id of chapter
* @return exist in recent list
*/
private boolean chapterIdEquals(Long chaptersId) {
for (MangaChapter mangaChapter : mangaChapters) {
if (chaptersId.equals(mangaChapter.chapter.id)) {
return true;
}
}
return false;
}
/**
* Update status of chapters.
*
* @param download download object containing progress.
*/
private void updateChapterStatus(Download download) {
// Loop through list
for (MangaChapter item : mangaChapters) {
if (download.chapter.id.equals(item.chapter.id)) {
item.chapter.status = download.getStatus();
break;
}
}
}
/**
* Get observable containing recent chapters and date
* @return observable containing recent chapters and date
*/
private Observable<List<Object>> getRecentChaptersObservable() { private Observable<List<Object>> getRecentChaptersObservable() {
return db.getRecentChapters().asRxObservable() // Set date for recent chapters
// group chapters by the date they were fetched on a ordered map Calendar cal = Calendar.getInstance();
cal.setTime(new Date());
cal.add(Calendar.MONTH, -1);
// Get recent chapters from database.
return db.getRecentChapters(cal.getTime()).asRxObservable()
// Group chapters by the date they were fetched on a ordered map.
.flatMap(recents -> Observable.from(recents) .flatMap(recents -> Observable.from(recents)
.toMultimap( .toMultimap(
recent -> getMapKey(recent.chapter.date_fetch), recent -> getMapKey(recent.chapter.date_fetch),
recent -> recent, recent -> recent,
() -> new TreeMap<>((d1, d2) -> d2.compareTo(d1)))) () -> new TreeMap<>((d1, d2) -> d2.compareTo(d1))))
// add every day and all its chapters to a single list // Add every day and all its chapters to a single list.
.map(recents -> { .map(recents -> {
List<Object> items = new ArrayList<>(); List<Object> items = new ArrayList<>();
for (Map.Entry<Date, Collection<MangaChapter>> recent : recents.entrySet()) { for (Map.Entry<Date, Collection<MangaChapter>> recent : recents.entrySet()) {
@ -61,6 +204,35 @@ public class RecentChaptersPresenter extends BasePresenter<RecentChaptersFragmen
.observeOn(AndroidSchedulers.mainThread()); .observeOn(AndroidSchedulers.mainThread());
} }
/**
* Set the chapter status
* @param mangaChapter MangaChapter which status gets updated
*/
private void setChapterStatus(MangaChapter mangaChapter) {
// Check if chapter in queue
for (Download download : downloadManager.getQueue()) {
if (mangaChapter.chapter.id.equals(download.chapter.id)) {
mangaChapter.chapter.status = download.getStatus();
return;
}
}
// Get source of chapter
Source source = sourceManager.get(mangaChapter.manga.source);
// Check if chapter is downloaded
if (downloadManager.isChapterDownloaded(source, mangaChapter.manga, mangaChapter.chapter)) {
mangaChapter.chapter.status = Download.DOWNLOADED;
} else {
mangaChapter.chapter.status = Download.NOT_DOWNLOADED;
}
}
/**
* Get date as time key
* @param date desired date
* @return date as time key
*/
private Date getMapKey(long date) { private Date getMapKey(long date) {
Calendar cal = Calendar.getInstance(); Calendar cal = Calendar.getInstance();
cal.setTime(new Date(date)); cal.setTime(new Date(date));
@ -71,8 +243,67 @@ public class RecentChaptersPresenter extends BasePresenter<RecentChaptersFragmen
return cal.getTime(); return cal.getTime();
} }
/**
* Open chapter in reader
* @param item chapter that is opened
*/
public void onOpenChapter(MangaChapter item) { public void onOpenChapter(MangaChapter item) {
Source source = sourceManager.get(item.manga.source); Source source = sourceManager.get(item.manga.source);
EventBus.getDefault().postSticky(new ReaderEvent(source, item.manga, item.chapter)); EventBus.getDefault().postSticky(new ReaderEvent(source, item.manga, item.chapter));
} }
/**
* Download selected chapter
* @param selectedChapter chapter that is selected
* @param manga manga that belongs to chapter
*/
public void downloadChapter(Observable<Chapter> selectedChapter, Manga manga) {
add(selectedChapter
.toList()
.subscribe(chapters -> {
EventBus.getDefault().postSticky(new DownloadChaptersEvent(manga, chapters));
}));
}
/**
* Delete selected chapter
* @param chapter chapter that is selected
* @param manga manga that belongs to chapter
*/
public void deleteChapter(Chapter chapter, Manga manga) {
Source source = sourceManager.get(manga.source);
downloadManager.deleteChapter(source, manga, chapter);
}
/**
* Delete selected chapter observable
* @param selectedChapters chapter that are selected
*/
public void deleteChapters(Observable<Chapter> selectedChapters) {
add(selectedChapters
.subscribe(chapter -> {
downloadManager.getQueue().remove(chapter);
}, error -> {
Timber.e(error.getMessage());
}));
}
/**
* Mark selected chapter as read
* @param selectedChapters chapter that is selected
* @param read read status
*/
public void markChaptersRead(Observable<Chapter> selectedChapters, boolean read) {
add(selectedChapters
.subscribeOn(Schedulers.io())
.map(chapter -> {
chapter.read = read;
if (!read) chapter.last_page_read = 0;
return chapter;
})
.toList()
.flatMap(chapters -> db.insertChapters(chapters).asRxObservable())
.observeOn(AndroidSchedulers.mainThread())
.subscribe());
}
} }

View File

@ -6,6 +6,8 @@ import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import com.afollestad.materialdialogs.MaterialDialog;
import java.text.DateFormat; import java.text.DateFormat;
import java.text.ParseException; import java.text.ParseException;
import java.text.SimpleDateFormat; import java.text.SimpleDateFormat;
@ -15,8 +17,23 @@ import java.util.TimeZone;
import eu.kanade.tachiyomi.BuildConfig; import eu.kanade.tachiyomi.BuildConfig;
import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.updater.UpdateChecker;
import eu.kanade.tachiyomi.data.updater.UpdateDownloader;
import eu.kanade.tachiyomi.util.ToastUtil;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class SettingsAboutFragment extends SettingsNestedFragment { public class SettingsAboutFragment extends SettingsNestedFragment {
/**
* Checks for new releases
*/
private UpdateChecker updateChecker;
/**
* The subscribtion service of the obtained release object
*/
private Subscription releaseSubscription;
public static SettingsNestedFragment newInstance(int resourcePreference, int resourceTitle) { public static SettingsNestedFragment newInstance(int resourcePreference, int resourceTitle) {
SettingsNestedFragment fragment = new SettingsAboutFragment(); SettingsNestedFragment fragment = new SettingsAboutFragment();
@ -25,14 +42,36 @@ public class SettingsAboutFragment extends SettingsNestedFragment {
} }
@Override @Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) { public void onCreate(Bundle savedInstanceState) {
//Check for update
updateChecker = new UpdateChecker(getActivity());
super.onCreate(savedInstanceState);
}
@Override
public void onDestroyView() {
if (releaseSubscription != null)
releaseSubscription.unsubscribe();
super.onDestroyView();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Preference version = findPreference(getString(R.string.pref_version)); Preference version = findPreference(getString(R.string.pref_version));
Preference buildTime = findPreference(getString(R.string.pref_build_time)); Preference buildTime = findPreference(getString(R.string.pref_build_time));
version.setSummary(BuildConfig.DEBUG ? "r" + BuildConfig.COMMIT_COUNT : version.setSummary(BuildConfig.DEBUG ? "r" + BuildConfig.COMMIT_COUNT :
BuildConfig.VERSION_NAME); BuildConfig.VERSION_NAME);
//Set onClickListener to check for new version
version.setOnPreferenceClickListener(preference -> {
if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER)
checkVersion();
return true;
});
buildTime.setSummary(getFormattedBuildTime()); buildTime.setSummary(getFormattedBuildTime());
return super.onCreateView(inflater, container, savedState); return super.onCreateView(inflater, container, savedState);
@ -54,4 +93,40 @@ public class SettingsAboutFragment extends SettingsNestedFragment {
} }
return ""; return "";
} }
/**
* Checks version and shows a user prompt when update available.
*/
private void checkVersion() {
releaseSubscription = updateChecker.checkForApplicationUpdate()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(release -> {
//Get version of latest release
String newVersion = release.getVersion();
newVersion = newVersion.replaceAll("[^\\d.]", "");
//Check if latest version is different from current version
if (!newVersion.equals(BuildConfig.VERSION_NAME)) {
String downloadLink = release.getDownloadLink();
String body = release.getChangeLog();
//Create confirmation window
new MaterialDialog.Builder(getActivity())
.title(getString(R.string.update_check_title))
.content(body)
.positiveText(getString(R.string.update_check_confirm))
.negativeText(getString(R.string.update_check_ignore))
.onPositive((dialog, which) -> {
// User output that download has started
ToastUtil.showShort(getActivity(), getString(R.string.update_check_download_started));
// Start download
new UpdateDownloader(getActivity().getApplicationContext()).execute(downloadLink);
})
.show();
} else {
ToastUtil.showShort(getActivity(), getString(R.string.update_check_no_new_updates));
}
}, Throwable::printStackTrace);
}
} }

View File

@ -71,7 +71,7 @@ public class SettingsAdvancedFragment extends SettingsNestedFragment {
subscriptions.add(Observable.defer(() -> Observable.from(files)) subscriptions.add(Observable.defer(() -> Observable.from(files))
.concatMap(file -> { .concatMap(file -> {
if (chapterCache.remove(file.getName())) { if (chapterCache.removeFileFromCache(file.getName())) {
deletedFiles.incrementAndGet(); deletedFiles.incrementAndGet();
} }
return Observable.just(file); return Observable.just(file);

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.setting;
import android.app.Activity; import android.app.Activity;
import android.content.Intent; import android.content.Intent;
import android.net.Uri; import android.net.Uri;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.preference.Preference; import android.preference.Preference;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
@ -61,6 +62,13 @@ public class SettingsDownloadsFragment extends SettingsNestedFragment {
if (requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) { if (requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) {
Uri uri = data.getData(); Uri uri = data.getData();
preferences.setDownloadsDirectory(uri.getPath()); preferences.setDownloadsDirectory(uri.getPath());
// Persist access permissions.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
getActivity().getContentResolver().takePersistableUriPermission(uri,
Intent.FLAG_GRANT_READ_URI_PERMISSION |
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
}
} }
} }

View File

@ -10,12 +10,18 @@ import eu.kanade.tachiyomi.data.database.models.Manga;
public class ChapterRecognition { public class ChapterRecognition {
private static final Pattern p1 = Pattern.compile("ch[^0-9]?\\s*(\\d+[\\.,]?\\d*)"); private static final Pattern cleanWithToken = Pattern.compile("ch[^0-9]?\\s*(\\d+[\\.,]?\\d+)($|\\b)");
private static final Pattern p2 = Pattern.compile("(\\d+[\\.,]?\\d*)"); private static final Pattern uncleanWithToken = Pattern.compile("ch[^0-9]?\\s*(\\d+[\\.,]?\\d*)");
private static final Pattern p3 = Pattern.compile("(\\d+[\\.,]?\\d*\\s*:)"); private static final Pattern withAlphaPostfix = Pattern.compile("(\\d+[\\.,]?\\d*\\s*)([a-z])($|\\b)");
private static final Pattern cleanNumber = Pattern.compile("(\\d+[\\.,]?\\d+)($|\\b)");
private static final Pattern uncleanNumber = Pattern.compile("(\\d+[\\.,]?\\d*)");
private static final Pattern withColon = Pattern.compile("(\\d+[\\.,]?\\d*\\s*:)([^\\d]|$)");
private static final Pattern startingNumber = Pattern.compile("^(\\d+[\\.,]?\\d*)");
private static final Pattern pUnwanted = private static final Pattern pUnwanted =
Pattern.compile("\\b(v|ver|vol|version|volume)\\.?\\s*\\d+\\b"); Pattern.compile("(\\b|\\d)(v|ver|vol|version|volume)\\.?\\s*\\d+\\b");
private static final Pattern pPart =
Pattern.compile("(\\b|\\d)part\\s*\\d+.+");
public static void parseChapterNumber(Chapter chapter, Manga manga) { public static void parseChapterNumber(Chapter chapter, Manga manga) {
if (chapter.chapter_number != -1) if (chapter.chapter_number != -1)
@ -24,20 +30,34 @@ public class ChapterRecognition {
String name = chapter.name.toLowerCase(); String name = chapter.name.toLowerCase();
Matcher matcher; Matcher matcher;
// Safest option, the chapter has a token prepended // Safest option, the chapter has a token prepended and nothing at the end of the number
matcher = p1.matcher(name); matcher = cleanWithToken.matcher(name);
if (matcher.find()) {
chapter.chapter_number = Float.parseFloat(matcher.group(1));
return;
}
// a number with a single alpha prefix is parsed as sub-chapter
matcher = withAlphaPostfix.matcher(name);
if (matcher.find()) {
chapter.chapter_number = Float.parseFloat(matcher.group(1)) + parseAlphaPostFix(matcher.group(2));
return;
}
// the chapter has a token prepended and something at the end of the number
matcher = uncleanWithToken.matcher(name);
if (matcher.find()) { if (matcher.find()) {
chapter.chapter_number = Float.parseFloat(matcher.group(1)); chapter.chapter_number = Float.parseFloat(matcher.group(1));
return; return;
} }
// Remove anything related to the volume or version // Remove anything related to the volume or version
name = pUnwanted.matcher(name).replaceAll(""); name = pUnwanted.matcher(name).replaceAll("$1");
List<Float> occurrences; List<Float> occurrences;
// If there's only one number, use it // If there's only one number, use it
matcher = p2.matcher(name); matcher = uncleanNumber.matcher(name);
occurrences = getAllOccurrences(matcher); occurrences = getAllOccurrences(matcher);
if (occurrences.size() == 1) { if (occurrences.size() == 1) {
chapter.chapter_number = occurrences.get(0); chapter.chapter_number = occurrences.get(0);
@ -45,7 +65,15 @@ public class ChapterRecognition {
} }
// If it has a colon, the chapter number should be that one // If it has a colon, the chapter number should be that one
matcher = p3.matcher(name); matcher = withColon.matcher(name);
occurrences = getAllOccurrences(matcher);
if (occurrences.size() == 1) {
chapter.chapter_number = occurrences.get(0);
return;
}
// Prefer numbers without anything appended
matcher = cleanNumber.matcher(name);
occurrences = getAllOccurrences(matcher); occurrences = getAllOccurrences(matcher);
if (occurrences.size() == 1) { if (occurrences.size() == 1) {
chapter.chapter_number = occurrences.get(0); chapter.chapter_number = occurrences.get(0);
@ -57,9 +85,9 @@ public class ChapterRecognition {
// Try to remove the manga name from the chapter, and try again // Try to remove the manga name from the chapter, and try again
String mangaName = replaceIrrelevantCharacters(manga.title); String mangaName = replaceIrrelevantCharacters(manga.title);
String nameWithoutManga = difference(mangaName, name); String nameWithoutManga = difference(mangaName, name).trim();
if (!nameWithoutManga.isEmpty()) { if (!nameWithoutManga.isEmpty()) {
matcher = p2.matcher(nameWithoutManga); matcher = uncleanNumber.matcher(nameWithoutManga);
occurrences = getAllOccurrences(matcher); occurrences = getAllOccurrences(matcher);
if (occurrences.size() == 1) { if (occurrences.size() == 1) {
chapter.chapter_number = occurrences.get(0); chapter.chapter_number = occurrences.get(0);
@ -69,6 +97,52 @@ public class ChapterRecognition {
// TODO more checks (maybe levenshtein?) // TODO more checks (maybe levenshtein?)
// try splitting the name in parts an pick the first valid one
String[] nameParts = chapter.name.split("-");
Chapter dummyChapter = Chapter.create();
if (nameParts.length > 1) {
for (String part : nameParts) {
dummyChapter.name = part;
parseChapterNumber(dummyChapter, manga);
if (dummyChapter.chapter_number >= 0) {
chapter.chapter_number = dummyChapter.chapter_number;
return;
}
}
}
// Strip anything after "part xxx" and try that
matcher = pPart.matcher(name);
if (matcher.find()) {
name = pPart.matcher(name).replaceAll("$1");
dummyChapter.name = name;
parseChapterNumber(dummyChapter, manga);
if (dummyChapter.chapter_number >= 0) {
chapter.chapter_number = dummyChapter.chapter_number;
return;
}
}
// check for a number either at the start or right after the manga title
matcher = startingNumber.matcher(name);
if (matcher.find()) {
chapter.chapter_number = Float.parseFloat(matcher.group(1));
return;
}
matcher = startingNumber.matcher(nameWithoutManga);
if (matcher.find()) {
chapter.chapter_number = Float.parseFloat(matcher.group(1));
return;
}
}
/**
* x.a -> x.1, x.b -> x.2, etc
*/
private static float parseAlphaPostFix(String postfix) {
char alpha = postfix.charAt(0);
return Float.parseFloat("0." + Integer.toString((int)alpha - 96));
} }
public static List<Float> getAllOccurrences(Matcher matcher) { public static List<Float> getAllOccurrences(Matcher matcher) {
@ -76,7 +150,7 @@ public class ChapterRecognition {
while (matcher.find()) { while (matcher.find()) {
// Match again to get only numbers from the captured text // Match again to get only numbers from the captured text
String text = matcher.group(); String text = matcher.group();
Matcher m = p2.matcher(text); Matcher m = uncleanNumber.matcher(text);
if (m.find()) { if (m.find()) {
try { try {
Float value = Float.parseFloat(m.group(1)); Float value = Float.parseFloat(m.group(1));

View File

@ -1,7 +0,0 @@
package eu.kanade.tachiyomi.util;
import java.lang.annotation.ElementType;
import java.lang.annotation.Target;
@Target({ElementType.METHOD})
public @interface EventBusHook {}

View File

@ -1,32 +1,26 @@
package eu.kanade.tachiyomi.util; package eu.kanade.tachiyomi.util;
import android.util.Pair;
import java.util.List;
import rx.Observable; import rx.Observable;
import rx.functions.Func1;
import rx.subjects.PublishSubject; import rx.subjects.PublishSubject;
public class RxPager { public class RxPager<T> {
private final int initialPageCount; private final PublishSubject<List<T>> results = PublishSubject.create();
private final PublishSubject<Integer> requests = PublishSubject.create();
private int requestedCount; private int requestedCount;
public RxPager() { public Observable<Pair<Integer, List<T>>> results() {
this(1); requestedCount = 0;
return results.map(list -> Pair.create(requestedCount++, list));
} }
public RxPager(int initialPageCount) { public Observable<List<T>> request(Func1<Integer, Observable<List<T>>> networkObservable) {
this.initialPageCount = initialPageCount; return networkObservable.call(requestedCount).doOnNext(results::onNext);
} }
public void requestNext(int page) { }
requests.onNext(page);
}
public Observable<Integer> pages() {
return requests
.concatMap(targetPage -> targetPage <= requestedCount ?
Observable.<Integer>empty() :
Observable.range(requestedCount, targetPage - requestedCount))
.startWith(Observable.range(0, initialPageCount))
.doOnNext(it -> requestedCount = it + 1);
}
}

View File

@ -5,6 +5,10 @@ import java.net.URISyntaxException;
public class UrlUtil { public class UrlUtil {
private static final String JPG = ".jpg";
private static final String PNG = ".png";
private static final String GIF = ".gif";
public static String getPath(String s) { public static String getPath(String s) {
try { try {
URI uri = new URI(s); URI uri = new URI(s);
@ -18,4 +22,37 @@ public class UrlUtil {
return s; return s;
} }
} }
public static boolean isJpg(String url) {
return containsIgnoreCase(url, JPG);
}
public static boolean isPng(String url) {
return containsIgnoreCase(url, PNG);
}
public static boolean isGif(String url) {
return containsIgnoreCase(url, GIF);
}
public static boolean containsIgnoreCase(String src, String what) {
final int length = what.length();
if (length == 0)
return true; // Empty string is contained
final char firstLo = Character.toLowerCase(what.charAt(0));
final char firstUp = Character.toUpperCase(what.charAt(0));
for (int i = src.length() - length; i >= 0; i--) {
// Quick check before calling the more expensive regionMatches() method:
final char ch = src.charAt(i);
if (ch != firstLo && ch != firstUp)
continue;
if (src.regionMatches(true, i, what, 0, length))
return true;
}
return false;
}
} }

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