mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
147 Commits
Author | SHA1 | Date | |
---|---|---|---|
110df59197 | |||
75ae4081d8 | |||
2efca050b3 | |||
37e119c4f2 | |||
9786074119 | |||
d4876b426f | |||
8581e4667a | |||
b94f86765d | |||
ba0f3778ce | |||
aac6b242a0 | |||
dec9442a65 | |||
b33da641d9 | |||
96d498e7e5 | |||
eee137a084 | |||
5e834ae3be | |||
dcfda61aba | |||
5ac7f7057a | |||
ff46c61f63 | |||
57b64a412e | |||
1e81f75377 | |||
1dd49a2ab1 | |||
1cd77a97a7 | |||
f820522a69 | |||
3da613dedb | |||
5c329d2314 | |||
4c073e713d | |||
2832f4ae5e | |||
7690e8a53f | |||
3a19f8e40b | |||
a33b525f9e | |||
7d6ce46829 | |||
a90a4bf80c | |||
140bf8caee | |||
56a45f263e | |||
01d6ddfafb | |||
393b4916f6 | |||
cb3c3af865 | |||
5a83976fa5 | |||
a81f6c3ac4 | |||
6846ce5bfb | |||
0c0ebe06e5 | |||
e50c683159 | |||
872af276ea | |||
e6faee9779 | |||
bc1ddd4379 | |||
e348d6c1cf | |||
7835921045 | |||
1611a274b9 | |||
fa4a8204a4 | |||
5977e9f47f | |||
63d0161da5 | |||
d8b46c1969 | |||
f84731c2df | |||
50d71d1395 | |||
4be0b2502e | |||
6c069ad87b | |||
e69011ac5b | |||
ea130a0899 | |||
2566862e0f | |||
16081817c2 | |||
945625d3ad | |||
050b9c9fce | |||
c35184abdc | |||
34c5f0b7ba | |||
6435eeb251 | |||
eec2dcd981 | |||
57ba368ae0 | |||
ed06469885 | |||
79cd8c691e | |||
391550f49a | |||
6aa07dd17e | |||
aada373a0c | |||
3deac86bbe | |||
d7aef2e97a | |||
7953ba6e78 | |||
8aa3c2a260 | |||
c204548df5 | |||
4d47f5a387 | |||
7944bb8479 | |||
c4ae88a8ff | |||
ad953b7bf6 | |||
d799ae5d72 | |||
a3ec057384 | |||
486f129e62 | |||
e6c3864c71 | |||
7461f12066 | |||
e53b05feba | |||
bcefc176c1 | |||
d0580d0df1 | |||
28fd22dfe0 | |||
742924625d | |||
78a2eae719 | |||
38bb0b61d4 | |||
8b52fea602 | |||
c03495be94 | |||
f19889c222 | |||
af0ab5ec86 | |||
ea4fa60e01 | |||
4b60560a9f | |||
733b0da461 | |||
db074a371d | |||
bb110ce353 | |||
74c32f9e16 | |||
d8ab8f297f | |||
ec7df6b1f2 | |||
ef03ca22d1 | |||
82865dd3fd | |||
ba5d13936c | |||
23a6f76c37 | |||
0c9bc97fe8 | |||
c6ecfb2f67 | |||
8ca0814aff | |||
eceb4c3682 | |||
e7ecd5a5c2 | |||
f7c20a5517 | |||
6f409c0e3b | |||
0a31c223e3 | |||
0f42956f3f | |||
ac2485d4a7 | |||
7993ec5074 | |||
4521174138 | |||
27b95e9d73 | |||
a54425f47d | |||
4918e67fda | |||
b174adbab0 | |||
59cc87c583 | |||
0e87dc995a | |||
fad7b75b96 | |||
c99c90fc4c | |||
594219848d | |||
fa301bfbd2 | |||
50306f6ea3 | |||
9b90ad0a3b | |||
5c854984e4 | |||
6c844cfd9c | |||
9e666dcdb3 | |||
e81f98a975 | |||
11dc0d7e9e | |||
07ed2e2ebb | |||
e1aa460106 | |||
75a77566cf | |||
dd0a2d842a | |||
fa71e906c9 | |||
e6a17e25a9 | |||
d88513de56 | |||
ad97d03f1d | |||
7fc23d526b |
30
.github/CONTRIBUTING.md
vendored
Normal file
30
.github/CONTRIBUTING.md
vendored
Normal 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
6
.github/ISSUE_TEMPLATE.md
vendored
Normal 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
|
15
README.md
15
README.md
@ -1,8 +1,16 @@
|
|||||||
|
[](https://github.com/inorichi/tachiyomi/releases)
|
||||||
|
[](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi)
|
||||||
|
[](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
|
|
||||||
|
|
||||||
[](https://github.com/inorichi/tachiyomi/releases/download/v0.1.0/tachiyomi-v0.1.0.apk)
|
|
||||||
[](http://tachiyomi.kanade.eu/latest/app-debug.apk)
|
|
||||||
|
|
||||||
## License
|
## License
|
||||||
|
|
||||||
Copyright 2015 Javier Tomás
|
Copyright 2015 Javier Tomás
|
||||||
|
@ -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"
|
||||||
|
40
app/proguard-rules.pro
vendored
40
app/proguard-rules.pro
vendored
@ -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.** { *; }
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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);
|
||||||
|
@ -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) {
|
||||||
|
110
app/src/main/java/eu/kanade/tachiyomi/data/io/IOHandler.java
Normal file
110
app/src/main/java/eu/kanade/tachiyomi/data/io/IOHandler.java
Normal 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;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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.
|
||||||
|
@ -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() {
|
||||||
|
@ -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();
|
||||||
|
|
||||||
|
}
|
93
app/src/main/java/eu/kanade/tachiyomi/data/rest/Release.java
Normal file
93
app/src/main/java/eu/kanade/tachiyomi/data/rest/Release.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
||||||
|
@ -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) {
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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§ion=login";
|
public static final String LOGIN_URL = BASE_URL + "/forums/index.php?app=core&module=global§ion=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 {
|
||||||
|
@ -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", "");
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
12
app/src/main/java/eu/kanade/tachiyomi/event/MangaEvent.java
Normal file
12
app/src/main/java/eu/kanade/tachiyomi/event/MangaEvent.java
Normal 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;
|
||||||
|
}
|
||||||
|
}
|
@ -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();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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}.
|
||||||
|
@ -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
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -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++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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));
|
||||||
|
@ -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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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
|
||||||
|
@ -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())
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -1,7 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.ui.reader.viewer.base;
|
|
||||||
|
|
||||||
public interface OnChapterSingleTapListener {
|
|
||||||
void onCenterTap();
|
|
||||||
void onLeftSideTap();
|
|
||||||
void onRightSideTap();
|
|
||||||
}
|
|
@ -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();
|
@ -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();
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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() {
|
||||||
|
@ -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);
|
||||||
|
|
||||||
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -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);
|
||||||
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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));
|
||||||
|
@ -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 {}
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
@ -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
Reference in New Issue
Block a user