Compare commits
88 Commits
Author | SHA1 | Date | |
---|---|---|---|
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 | |||
0210fd8828 | |||
0332d8dd79 | |||
111ec5541f | |||
4bf15a5a2c | |||
416fd128ba | |||
dda0c50a1c | |||
f0a3c9c2dc | |||
8520a47286 | |||
522e900b5a | |||
b9bb41164f | |||
2b2fa0de2f | |||
e747083b06 | |||
e08dd95435 | |||
173e86320b | |||
b2e579173b | |||
79229d9c6a | |||
d25cbe9005 |
30
CONTRIBUTING.md
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)
|
10
README.md
@ -1,8 +1,14 @@
|
|||||||
Tachiyomi is a manga reader for Android free and open source.
|
[](https://github.com/inorichi/tachiyomi/releases)
|
||||||
|
[](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi)
|
||||||
|
[](http://tachiyomi.kanade.eu/latest/app-debug.apk)
|
||||||
|
|
||||||
|
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:
|
Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened issues.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
* Online and offline reading
|
* Online and offline reading
|
||||||
* Configurable reader with multiple viewers and settings
|
* Configurable reader with multiple viewers and settings
|
||||||
|
@ -39,8 +39,8 @@ android {
|
|||||||
minSdkVersion 16
|
minSdkVersion 16
|
||||||
targetSdkVersion 23
|
targetSdkVersion 23
|
||||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||||
versionCode 1
|
versionCode 4
|
||||||
versionName "0.1.0"
|
versionName "0.1.3"
|
||||||
|
|
||||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||||
@ -53,6 +53,9 @@ android {
|
|||||||
}
|
}
|
||||||
|
|
||||||
buildTypes {
|
buildTypes {
|
||||||
|
debug {
|
||||||
|
applicationIdSuffix ".debug"
|
||||||
|
}
|
||||||
release {
|
release {
|
||||||
minifyEnabled true
|
minifyEnabled true
|
||||||
shrinkResources true
|
shrinkResources true
|
||||||
@ -70,6 +73,7 @@ android {
|
|||||||
|
|
||||||
lintOptions {
|
lintOptions {
|
||||||
abortOnError false
|
abortOnError false
|
||||||
|
checkReleaseBuilds false
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@ -77,8 +81,9 @@ android {
|
|||||||
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 OKHTTP_VERSION = '3.0.1'
|
||||||
final MOCKITO_VERSION = '1.10.19'
|
final MOCKITO_VERSION = '1.10.19'
|
||||||
final STORIO_VERSION = '1.7.0'
|
final STORIO_VERSION = '1.8.0'
|
||||||
final ICEPICK_VERSION = '3.1.0'
|
final ICEPICK_VERSION = '3.1.0'
|
||||||
|
|
||||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||||
@ -90,8 +95,9 @@ 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'
|
||||||
@ -111,9 +117,9 @@ dependencies {
|
|||||||
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 'com.github.pwittchen:reactivenetwork:0.1.5'
|
||||||
|
|
||||||
compile "com.google.dagger:dagger:$DAGGER_VERSION"
|
compile "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||||
@ -124,10 +130,15 @@ dependencies {
|
|||||||
compile('com.mikepenz:materialdrawer:4.6.4@aar') {
|
compile('com.mikepenz:materialdrawer:4.6.4@aar') {
|
||||||
transitive = true
|
transitive = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//Google material icons SVG.
|
||||||
|
compile 'com.mikepenz:google-material-typeface:2.1.0.1.original@aar'
|
||||||
|
|
||||||
compile('com.github.afollestad.material-dialogs:core:0.8.5.3@aar') {
|
compile('com.github.afollestad.material-dialogs:core:0.8.5.3@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"
|
||||||
|
6
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
|
||||||
|
@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.injection.module.AppModule;
|
|||||||
import timber.log.Timber;
|
import timber.log.Timber;
|
||||||
|
|
||||||
@ReportsCrashes(
|
@ReportsCrashes(
|
||||||
formUri = "http://mangafeed.kanade.eu/crash_report",
|
formUri = "http://tachiyomi.kanade.eu/crash_report",
|
||||||
reportType = org.acra.sender.HttpSender.Type.JSON,
|
reportType = org.acra.sender.HttpSender.Type.JSON,
|
||||||
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
httpMethod = org.acra.sender.HttpSender.Method.PUT,
|
||||||
excludeMatchingSharedPreferencesKeys={".*username.*",".*password.*"}
|
excludeMatchingSharedPreferencesKeys={".*username.*",".*password.*"}
|
||||||
|
@ -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!
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -27,10 +27,12 @@ import eu.kanade.tachiyomi.data.database.models.ChapterSQLiteTypeMapping;
|
|||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory;
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory;
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategorySQLiteTypeMapping;
|
import eu.kanade.tachiyomi.data.database.models.MangaCategorySQLiteTypeMapping;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSQLiteTypeMapping;
|
import eu.kanade.tachiyomi.data.database.models.MangaSQLiteTypeMapping;
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaSyncSQLiteTypeMapping;
|
import eu.kanade.tachiyomi.data.database.models.MangaSyncSQLiteTypeMapping;
|
||||||
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver;
|
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver;
|
||||||
|
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver;
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
||||||
@ -160,23 +162,14 @@ public class DatabaseHelper {
|
|||||||
.prepare();
|
.prepare();
|
||||||
}
|
}
|
||||||
|
|
||||||
public PreparedGetListOfObjects<Chapter> getChapters(long manga_id, boolean sortAToZ, boolean onlyUnread) {
|
public PreparedGetListOfObjects<MangaChapter> getRecentChapters(Date date) {
|
||||||
Query.CompleteBuilder query = Query.builder()
|
|
||||||
.table(ChapterTable.TABLE)
|
|
||||||
|
|
||||||
.orderBy(ChapterTable.COLUMN_CHAPTER_NUMBER + (sortAToZ ? " ASC" : " DESC"));
|
|
||||||
|
|
||||||
if (onlyUnread) {
|
|
||||||
query = query.where(ChapterTable.COLUMN_MANGA_ID + "=? AND " + ChapterTable.COLUMN_READ + "=?")
|
|
||||||
.whereArgs(manga_id, 0);
|
|
||||||
} else {
|
|
||||||
query = query.where(ChapterTable.COLUMN_MANGA_ID + "=?")
|
|
||||||
.whereArgs(manga_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return db.get()
|
return db.get()
|
||||||
.listOfObjects(Chapter.class)
|
.listOfObjects(MangaChapter.class)
|
||||||
.withQuery(query.build())
|
.withQuery(RawQuery.builder()
|
||||||
|
.query(MangaChapterGetResolver.getRecentChaptersQuery(date))
|
||||||
|
.observesTables(ChapterTable.TABLE)
|
||||||
|
.build())
|
||||||
|
.withGetResolver(MangaChapterGetResolver.INSTANCE)
|
||||||
.prepare();
|
.prepare();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -6,9 +6,9 @@ import android.database.sqlite.SQLiteOpenHelper;
|
|||||||
import android.support.annotation.NonNull;
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
||||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
||||||
|
|
||||||
public class DbOpenHelper extends SQLiteOpenHelper {
|
public class DbOpenHelper extends SQLiteOpenHelper {
|
||||||
|
@ -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;
|
||||||
|
@ -0,0 +1,12 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.models;
|
||||||
|
|
||||||
|
public class MangaChapter {
|
||||||
|
|
||||||
|
public Manga manga;
|
||||||
|
public Chapter chapter;
|
||||||
|
|
||||||
|
public MangaChapter(Manga manga, Chapter chapter) {
|
||||||
|
this.manga = manga;
|
||||||
|
this.chapter = chapter;
|
||||||
|
}
|
||||||
|
}
|
@ -5,8 +5,8 @@ import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
|||||||
|
|
||||||
import java.io.Serializable;
|
import java.io.Serializable;
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||||
|
|
||||||
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
|
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
|
||||||
public class MangaSync implements Serializable {
|
public class MangaSync implements Serializable {
|
||||||
|
@ -0,0 +1,56 @@
|
|||||||
|
package eu.kanade.tachiyomi.data.database.resolvers;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.support.annotation.NonNull;
|
||||||
|
|
||||||
|
import com.pushtorefresh.storio.sqlite.operations.get.DefaultGetResolver;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.ChapterStorIOSQLiteGetResolver;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver;
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
||||||
|
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
||||||
|
|
||||||
|
public class MangaChapterGetResolver extends DefaultGetResolver<MangaChapter> {
|
||||||
|
|
||||||
|
public static final MangaChapterGetResolver INSTANCE = new MangaChapterGetResolver();
|
||||||
|
|
||||||
|
public static final String QUERY = String.format(
|
||||||
|
"SELECT * FROM %1$s JOIN %2$s on %1$s.%3$s = %2$s.%4$s",
|
||||||
|
MangaTable.TABLE,
|
||||||
|
ChapterTable.TABLE,
|
||||||
|
MangaTable.COLUMN_ID,
|
||||||
|
ChapterTable.COLUMN_MANGA_ID);
|
||||||
|
|
||||||
|
public static String getRecentChaptersQuery(Date date) {
|
||||||
|
return QUERY + String.format(" WHERE %1$s = 1 AND %2$s > %3$d ORDER BY %2$s DESC",
|
||||||
|
MangaTable.COLUMN_FAVORITE,
|
||||||
|
ChapterTable.COLUMN_DATE_UPLOAD,
|
||||||
|
date.getTime());
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final MangaStorIOSQLiteGetResolver mangaGetResolver;
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
private final ChapterStorIOSQLiteGetResolver chapterGetResolver;
|
||||||
|
|
||||||
|
public MangaChapterGetResolver() {
|
||||||
|
this.mangaGetResolver = new MangaStorIOSQLiteGetResolver();
|
||||||
|
this.chapterGetResolver = new ChapterStorIOSQLiteGetResolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
|
@Override
|
||||||
|
public MangaChapter mapFromCursor(@NonNull Cursor cursor) {
|
||||||
|
final Manga manga = mangaGetResolver.mapFromCursor(cursor);
|
||||||
|
final Chapter chapter = chapterGetResolver.mapFromCursor(cursor);
|
||||||
|
manga.id = chapter.manga_id;
|
||||||
|
|
||||||
|
return new MangaChapter(manga, chapter);
|
||||||
|
}
|
||||||
|
}
|
@ -1,23 +0,0 @@
|
|||||||
package eu.kanade.tachiyomi.data.database.resolvers;
|
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.support.annotation.NonNull;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaStorIOSQLiteGetResolver;
|
|
||||||
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
|
|
||||||
|
|
||||||
public class MangaWithUnreadGetResolver extends MangaStorIOSQLiteGetResolver {
|
|
||||||
|
|
||||||
public static final MangaWithUnreadGetResolver INSTANCE = new MangaWithUnreadGetResolver();
|
|
||||||
|
|
||||||
@Override
|
|
||||||
@NonNull
|
|
||||||
public Manga mapFromCursor(@NonNull Cursor cursor) {
|
|
||||||
Manga manga = super.mapFromCursor(cursor);
|
|
||||||
int unreadColumn = cursor.getColumnIndex(MangaTable.COLUMN_UNREAD);
|
|
||||||
manga.unread = cursor.getInt(unreadColumn);
|
|
||||||
return manga;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@ -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;
|
||||||
@ -27,6 +26,7 @@ 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.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;
|
||||||
@ -42,10 +42,13 @@ public class DownloadManager {
|
|||||||
private PreferencesHelper preferences;
|
private PreferencesHelper preferences;
|
||||||
private Gson gson;
|
private Gson gson;
|
||||||
|
|
||||||
private PublishSubject<Download> downloadsQueueSubject;
|
private PublishSubject<List<Download>> downloadsQueueSubject;
|
||||||
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;
|
||||||
|
|
||||||
@ -61,14 +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
|
||||||
.flatMap(this::downloadChapter, preferences.downloadThreads())
|
.flatMap(Observable::from)
|
||||||
|
.lift(new DynamicConcurrentMergeOperator<>(this::downloadChapter, threadsSubject))
|
||||||
.onBackpressureBuffer()
|
.onBackpressureBuffer()
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.map(download -> areAllDownloadsFinished())
|
.map(download -> areAllDownloadsFinished())
|
||||||
@ -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
|
||||||
@ -103,6 +116,7 @@ public class DownloadManager {
|
|||||||
|
|
||||||
// Used to avoid downloading chapters with the same name
|
// Used to avoid downloading chapters with the same name
|
||||||
final List<String> addedChapters = new ArrayList<>();
|
final List<String> addedChapters = new ArrayList<>();
|
||||||
|
final List<Download> pending = new ArrayList<>();
|
||||||
|
|
||||||
for (Chapter chapter : event.getChapters()) {
|
for (Chapter chapter : event.getChapters()) {
|
||||||
if (addedChapters.contains(chapter.name))
|
if (addedChapters.contains(chapter.name))
|
||||||
@ -113,9 +127,10 @@ public class DownloadManager {
|
|||||||
|
|
||||||
if (!prepareDownload(download)) {
|
if (!prepareDownload(download)) {
|
||||||
queue.add(download);
|
queue.add(download);
|
||||||
if (isRunning) downloadsQueueSubject.onNext(download);
|
pending.add(download);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if (isRunning) downloadsQueueSubject.onNext(pending);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Public method to check if a chapter is downloaded
|
// Public method to check if a chapter is downloaded
|
||||||
@ -179,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);
|
||||||
@ -197,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
|
||||||
@ -269,7 +284,16 @@ 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();
|
||||||
return Uri.parse(url).getLastPathSegment();
|
int number = page.getPageNumber() + 1;
|
||||||
|
// Try to preserve file extension
|
||||||
|
if (UrlUtil.isJpg(url)) {
|
||||||
|
return number + ".jpg";
|
||||||
|
} else if (UrlUtil.isPng(url)) {
|
||||||
|
return number + ".png";
|
||||||
|
} else if (UrlUtil.isGif(url)) {
|
||||||
|
return number + ".gif";
|
||||||
|
}
|
||||||
|
return Uri.parse(url).getLastPathSegment().replaceAll("[^\\sa-zA-Z0-9.-]", "_");
|
||||||
}
|
}
|
||||||
|
|
||||||
private boolean isImageDownloaded(File imagePath) {
|
private boolean isImageDownloaded(File imagePath) {
|
||||||
@ -304,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);
|
||||||
|
|
||||||
@ -313,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
|
||||||
@ -387,18 +410,19 @@ public class DownloadManager {
|
|||||||
if (queue.isEmpty())
|
if (queue.isEmpty())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
boolean hasPendingDownloads = false;
|
|
||||||
if (downloadsSubscription == null)
|
if (downloadsSubscription == null)
|
||||||
initializeSubscriptions();
|
initializeSubscriptions();
|
||||||
|
|
||||||
|
final List<Download> pending = new ArrayList<>();
|
||||||
for (Download download : queue) {
|
for (Download download : queue) {
|
||||||
if (download.getStatus() != Download.DOWNLOADED) {
|
if (download.getStatus() != Download.DOWNLOADED) {
|
||||||
if (download.getStatus() != Download.QUEUE) download.setStatus(Download.QUEUE);
|
if (download.getStatus() != Download.QUEUE) download.setStatus(Download.QUEUE);
|
||||||
if (!hasPendingDownloads) hasPendingDownloads = true;
|
pending.add(download);
|
||||||
downloadsQueueSubject.onNext(download);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return hasPendingDownloads;
|
downloadsQueueSubject.onNext(pending);
|
||||||
|
|
||||||
|
return !pending.isEmpty();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void stopDownloads() {
|
public void stopDownloads() {
|
||||||
|
@ -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
@ -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 {
|
||||||
@ -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,34 +1,47 @@
|
|||||||
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 okhttp3.Cache;
|
||||||
|
import okhttp3.CacheControl;
|
||||||
|
import okhttp3.FormBody;
|
||||||
|
import okhttp3.Headers;
|
||||||
|
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 CookieManager cookieManager;
|
private CookieManager cookieManager;
|
||||||
|
|
||||||
public final CacheControl NULL_CACHE_CONTROL = new CacheControl.Builder().noCache().build();
|
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();
|
||||||
|
|
||||||
|
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();
|
||||||
}
|
}
|
||||||
|
|
||||||
public Observable<Response> getResponse(final String url, final Headers headers, final CacheControl cacheControl) {
|
public Observable<Response> getResponse(final String url, final Headers headers, final CacheControl cacheControl) {
|
||||||
@ -86,19 +99,20 @@ public final class NetworkHelper {
|
|||||||
.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.
|
||||||
|
@ -88,6 +88,10 @@ 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> 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);
|
||||||
}
|
}
|
||||||
@ -116,6 +120,10 @@ public class PreferencesHelper {
|
|||||||
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0);
|
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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) {
|
||||||
return prefs.getString(SOURCE_ACCOUNT_USERNAME + source.getId(), "");
|
return prefs.getString(SOURCE_ACCOUNT_USERNAME + source.getId(), "");
|
||||||
}
|
}
|
||||||
@ -155,8 +163,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() {
|
||||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.source;
|
|||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -59,7 +60,9 @@ public class SourceManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<Source> getSources() {
|
public List<Source> getSources() {
|
||||||
return new ArrayList<>(sourcesMap.values());
|
List<Source> sources = new ArrayList<>(sourcesMap.values());
|
||||||
|
Collections.sort(sources, (s1, s2) -> s1.getName().compareTo(s2.getName()));
|
||||||
|
return sources;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
|
|
||||||
@ -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);
|
||||||
})
|
})
|
||||||
@ -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) {
|
||||||
|
@ -4,10 +4,6 @@ import android.content.Context;
|
|||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
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 +31,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 {
|
||||||
@ -320,7 +319,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 +353,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", "");
|
||||||
|
@ -228,12 +228,8 @@ public class Mangafox extends Source {
|
|||||||
|
|
||||||
Elements pageUrlElements = parsedDocument.select("select.m").first().select("option:not([value=0])");
|
Elements pageUrlElements = parsedDocument.select("select.m").first().select("option:not([value=0])");
|
||||||
String baseUrl = parsedDocument.select("div#series a").first().attr("href").replace("1.html", "");
|
String baseUrl = parsedDocument.select("div#series a").first().attr("href").replace("1.html", "");
|
||||||
int counter = 1;
|
|
||||||
for (Element pageUrlElement : pageUrlElements) {
|
for (Element pageUrlElement : pageUrlElements) {
|
||||||
if(counter < pageUrlElements.size()) {
|
pageUrlList.add(baseUrl + pageUrlElement.attr("value") + ".html");
|
||||||
pageUrlList.add(baseUrl + pageUrlElement.attr("value") + ".html");
|
|
||||||
}
|
|
||||||
counter++;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return pageUrlList;
|
return pageUrlList;
|
||||||
|
@ -63,7 +63,7 @@ public class UpdateMangaSyncService extends Service {
|
|||||||
subscriptions.add(Observable.defer(() -> sync.update(mangaSync))
|
subscriptions.add(Observable.defer(() -> sync.update(mangaSync))
|
||||||
.flatMap(response -> {
|
.flatMap(response -> {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
return db.insertMangaSync(mangaSync).createObservable();
|
return db.insertMangaSync(mangaSync).asRxObservable();
|
||||||
}
|
}
|
||||||
return Observable.error(new Exception("Could not update MAL"));
|
return Observable.error(new Exception("Could not update MAL"));
|
||||||
})
|
})
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -14,8 +14,8 @@ 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;
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadPresenter;
|
import eu.kanade.tachiyomi.ui.download.DownloadPresenter;
|
||||||
import eu.kanade.tachiyomi.ui.library.category.CategoryPresenter;
|
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryPresenter;
|
import eu.kanade.tachiyomi.ui.library.LibraryPresenter;
|
||||||
|
import eu.kanade.tachiyomi.ui.library.category.CategoryPresenter;
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
|
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaPresenter;
|
import eu.kanade.tachiyomi.ui.manga.MangaPresenter;
|
||||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter;
|
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersPresenter;
|
||||||
@ -23,6 +23,7 @@ import eu.kanade.tachiyomi.ui.manga.info.MangaInfoPresenter;
|
|||||||
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListPresenter;
|
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListPresenter;
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
|
||||||
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter;
|
import eu.kanade.tachiyomi.ui.reader.ReaderPresenter;
|
||||||
|
import eu.kanade.tachiyomi.ui.recent.RecentChaptersPresenter;
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsAccountsFragment;
|
import eu.kanade.tachiyomi.ui.setting.SettingsAccountsFragment;
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsActivity;
|
import eu.kanade.tachiyomi.ui.setting.SettingsActivity;
|
||||||
|
|
||||||
@ -44,6 +45,7 @@ public interface AppComponent {
|
|||||||
void inject(DownloadPresenter downloadPresenter);
|
void inject(DownloadPresenter downloadPresenter);
|
||||||
void inject(MyAnimeListPresenter myAnimeListPresenter);
|
void inject(MyAnimeListPresenter myAnimeListPresenter);
|
||||||
void inject(CategoryPresenter categoryPresenter);
|
void inject(CategoryPresenter categoryPresenter);
|
||||||
|
void inject(RecentChaptersPresenter recentChaptersPresenter);
|
||||||
|
|
||||||
void inject(ReaderActivity readerActivity);
|
void inject(ReaderActivity readerActivity);
|
||||||
void inject(MangaActivity mangaActivity);
|
void inject(MangaActivity mangaActivity);
|
||||||
@ -57,7 +59,6 @@ public interface AppComponent {
|
|||||||
void inject(LibraryUpdateService libraryUpdateService);
|
void inject(LibraryUpdateService libraryUpdateService);
|
||||||
void inject(DownloadService downloadService);
|
void inject(DownloadService downloadService);
|
||||||
void inject(UpdateMangaSyncService updateMangaSyncService);
|
void inject(UpdateMangaSyncService updateMangaSyncService);
|
||||||
|
|
||||||
Application application();
|
Application application();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -8,9 +8,9 @@ import dagger.Module;
|
|||||||
import dagger.Provides;
|
import dagger.Provides;
|
||||||
import eu.kanade.tachiyomi.data.cache.ChapterCache;
|
import eu.kanade.tachiyomi.data.cache.ChapterCache;
|
||||||
import eu.kanade.tachiyomi.data.cache.CoverCache;
|
import eu.kanade.tachiyomi.data.cache.CoverCache;
|
||||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
|
||||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||||
import eu.kanade.tachiyomi.data.download.DownloadManager;
|
import eu.kanade.tachiyomi.data.download.DownloadManager;
|
||||||
|
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
||||||
import eu.kanade.tachiyomi.data.network.NetworkHelper;
|
import eu.kanade.tachiyomi.data.network.NetworkHelper;
|
||||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||||
@ -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
|
||||||
|
@ -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();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -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,14 @@ 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.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.Spinner;
|
import android.widget.Spinner;
|
||||||
|
import android.widget.ViewSwitcher;
|
||||||
|
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
@ -28,11 +36,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;
|
||||||
@ -43,14 +53,17 @@ 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 = -1;
|
||||||
@ -59,6 +72,9 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
|
|||||||
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();
|
||||||
}
|
}
|
||||||
@ -75,13 +91,32 @@ 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 ?
|
||||||
@ -107,6 +142,8 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
|
|||||||
} else {
|
} else {
|
||||||
selectedIndex = position;
|
selectedIndex = position;
|
||||||
showProgressBar();
|
showProgressBar();
|
||||||
|
glm.scrollToPositionWithOffset(0, 0);
|
||||||
|
llm.scrollToPositionWithOffset(0, 0);
|
||||||
getPresenter().startRequesting(source);
|
getPresenter().startRequesting(source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -128,7 +165,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)) {
|
||||||
@ -149,6 +186,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
|
||||||
@ -165,6 +218,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();
|
||||||
}
|
}
|
||||||
@ -191,11 +247,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);
|
||||||
}
|
}
|
||||||
@ -209,9 +267,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);
|
||||||
}
|
}
|
||||||
@ -221,15 +280,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() {
|
||||||
@ -257,6 +329,20 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onListItemLongClick(int position) {
|
public void onListItemLongClick(int position) {
|
||||||
// Do nothing
|
final Manga selectedManga = adapter.getItem(position);
|
||||||
|
|
||||||
|
int textRes = selectedManga.favorite ? R.string.remove_from_library : R.string.add_to_library;
|
||||||
|
|
||||||
|
new MaterialDialog.Builder(getActivity())
|
||||||
|
.items(getString(textRes))
|
||||||
|
.itemsCallback((dialog, itemView, which, text) -> {
|
||||||
|
switch (which) {
|
||||||
|
case 0:
|
||||||
|
getPresenter().changeMangaFavorite(selectedManga);
|
||||||
|
adapter.notifyItemChanged(position);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.show();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,38 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.catalogue;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
public class CatalogueGridHolder extends CatalogueHolder {
|
||||||
|
|
||||||
|
@Bind(R.id.title) TextView title;
|
||||||
|
@Bind(R.id.thumbnail) ImageView thumbnail;
|
||||||
|
@Bind(R.id.favorite_sticker) ImageView 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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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;
|
||||||
|
|
||||||
@ -38,14 +37,16 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
|
|||||||
|
|
||||||
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) {
|
||||||
@ -57,31 +58,46 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
|
|||||||
|
|
||||||
mangaDetailSubject = PublishSubject.create();
|
mangaDetailSubject = PublishSubject.create();
|
||||||
|
|
||||||
|
pager = new RxPager<>();
|
||||||
|
|
||||||
restartableReplay(GET_MANGA_LIST,
|
restartableReplay(GET_MANGA_LIST,
|
||||||
() -> pager.pages().concatMap(page -> getMangasPageObservable(page + 1)),
|
pager::results,
|
||||||
(view, pair) -> view.onAddPage(pair.first, pair.second),
|
(view, pair) -> view.onAddPage(pair.first, pair.second));
|
||||||
(view, error) -> {
|
|
||||||
view.onAddPageError();
|
restartableFirst(GET_MANGA_PAGE,
|
||||||
Timber.e(error.getMessage());
|
() -> pager.request(page -> getMangasPageObservable(page + 1)),
|
||||||
});
|
(view, next) -> {},
|
||||||
|
(view, error) -> view.onAddPageError());
|
||||||
|
|
||||||
restartableLatestCache(GET_MANGA_DETAIL,
|
restartableLatestCache(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 onProcessRestart() {
|
||||||
source = sourceManager.get(sourceId);
|
source = sourceManager.get(sourceId);
|
||||||
stop(GET_MANGA_LIST);
|
stop(GET_MANGA_LIST);
|
||||||
stop(GET_MANGA_DETAIL);
|
stop(GET_MANGA_DETAIL);
|
||||||
|
stop(GET_MANGA_PAGE);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void setDisplayMode(boolean asList) {
|
||||||
|
this.isListMode = asList;
|
||||||
|
if (asList) {
|
||||||
|
stop(GET_MANGA_DETAIL);
|
||||||
|
} else {
|
||||||
|
start(GET_MANGA_DETAIL);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startRequesting(Source source) {
|
public void startRequesting(Source source) {
|
||||||
@ -92,20 +108,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 +139,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 +153,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();
|
||||||
@ -170,4 +188,17 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
|
|||||||
return sourceManager.getSources();
|
return sourceManager.getSources();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void changeMangaFavorite(Manga manga) {
|
||||||
|
manga.favorite = !manga.favorite;
|
||||||
|
db.insertManga(manga).executeAsBlocking();
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isListMode() {
|
||||||
|
return isListMode;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void swapDisplayMode() {
|
||||||
|
prefs.catalogueAsList().set(!isListMode);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,13 @@ package eu.kanade.tachiyomi.ui.decoration;
|
|||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.util.AttributeSet;
|
import android.graphics.Canvas;
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
|
||||||
import android.view.View;
|
|
||||||
import android.graphics.Rect;
|
import android.graphics.Rect;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.Canvas;
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.util.AttributeSet;
|
||||||
|
import android.view.View;
|
||||||
|
|
||||||
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
|
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
|
||||||
|
|
||||||
|
@ -90,6 +90,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) {
|
||||||
|
@ -32,8 +32,10 @@ public class LibraryAdapter extends SmartFragmentStatePagerAdapter {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void setCategories(List<Category> categories) {
|
public void setCategories(List<Category> categories) {
|
||||||
this.categories = categories;
|
if (this.categories != categories) {
|
||||||
notifyDataSetChanged();
|
this.categories = categories;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setSelectionMode(int mode) {
|
public void setSelectionMode(int mode) {
|
||||||
|
@ -52,7 +52,7 @@ public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga
|
|||||||
|
|
||||||
@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);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -67,7 +67,7 @@ public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga
|
|||||||
}
|
}
|
||||||
|
|
||||||
public int getCoverHeight() {
|
public int getCoverHeight() {
|
||||||
return fragment.recycler.getItemWidth() / 9 * 12;
|
return fragment.recycler.getItemWidth() / 3 * 4;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -34,6 +34,7 @@ public class LibraryCategoryFragment extends BaseFragment
|
|||||||
|
|
||||||
@State int position;
|
@State int position;
|
||||||
private LibraryCategoryAdapter adapter;
|
private LibraryCategoryAdapter adapter;
|
||||||
|
private List<Manga> mangas;
|
||||||
|
|
||||||
private Subscription numColumnsSubscription;
|
private Subscription numColumnsSubscription;
|
||||||
|
|
||||||
@ -112,10 +113,13 @@ public class LibraryCategoryFragment extends BaseFragment
|
|||||||
|
|
||||||
Category category = categories.get(position);
|
Category category = categories.get(position);
|
||||||
List<Manga> mangas = event.getMangasForCategory(category);
|
List<Manga> mangas = event.getMangasForCategory(category);
|
||||||
if (mangas == null) {
|
if (this.mangas != mangas) {
|
||||||
mangas = new ArrayList<>();
|
this.mangas = mangas;
|
||||||
|
if (mangas == null) {
|
||||||
|
mangas = new ArrayList<>();
|
||||||
|
}
|
||||||
|
setMangas(mangas);
|
||||||
}
|
}
|
||||||
setMangas(mangas);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void openManga(Manga manga) {
|
protected void openManga(Manga manga) {
|
||||||
|
@ -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,7 +44,7 @@ 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);
|
||||||
}
|
}
|
||||||
|
@ -58,7 +58,7 @@ public class LibraryPresenter extends BasePresenter<LibraryFragment> {
|
|||||||
@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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -70,12 +70,12 @@ public class LibraryPresenter extends BasePresenter<LibraryFragment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Observable<List<Category>> getCategoriesObservable() {
|
private Observable<List<Category>> getCategoriesObservable() {
|
||||||
return db.getCategories().createObservable()
|
return db.getCategories().asRxObservable()
|
||||||
.doOnNext(categories -> this.categories = categories);
|
.doOnNext(categories -> this.categories = categories);
|
||||||
}
|
}
|
||||||
|
|
||||||
private Observable<Map<Integer, List<Manga>>> getLibraryMangasObservable() {
|
private Observable<Map<Integer, List<Manga>>> getLibraryMangasObservable() {
|
||||||
return db.getLibraryMangas().createObservable()
|
return db.getLibraryMangas().asRxObservable()
|
||||||
.flatMap(mangas -> Observable.from(mangas)
|
.flatMap(mangas -> Observable.from(mangas)
|
||||||
.groupBy(manga -> manga.category)
|
.groupBy(manga -> manga.category)
|
||||||
.flatMap(group -> group.toList()
|
.flatMap(group -> group.toList()
|
||||||
|
@ -24,7 +24,7 @@ public class CategoryPresenter extends BasePresenter<CategoryActivity> {
|
|||||||
super.onCreate(savedState);
|
super.onCreate(savedState);
|
||||||
|
|
||||||
restartableLatestCache(GET_CATEGORIES,
|
restartableLatestCache(GET_CATEGORIES,
|
||||||
() -> db.getCategories().createObservable()
|
() -> db.getCategories().asRxObservable()
|
||||||
.doOnNext(categories -> this.categories = categories)
|
.doOnNext(categories -> this.categories = categories)
|
||||||
.observeOn(AndroidSchedulers.mainThread()),
|
.observeOn(AndroidSchedulers.mainThread()),
|
||||||
CategoryActivity::setCategories);
|
CategoryActivity::setCategories);
|
||||||
@ -46,11 +46,11 @@ public class CategoryPresenter extends BasePresenter<CategoryActivity> {
|
|||||||
}
|
}
|
||||||
cat.order = max;
|
cat.order = max;
|
||||||
|
|
||||||
db.insertCategory(cat).createObservable().subscribe();
|
db.insertCategory(cat).asRxObservable().subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void deleteCategories(List<Category> categories) {
|
public void deleteCategories(List<Category> categories) {
|
||||||
db.deleteCategories(categories).createObservable().subscribe();
|
db.deleteCategories(categories).asRxObservable().subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void reorderCategories(List<Category> categories) {
|
public void reorderCategories(List<Category> categories) {
|
||||||
@ -58,11 +58,11 @@ public class CategoryPresenter extends BasePresenter<CategoryActivity> {
|
|||||||
categories.get(i).order = i;
|
categories.get(i).order = i;
|
||||||
}
|
}
|
||||||
|
|
||||||
db.insertCategories(categories).createObservable().subscribe();
|
db.insertCategories(categories).asRxObservable().subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
public void renameCategory(Category category, String name) {
|
public void renameCategory(Category category, String name) {
|
||||||
category.name = name;
|
category.name = name;
|
||||||
db.insertCategory(category).createObservable().subscribe();
|
db.insertCategory(category).asRxObservable().subscribe();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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;
|
||||||
@ -19,6 +21,7 @@ import eu.kanade.tachiyomi.ui.base.activity.BaseActivity;
|
|||||||
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment;
|
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment;
|
||||||
import eu.kanade.tachiyomi.ui.download.DownloadFragment;
|
import eu.kanade.tachiyomi.ui.download.DownloadFragment;
|
||||||
import eu.kanade.tachiyomi.ui.library.LibraryFragment;
|
import eu.kanade.tachiyomi.ui.library.LibraryFragment;
|
||||||
|
import eu.kanade.tachiyomi.ui.recent.RecentChaptersFragment;
|
||||||
import eu.kanade.tachiyomi.ui.setting.SettingsActivity;
|
import eu.kanade.tachiyomi.ui.setting.SettingsActivity;
|
||||||
import icepick.State;
|
import icepick.State;
|
||||||
import nucleus.view.ViewWithPresenter;
|
import nucleus.view.ViewWithPresenter;
|
||||||
@ -28,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);
|
||||||
@ -52,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()
|
||||||
@ -70,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.recent_updates_title)
|
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(
|
||||||
@ -95,6 +104,7 @@ public class MainActivity extends BaseActivity {
|
|||||||
setFragment(LibraryFragment.newInstance());
|
setFragment(LibraryFragment.newInstance());
|
||||||
break;
|
break;
|
||||||
case R.id.nav_drawer_recent_updates:
|
case R.id.nav_drawer_recent_updates:
|
||||||
|
setFragment(RecentChaptersFragment.newInstance());
|
||||||
break;
|
break;
|
||||||
case R.id.nav_drawer_catalogues:
|
case R.id.nav_drawer_catalogues:
|
||||||
setFragment(CatalogueFragment.newInstance());
|
setFragment(CatalogueFragment.newInstance());
|
||||||
@ -177,4 +187,4 @@ public class MainActivity extends BaseActivity {
|
|||||||
return appBar;
|
return appBar;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
@ -1,12 +1,17 @@
|
|||||||
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;
|
||||||
|
|
||||||
@ -14,6 +19,7 @@ import javax.inject.Inject;
|
|||||||
|
|
||||||
import butterknife.Bind;
|
import butterknife.Bind;
|
||||||
import butterknife.ButterKnife;
|
import butterknife.ButterKnife;
|
||||||
|
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.database.models.Manga;
|
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||||
@ -30,21 +36,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 +65,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 +87,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 +123,7 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
|
|||||||
};
|
};
|
||||||
|
|
||||||
pageCount = 2;
|
pageCount = 2;
|
||||||
if (!is_online && mangaSyncManager.getMyAnimeList().isLogged())
|
if (!isOnline && mangaSyncManager.getMyAnimeList().isLogged())
|
||||||
pageCount++;
|
pageCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,44 +7,48 @@ import javax.inject.Inject;
|
|||||||
import de.greenrobot.event.EventBus;
|
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 eu.kanade.tachiyomi.util.EventBusHook;
|
||||||
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)
|
||||||
|
registerForStickyEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
@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).createObservable()
|
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) {
|
@EventBusHook
|
||||||
this.mangaId = mangaId;
|
public void onEventMainThread(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;
|
||||||
@ -24,6 +25,7 @@ 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 +63,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 +79,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 +100,40 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onResume() {
|
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||||
super.onResume();
|
inflater.inflate(R.menu.chapters, menu);
|
||||||
observeChapterDownloadProgress();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPause() {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
unsubscribeChapterDownloadProgress();
|
switch (item.getItemId()) {
|
||||||
super.onPause();
|
case R.id.action_display_mode:
|
||||||
|
showDisplayModeDialog();
|
||||||
|
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) {
|
||||||
@ -158,6 +179,29 @@ 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())
|
||||||
|
.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 observeChapterDownloadProgress() {
|
private void observeChapterDownloadProgress() {
|
||||||
downloadProgressSubscription = getPresenter().getDownloadProgressObs()
|
downloadProgressSubscription = getPresenter().getDownloadProgressObs()
|
||||||
.subscribe(this::onDownloadProgressChange,
|
.subscribe(this::onDownloadProgressChange,
|
||||||
@ -175,10 +219,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
|
||||||
@ -254,6 +298,11 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean onMarkPreviousAsRead(Chapter chapter) {
|
||||||
|
getPresenter().markPreviousChaptersAsRead(chapter);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
protected boolean onDownload(Observable<Chapter> chapters) {
|
protected boolean onDownload(Observable<Chapter> chapters) {
|
||||||
DownloadService.start(getActivity());
|
DownloadService.start(getActivity());
|
||||||
|
|
||||||
@ -337,13 +386,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,6 +7,8 @@ import android.widget.PopupMenu;
|
|||||||
import android.widget.RelativeLayout;
|
import android.widget.RelativeLayout;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.text.DecimalFormat;
|
||||||
|
import java.text.DecimalFormatSymbols;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
|
|
||||||
@ -14,40 +16,61 @@ 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;
|
||||||
|
|
||||||
public class ChaptersHolder extends FlexibleViewHolder {
|
public class ChaptersHolder extends FlexibleViewHolder {
|
||||||
|
|
||||||
private final ChaptersAdapter adapter;
|
|
||||||
private Chapter item;
|
|
||||||
|
|
||||||
@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;
|
||||||
|
|
||||||
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
|
private Context context;
|
||||||
|
|
||||||
|
private final ChaptersAdapter adapter;
|
||||||
|
private Chapter item;
|
||||||
|
|
||||||
|
private final int readColor;
|
||||||
|
private final int unreadColor;
|
||||||
|
|
||||||
|
private final DecimalFormat decimalFormat;
|
||||||
|
private SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
|
||||||
|
|
||||||
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);
|
||||||
|
|
||||||
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));
|
||||||
@ -99,6 +122,8 @@ public class ChaptersHolder extends FlexibleViewHolder {
|
|||||||
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_previous_as_read:
|
||||||
|
return adapter.getFragment().onMarkPreviousAsRead(item);
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
@ -18,6 +18,7 @@ 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 eu.kanade.tachiyomi.util.EventBusHook;
|
||||||
@ -38,16 +39,14 @@ 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) {
|
||||||
@ -59,6 +58,10 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
|
|||||||
|
|
||||||
chaptersSubject = PublishSubject.create();
|
chaptersSubject = PublishSubject.create();
|
||||||
|
|
||||||
|
restartableLatestCache(GET_MANGA,
|
||||||
|
() -> Observable.just(manga),
|
||||||
|
ChaptersFragment::onNextManga);
|
||||||
|
|
||||||
restartableLatestCache(DB_CHAPTERS,
|
restartableLatestCache(DB_CHAPTERS,
|
||||||
this::getDbChaptersObs,
|
this::getDbChaptersObs,
|
||||||
ChaptersFragment::onNextChapters);
|
ChaptersFragment::onNextChapters);
|
||||||
@ -70,13 +73,14 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
|
|||||||
|
|
||||||
restartableLatestCache(CHAPTER_STATUS_CHANGES,
|
restartableLatestCache(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();
|
registerForStickyEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void onProcessRestart() {
|
private void onProcessRestart() {
|
||||||
|
stop(GET_MANGA);
|
||||||
stop(DB_CHAPTERS);
|
stop(DB_CHAPTERS);
|
||||||
stop(FETCH_CHAPTERS);
|
stop(FETCH_CHAPTERS);
|
||||||
stop(CHAPTER_STATUS_CHANGES);
|
stop(CHAPTER_STATUS_CHANGES);
|
||||||
@ -90,14 +94,15 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@EventBusHook
|
@EventBusHook
|
||||||
public void onEventMainThread(Manga manga) {
|
public void onEventMainThread(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);
|
||||||
|
|
||||||
add(db.getChapters(manga).createObservable()
|
add(db.getChapters(manga).asRxObservable()
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.doOnNext(chapters -> {
|
.doOnNext(chapters -> {
|
||||||
this.chapters = chapters;
|
this.chapters = chapters;
|
||||||
@ -135,13 +140,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 +180,7 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
|
|||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (onlyDownloaded && download.getStatus() == Download.DOWNLOADED)
|
if (onlyDownloaded() && download.getStatus() == Download.DOWNLOADED)
|
||||||
refreshChapters();
|
refreshChapters();
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,11 +207,20 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
|
|||||||
return chapter;
|
return chapter;
|
||||||
})
|
})
|
||||||
.toList()
|
.toList()
|
||||||
.flatMap(chapters -> db.insertChapters(chapters).createObservable())
|
.flatMap(chapters -> db.insertChapters(chapters).asRxObservable())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe());
|
.subscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void markPreviousChaptersAsRead(Chapter selected) {
|
||||||
|
Observable.from(chapters)
|
||||||
|
.filter(c -> c.chapter_number > -1 && c.chapter_number < selected.chapter_number)
|
||||||
|
.doOnNext(c -> c.read = true)
|
||||||
|
.toList()
|
||||||
|
.flatMap(chapters -> db.insertChapters(chapters).asRxObservable())
|
||||||
|
.subscribe();
|
||||||
|
}
|
||||||
|
|
||||||
public void downloadChapters(Observable<Chapter> selectedChapters) {
|
public void downloadChapters(Observable<Chapter> selectedChapters) {
|
||||||
add(selectedChapters
|
add(selectedChapters
|
||||||
.toList()
|
.toList()
|
||||||
@ -222,7 +236,7 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
|
|||||||
}, error -> {
|
}, error -> {
|
||||||
Timber.e(error.getMessage());
|
Timber.e(error.getMessage());
|
||||||
}, () -> {
|
}, () -> {
|
||||||
if (onlyDownloaded)
|
if (onlyDownloaded())
|
||||||
refreshChapters();
|
refreshChapters();
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
@ -232,32 +246,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,6 +1,11 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.info;
|
package eu.kanade.tachiyomi.ui.manga.info;
|
||||||
|
|
||||||
|
import android.app.Activity;
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.net.Uri;
|
||||||
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;
|
||||||
@ -10,30 +15,38 @@ import android.widget.ImageView;
|
|||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.bumptech.glide.load.model.LazyHeaders;
|
import com.bumptech.glide.load.model.LazyHeaders;
|
||||||
|
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
|
||||||
|
import com.mikepenz.iconics.IconicsDrawable;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
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.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.io.IOHandler;
|
||||||
|
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 eu.kanade.tachiyomi.util.ToastUtil;
|
||||||
import nucleus.factory.RequiresPresenter;
|
import nucleus.factory.RequiresPresenter;
|
||||||
|
|
||||||
@RequiresPresenter(MangaInfoPresenter.class)
|
@RequiresPresenter(MangaInfoPresenter.class)
|
||||||
public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
|
public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
|
||||||
|
|
||||||
|
private static final int REQUEST_IMAGE_OPEN = 101;
|
||||||
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
|
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
|
||||||
|
|
||||||
@Bind(R.id.manga_artist) TextView artist;
|
@Bind(R.id.manga_artist) TextView artist;
|
||||||
@Bind(R.id.manga_author) TextView author;
|
@Bind(R.id.manga_author) TextView author;
|
||||||
@Bind(R.id.manga_chapters) TextView chapterCount;
|
@Bind(R.id.manga_chapters) TextView chapterCount;
|
||||||
@Bind(R.id.manga_genres) TextView genres;
|
@Bind(R.id.manga_genres) TextView genres;
|
||||||
@Bind(R.id.manga_status) TextView status;
|
@Bind(R.id.manga_status) TextView status;
|
||||||
|
@Bind(R.id.manga_source) TextView source;
|
||||||
@Bind(R.id.manga_summary) TextView description;
|
@Bind(R.id.manga_summary) TextView description;
|
||||||
@Bind(R.id.manga_cover) ImageView cover;
|
@Bind(R.id.manga_cover) ImageView cover;
|
||||||
|
|
||||||
@Bind(R.id.action_favorite) Button favoriteBtn;
|
@Bind(R.id.action_favorite) Button favoriteBtn;
|
||||||
|
@Bind(R.id.fab_edit) FloatingActionButton fabEdit;
|
||||||
|
|
||||||
public static MangaInfoFragment newInstance() {
|
public static MangaInfoFragment newInstance() {
|
||||||
return new MangaInfoFragment();
|
return new MangaInfoFragment();
|
||||||
@ -52,26 +65,47 @@ public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
|
|||||||
View view = inflater.inflate(R.layout.fragment_manga_info, container, false);
|
View view = inflater.inflate(R.layout.fragment_manga_info, container, false);
|
||||||
ButterKnife.bind(this, view);
|
ButterKnife.bind(this, view);
|
||||||
|
|
||||||
favoriteBtn.setOnClickListener(v -> {
|
//Create edit drawable with size 24dp (google guidelines)
|
||||||
getPresenter().toggleFavorite();
|
IconicsDrawable edit = new IconicsDrawable(this.getContext())
|
||||||
});
|
.icon(GoogleMaterial.Icon.gmd_edit)
|
||||||
|
.color(ContextCompat.getColor(this.getContext(), R.color.white))
|
||||||
|
.sizeDp(24);
|
||||||
|
|
||||||
|
// Update image of fab button
|
||||||
|
fabEdit.setImageDrawable(edit);
|
||||||
|
|
||||||
|
// Set listener.
|
||||||
|
fabEdit.setOnClickListener(v -> selectImage());
|
||||||
|
|
||||||
|
favoriteBtn.setOnClickListener(v -> getPresenter().toggleFavorite());
|
||||||
|
|
||||||
swipeRefresh.setOnRefreshListener(this::fetchMangaFromSource);
|
swipeRefresh.setOnRefreshListener(this::fetchMangaFromSource);
|
||||||
|
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void onNextManga(Manga manga) {
|
public void onNextManga(Manga manga, Source source) {
|
||||||
if (manga.initialized) {
|
if (manga.initialized) {
|
||||||
setMangaInfo(manga);
|
setMangaInfo(manga, source);
|
||||||
} else {
|
} else {
|
||||||
// Initialize manga
|
// Initialize manga
|
||||||
fetchMangaFromSource();
|
fetchMangaFromSource();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void setMangaInfo(Manga manga) {
|
/**
|
||||||
|
* Set the info of the manga
|
||||||
|
*
|
||||||
|
* @param manga manga object containing information about manga
|
||||||
|
* @param mangaSource the source of the manga
|
||||||
|
*/
|
||||||
|
private void setMangaInfo(Manga manga, Source mangaSource) {
|
||||||
artist.setText(manga.artist);
|
artist.setText(manga.artist);
|
||||||
author.setText(manga.author);
|
author.setText(manga.author);
|
||||||
|
|
||||||
|
if (mangaSource != null) {
|
||||||
|
source.setText(mangaSource.getName());
|
||||||
|
}
|
||||||
genres.setText(manga.genre);
|
genres.setText(manga.genre);
|
||||||
status.setText(manga.getStatus(getActivity()));
|
status.setText(manga.getStatus(getActivity()));
|
||||||
description.setText(manga.description);
|
description.setText(manga.description);
|
||||||
@ -82,7 +116,7 @@ public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
|
|||||||
LazyHeaders headers = getPresenter().source.getGlideHeaders();
|
LazyHeaders headers = getPresenter().source.getGlideHeaders();
|
||||||
if (manga.thumbnail_url != null && cover.getDrawable() == null) {
|
if (manga.thumbnail_url != null && cover.getDrawable() == null) {
|
||||||
if (manga.favorite) {
|
if (manga.favorite) {
|
||||||
coverCache.saveAndLoadFromCache(cover, manga.thumbnail_url, headers);
|
coverCache.saveOrLoadFromCache(cover, manga.thumbnail_url, headers);
|
||||||
} else {
|
} else {
|
||||||
coverCache.loadFromNetwork(cover, manga.thumbnail_url, headers);
|
coverCache.loadFromNetwork(cover, manga.thumbnail_url, headers);
|
||||||
}
|
}
|
||||||
@ -93,7 +127,7 @@ public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
|
|||||||
chapterCount.setText(String.valueOf(count));
|
chapterCount.setText(String.valueOf(count));
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setFavoriteText(boolean isFavorite) {
|
private void setFavoriteText(boolean isFavorite) {
|
||||||
favoriteBtn.setText(!isFavorite ? R.string.add_to_library : R.string.remove_from_library);
|
favoriteBtn.setText(!isFavorite ? R.string.add_to_library : R.string.remove_from_library);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -102,6 +136,44 @@ public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
|
|||||||
getPresenter().fetchMangaFromSource();
|
getPresenter().fetchMangaFromSource();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void selectImage() {
|
||||||
|
if (getPresenter().getManga().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 && requestCode == REQUEST_IMAGE_OPEN) {
|
||||||
|
// 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, cover))
|
||||||
|
ToastUtil.showShort(getContext(), R.string.notification_manga_update_failed);
|
||||||
|
|
||||||
|
} catch (IOException e) {
|
||||||
|
e.printStackTrace();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void onFetchMangaDone() {
|
public void onFetchMangaDone() {
|
||||||
setRefreshing(false);
|
setRefreshing(false);
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,10 @@
|
|||||||
package eu.kanade.tachiyomi.ui.manga.info;
|
package eu.kanade.tachiyomi.ui.manga.info;
|
||||||
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import android.widget.ImageView;
|
||||||
|
|
||||||
|
import java.io.File;
|
||||||
|
import java.io.IOException;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
@ -10,6 +14,7 @@ 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 eu.kanade.tachiyomi.util.EventBusHook;
|
||||||
import rx.Observable;
|
import rx.Observable;
|
||||||
@ -18,19 +23,42 @@ import rx.schedulers.Schedulers;
|
|||||||
|
|
||||||
public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
||||||
|
|
||||||
@Inject DatabaseHelper db;
|
/**
|
||||||
@Inject SourceManager sourceManager;
|
* The id of the restartable.
|
||||||
@Inject CoverCache coverCache;
|
*/
|
||||||
|
|
||||||
private Manga manga;
|
|
||||||
protected Source source;
|
|
||||||
private int count = -1;
|
|
||||||
|
|
||||||
private boolean isFetching;
|
|
||||||
|
|
||||||
private static final int GET_MANGA = 1;
|
private static final int GET_MANGA = 1;
|
||||||
|
/**
|
||||||
|
* The id of the restartable.
|
||||||
|
*/
|
||||||
private static final int GET_CHAPTER_COUNT = 2;
|
private static final int GET_CHAPTER_COUNT = 2;
|
||||||
|
/**
|
||||||
|
* The id of the restartable.
|
||||||
|
*/
|
||||||
private static final int FETCH_MANGA_INFO = 3;
|
private static final int FETCH_MANGA_INFO = 3;
|
||||||
|
/**
|
||||||
|
* Source information
|
||||||
|
*/
|
||||||
|
protected Source source;
|
||||||
|
/**
|
||||||
|
* Used to connect to database
|
||||||
|
*/
|
||||||
|
@Inject DatabaseHelper db;
|
||||||
|
/**
|
||||||
|
* Used to connect to different manga sources
|
||||||
|
*/
|
||||||
|
@Inject SourceManager sourceManager;
|
||||||
|
/**
|
||||||
|
* Used to connect to cache
|
||||||
|
*/
|
||||||
|
@Inject CoverCache coverCache;
|
||||||
|
/**
|
||||||
|
* Selected manga information
|
||||||
|
*/
|
||||||
|
private Manga manga;
|
||||||
|
/**
|
||||||
|
* Count of chapters
|
||||||
|
*/
|
||||||
|
private int count = -1;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onCreate(Bundle savedState) {
|
protected void onCreate(Bundle savedState) {
|
||||||
@ -40,22 +68,29 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
|||||||
onProcessRestart();
|
onProcessRestart();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Update manga cache
|
||||||
restartableLatestCache(GET_MANGA,
|
restartableLatestCache(GET_MANGA,
|
||||||
() -> Observable.just(manga),
|
() -> Observable.just(manga),
|
||||||
MangaInfoFragment::onNextManga);
|
(view, manga) -> view.onNextManga(manga, source));
|
||||||
|
|
||||||
|
// Update chapter count
|
||||||
restartableLatestCache(GET_CHAPTER_COUNT,
|
restartableLatestCache(GET_CHAPTER_COUNT,
|
||||||
() -> Observable.just(count),
|
() -> Observable.just(count),
|
||||||
MangaInfoFragment::setChapterCount);
|
MangaInfoFragment::setChapterCount);
|
||||||
|
|
||||||
|
// Fetch manga info from source
|
||||||
restartableFirst(FETCH_MANGA_INFO,
|
restartableFirst(FETCH_MANGA_INFO,
|
||||||
this::fetchMangaObs,
|
this::fetchMangaObs,
|
||||||
(view, manga) -> view.onFetchMangaDone(),
|
(view, manga) -> view.onFetchMangaDone(),
|
||||||
(view, error) -> view.onFetchMangaError());
|
(view, error) -> view.onFetchMangaError());
|
||||||
|
|
||||||
|
// onEventMainThread receives an event thanks to this line.
|
||||||
registerForStickyEvents();
|
registerForStickyEvents();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when savedState not null
|
||||||
|
*/
|
||||||
private void onProcessRestart() {
|
private void onProcessRestart() {
|
||||||
stop(GET_MANGA);
|
stop(GET_MANGA);
|
||||||
stop(GET_CHAPTER_COUNT);
|
stop(GET_CHAPTER_COUNT);
|
||||||
@ -69,10 +104,10 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@EventBusHook
|
@EventBusHook
|
||||||
public void onEventMainThread(Manga manga) {
|
public void onEventMainThread(MangaEvent event) {
|
||||||
this.manga = manga;
|
this.manga = event.manga;
|
||||||
source = sourceManager.get(manga.source);
|
source = sourceManager.get(manga.source);
|
||||||
start(GET_MANGA);
|
refreshManga();
|
||||||
}
|
}
|
||||||
|
|
||||||
@EventBusHook
|
@EventBusHook
|
||||||
@ -83,9 +118,11 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetch manga info 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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -97,23 +134,48 @@ 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());
|
||||||
}
|
}
|
||||||
|
|
||||||
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();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update cover with local file
|
||||||
|
*/
|
||||||
|
public boolean editCoverWithLocalFile(File file, ImageView imageView) throws IOException {
|
||||||
|
if (!manga.initialized)
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (manga.favorite) {
|
||||||
|
coverCache.copyToLocalCache(manga.thumbnail_url, file);
|
||||||
|
coverCache.saveOrLoadFromCache(imageView, manga.thumbnail_url, source.getGlideHeaders());
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Manga getManga() {
|
||||||
|
return manga;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used to refresh the view
|
||||||
|
protected void refreshManga() {
|
||||||
|
start(GET_MANGA);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,6 +4,8 @@ import android.content.Context;
|
|||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.R;
|
import eu.kanade.tachiyomi.R;
|
||||||
@ -12,6 +14,7 @@ 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.EventBusHook;
|
||||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||||
@ -35,6 +38,8 @@ 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);
|
||||||
@ -46,16 +51,14 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
|
|||||||
myAnimeList = syncManager.getMyAnimeList();
|
myAnimeList = syncManager.getMyAnimeList();
|
||||||
|
|
||||||
restartableLatestCache(GET_MANGA_SYNC,
|
restartableLatestCache(GET_MANGA_SYNC,
|
||||||
() -> db.getMangaSync(manga, myAnimeList).createObservable()
|
() -> 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,
|
restartableLatestCache(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) -> {
|
||||||
@ -75,7 +78,7 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
|
|||||||
}
|
}
|
||||||
return Observable.error(new Exception("Could not find manga"));
|
return Observable.error(new Exception("Could not find manga"));
|
||||||
})
|
})
|
||||||
.flatMap(myManga -> db.insertMangaSync(myManga).createObservable())
|
.flatMap(myManga -> db.insertMangaSync(myManga).asRxObservable())
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread()),
|
.observeOn(AndroidSchedulers.mainThread()),
|
||||||
(view, result) -> view.onRefreshDone(),
|
(view, result) -> view.onRefreshDone(),
|
||||||
@ -102,14 +105,30 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@EventBusHook
|
@EventBusHook
|
||||||
public void onEventMainThread(Manga manga) {
|
public void onEventMainThread(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).createObservable())
|
.flatMap(response -> db.insertMangaSync(mangaSync).asRxObservable())
|
||||||
.subscribeOn(Schedulers.io())
|
.subscribeOn(Schedulers.io())
|
||||||
.observeOn(AndroidSchedulers.mainThread())
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
.subscribe(next -> {},
|
.subscribe(next -> {},
|
||||||
@ -139,7 +158,7 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
|
|||||||
add(myAnimeList.bind(manga)
|
add(myAnimeList.bind(manga)
|
||||||
.flatMap(response -> {
|
.flatMap(response -> {
|
||||||
if (response.isSuccessful()) {
|
if (response.isSuccessful()) {
|
||||||
return db.insertMangaSync(manga).createObservable();
|
return db.insertMangaSync(manga).asRxObservable();
|
||||||
}
|
}
|
||||||
return Observable.error(new Exception("Could not bind manga"));
|
return Observable.error(new Exception("Could not bind manga"));
|
||||||
})
|
})
|
||||||
|
@ -7,6 +7,7 @@ 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.Menu;
|
import android.view.Menu;
|
||||||
@ -20,11 +21,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 +47,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 +71,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);
|
||||||
|
|
||||||
@ -164,9 +159,12 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void onChapterReady(List<Page> pages, Manga manga, Chapter chapter, int currentPage) {
|
public void onChapterReady(List<Page> pages, Manga manga, Chapter chapter, int currentPage) {
|
||||||
|
if (currentPage == -1) {
|
||||||
|
currentPage = pages.size() - 1;
|
||||||
|
}
|
||||||
|
|
||||||
if (viewer == null) {
|
if (viewer == null) {
|
||||||
viewer = createViewer(manga);
|
viewer = getOrCreateViewer(manga);
|
||||||
getSupportFragmentManager().beginTransaction().replace(R.id.reader, viewer).commit();
|
|
||||||
}
|
}
|
||||||
viewer.onPageListReady(pages, currentPage);
|
viewer.onPageListReady(pages, currentPage);
|
||||||
readerMenu.onChapterReady(pages.size(), manga, chapter, currentPage);
|
readerMenu.onChapterReady(pages.size(), manga, chapter, currentPage);
|
||||||
@ -176,19 +174,33 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
|
|||||||
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) {
|
||||||
@ -221,6 +233,8 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
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));
|
||||||
@ -286,7 +300,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 +358,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public PreferencesHelper getPreferences() {
|
public PreferencesHelper getPreferences() {
|
||||||
return preferences;
|
return getPresenter().prefs;
|
||||||
}
|
}
|
||||||
|
|
||||||
public BaseReader getViewer() {
|
public BaseReader getViewer() {
|
||||||
|
@ -7,6 +7,7 @@ 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;
|
||||||
@ -44,7 +45,7 @@ public class ReaderMenu {
|
|||||||
@Bind(R.id.lock_orientation) ImageButton lockOrientation;
|
@Bind(R.id.lock_orientation) ImageButton lockOrientation;
|
||||||
@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;
|
@Bind(R.id.reader_scale_type_selector) ImageButton scaleTypeSelector;
|
||||||
|
|
||||||
private MenuItem nextChapterBtn;
|
private MenuItem nextChapterBtn;
|
||||||
private MenuItem prevChapterBtn;
|
private MenuItem prevChapterBtn;
|
||||||
@ -56,7 +57,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 +70,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 +110,6 @@ public class ReaderMenu {
|
|||||||
bottomMenu.startAnimation(bottomMenuAnimation);
|
bottomMenu.startAnimation(bottomMenuAnimation);
|
||||||
|
|
||||||
settingsPopup.dismiss();
|
settingsPopup.dismiss();
|
||||||
brightnessPopup.dismiss();
|
|
||||||
|
|
||||||
showing = false;
|
showing = false;
|
||||||
}
|
}
|
||||||
@ -148,8 +147,8 @@ public class ReaderMenu {
|
|||||||
// Set initial values
|
// Set initial values
|
||||||
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.setToolbarTitle(manga.title);
|
||||||
activity.setToolbarSubtitle(chapter.chapter_number != -1 ?
|
activity.setToolbarSubtitle(chapter.chapter_number != -1 ?
|
||||||
@ -175,7 +174,7 @@ public class ReaderMenu {
|
|||||||
if (nextChapterBtn != null) nextChapterBtn.setVisible(nextChapter != null);
|
if (nextChapterBtn != null) nextChapterBtn.setVisible(nextChapter != null);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void initializeOptions() {
|
private void initializeMenu() {
|
||||||
// Orientation changes
|
// Orientation changes
|
||||||
add(preferences.lockOrientation().asObservable()
|
add(preferences.lockOrientation().asObservable()
|
||||||
.subscribe(locked -> {
|
.subscribe(locked -> {
|
||||||
@ -190,6 +189,18 @@ public class ReaderMenu {
|
|||||||
lockOrientation.setOnClickListener(v ->
|
lockOrientation.setOnClickListener(v ->
|
||||||
preferences.lockOrientation().set(!preferences.lockOrientation().get()));
|
preferences.lockOrientation().set(!preferences.lockOrientation().get()));
|
||||||
|
|
||||||
|
// Scale type selector
|
||||||
|
scaleTypeSelector.setOnClickListener(v -> {
|
||||||
|
showImmersiveDialog(new MaterialDialog.Builder(activity)
|
||||||
|
.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();
|
||||||
@ -215,17 +226,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 +247,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);
|
||||||
@ -282,7 +285,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 +297,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,37 +332,6 @@ 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
|
||||||
|
@ -172,8 +172,8 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
|||||||
|
|
||||||
private Observable<Pair<Chapter, Chapter>> getAdjacentChaptersObservable() {
|
private Observable<Pair<Chapter, Chapter>> getAdjacentChaptersObservable() {
|
||||||
return Observable.zip(
|
return Observable.zip(
|
||||||
db.getPreviousChapter(chapter).createObservable().take(1),
|
db.getPreviousChapter(chapter).asRxObservable().take(1),
|
||||||
db.getNextChapter(chapter).createObservable().take(1),
|
db.getNextChapter(chapter).asRxObservable().take(1),
|
||||||
Pair::create)
|
Pair::create)
|
||||||
.doOnNext(pair -> {
|
.doOnNext(pair -> {
|
||||||
previousChapter = pair.first;
|
previousChapter = pair.first;
|
||||||
@ -211,12 +211,16 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private Observable<List<MangaSync>> getMangaSyncObservable() {
|
private Observable<List<MangaSync>> getMangaSyncObservable() {
|
||||||
return db.getMangasSync(manga).createObservable()
|
return db.getMangasSync(manga).asRxObservable()
|
||||||
.doOnNext(mangaSync -> this.mangaSyncList = mangaSync);
|
.doOnNext(mangaSync -> this.mangaSyncList = mangaSync);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Loads the given chapter
|
|
||||||
private void loadChapter(Chapter chapter) {
|
private void loadChapter(Chapter chapter) {
|
||||||
|
loadChapter(chapter, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Loads the given chapter
|
||||||
|
private void loadChapter(Chapter chapter, int requestedPage) {
|
||||||
// Before loading the chapter, stop preloading (if it's working) and save current progress
|
// Before loading the chapter, stop preloading (if it's working) and save current progress
|
||||||
stopPreloadingNextChapter();
|
stopPreloadingNextChapter();
|
||||||
|
|
||||||
@ -227,7 +231,7 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
|||||||
if (!chapter.read && chapter.last_page_read != 0)
|
if (!chapter.read && chapter.last_page_read != 0)
|
||||||
currentPage = chapter.last_page_read;
|
currentPage = chapter.last_page_read;
|
||||||
else
|
else
|
||||||
currentPage = 0;
|
currentPage = 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;
|
||||||
@ -262,7 +266,7 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
|||||||
if (isChapterFinished()) {
|
if (isChapterFinished()) {
|
||||||
chapter.read = true;
|
chapter.read = true;
|
||||||
}
|
}
|
||||||
db.insertChapter(chapter).createObservable().subscribe();
|
db.insertChapter(chapter).asRxObservable().subscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check whether the chapter has been read
|
// Check whether the chapter has been read
|
||||||
@ -312,7 +316,7 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
|||||||
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 +325,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,7 +346,7 @@ 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 (nextChapterPageList != null)
|
||||||
source.savePageList(nextChapter.url, nextChapterPageList);
|
source.savePageList(nextChapter.url, nextChapterPageList);
|
||||||
|
@ -2,8 +2,10 @@ package eu.kanade.tachiyomi.ui.reader.viewer.base;
|
|||||||
|
|
||||||
import android.view.MotionEvent;
|
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.List;
|
import java.util.List;
|
||||||
@ -17,6 +19,7 @@ public abstract class BaseReader extends BaseFragment {
|
|||||||
protected int currentPage;
|
protected int currentPage;
|
||||||
protected List<Page> pages;
|
protected List<Page> pages;
|
||||||
protected Class<? extends ImageRegionDecoder> regionDecoderClass;
|
protected Class<? extends ImageRegionDecoder> regionDecoderClass;
|
||||||
|
protected Class<? extends ImageDecoder> bitmapDecoderClass;
|
||||||
|
|
||||||
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;
|
||||||
@ -50,14 +53,19 @@ public abstract class BaseReader extends BaseFragment {
|
|||||||
public abstract void onPageListReady(List<Page> pages, int currentPage);
|
public abstract void onPageListReady(List<Page> pages, int currentPage);
|
||||||
public abstract boolean onImageTouch(MotionEvent motionEvent);
|
public abstract boolean onImageTouch(MotionEvent motionEvent);
|
||||||
|
|
||||||
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 +74,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();
|
||||||
}
|
}
|
||||||
|
@ -19,9 +19,12 @@ public abstract class PagerReader extends BaseReader {
|
|||||||
protected PagerReaderAdapter adapter;
|
protected PagerReaderAdapter adapter;
|
||||||
protected Pager pager;
|
protected Pager pager;
|
||||||
|
|
||||||
|
private boolean isReady;
|
||||||
protected boolean transitions;
|
protected boolean transitions;
|
||||||
protected CompositeSubscription subscriptions;
|
protected CompositeSubscription subscriptions;
|
||||||
|
|
||||||
|
protected int scaleType = 1;
|
||||||
|
|
||||||
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));
|
||||||
@ -61,7 +64,14 @@ public abstract class PagerReader extends BaseReader {
|
|||||||
subscriptions = new CompositeSubscription();
|
subscriptions = new CompositeSubscription();
|
||||||
subscriptions.add(getReaderActivity().getPreferences().imageDecoder()
|
subscriptions.add(getReaderActivity().getPreferences().imageDecoder()
|
||||||
.asObservable()
|
.asObservable()
|
||||||
.doOnNext(this::setRegionDecoderClass)
|
.doOnNext(this::setDecoderClass)
|
||||||
|
.skip(1)
|
||||||
|
.distinctUntilChanged()
|
||||||
|
.subscribe(v -> adapter.notifyDataSetChanged()));
|
||||||
|
|
||||||
|
subscriptions.add(getReaderActivity().getPreferences().imageScaleType()
|
||||||
|
.asObservable()
|
||||||
|
.doOnNext(this::setImageScaleType)
|
||||||
.skip(1)
|
.skip(1)
|
||||||
.distinctUntilChanged()
|
.distinctUntilChanged()
|
||||||
.subscribe(v -> adapter.notifyDataSetChanged()));
|
.subscribe(v -> adapter.notifyDataSetChanged()));
|
||||||
@ -71,6 +81,7 @@ public abstract class PagerReader extends BaseReader {
|
|||||||
.subscribe(value -> transitions = value));
|
.subscribe(value -> transitions = value));
|
||||||
|
|
||||||
setPages();
|
setPages();
|
||||||
|
isReady = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -84,7 +95,7 @@ public abstract class PagerReader extends BaseReader {
|
|||||||
if (this.pages != pages) {
|
if (this.pages != pages) {
|
||||||
this.pages = pages;
|
this.pages = pages;
|
||||||
this.currentPage = currentPage;
|
this.currentPage = currentPage;
|
||||||
if (isResumed()) {
|
if (isReady) {
|
||||||
setPages();
|
setPages();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -110,6 +121,10 @@ public abstract class PagerReader extends BaseReader {
|
|||||||
return pager.onImageTouch(motionEvent);
|
return pager.onImageTouch(motionEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void setImageScaleType(int scaleType) {
|
||||||
|
this.scaleType = scaleType;
|
||||||
|
}
|
||||||
|
|
||||||
public abstract void onFirstPageOut();
|
public abstract void onFirstPageOut();
|
||||||
public abstract void onLastPageOut();
|
public abstract void onLastPageOut();
|
||||||
|
|
||||||
|
@ -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,19 @@ 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));
|
||||||
|
return f;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemPosition(Object object) {
|
||||||
|
return POSITION_NONE;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Page> getPages() {
|
public List<Page> getPages() {
|
||||||
@ -35,9 +48,4 @@ public class PagerReaderAdapter extends FragmentStatePagerAdapter {
|
|||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public int getItemPosition(Object object) {
|
|
||||||
return POSITION_NONE;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -25,7 +25,7 @@ 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.vertical.VerticalReader;
|
||||||
import rx.Observable;
|
import rx.Observable;
|
||||||
import rx.Subscription;
|
import rx.Subscription;
|
||||||
import rx.android.schedulers.AndroidSchedulers;
|
import rx.android.schedulers.AndroidSchedulers;
|
||||||
@ -41,13 +41,12 @@ public class PagerReaderFragment extends BaseFragment {
|
|||||||
@Bind(R.id.retry_button) Button retryButton;
|
@Bind(R.id.retry_button) Button retryButton;
|
||||||
|
|
||||||
private Page page;
|
private Page page;
|
||||||
|
private boolean isReady;
|
||||||
private Subscription progressSubscription;
|
private Subscription progressSubscription;
|
||||||
private Subscription statusSubscription;
|
private Subscription statusSubscription;
|
||||||
|
|
||||||
public static PagerReaderFragment newInstance(Page page) {
|
public static PagerReaderFragment newInstance() {
|
||||||
PagerReaderFragment fragment = new PagerReaderFragment();
|
return new PagerReaderFragment();
|
||||||
fragment.setPage(page);
|
|
||||||
return fragment;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -55,18 +54,20 @@ 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();
|
||||||
|
|
||||||
if (activity.getReaderTheme() == ReaderActivity.BLACK_THEME) {
|
if (activity.getReaderTheme() == ReaderActivity.BLACK_THEME) {
|
||||||
progressText.setTextColor(ContextCompat.getColor(getContext(), R.color.light_grey));
|
progressText.setTextColor(ContextCompat.getColor(getContext(), R.color.light_grey));
|
||||||
}
|
}
|
||||||
|
|
||||||
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.setRegionDecoderClass(parentFragment.getRegionDecoderClass());
|
imageView.setRegionDecoderClass(parentFragment.getRegionDecoderClass());
|
||||||
|
imageView.setBitmapDecoderClass(parentFragment.getBitmapDecoderClass());
|
||||||
|
imageView.setVerticalScrollingParent(parentFragment instanceof VerticalReader);
|
||||||
imageView.setOnTouchListener((v, motionEvent) -> parentFragment.onImageTouch(motionEvent));
|
imageView.setOnTouchListener((v, motionEvent) -> parentFragment.onImageTouch(motionEvent));
|
||||||
imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
||||||
@Override
|
@Override
|
||||||
@ -84,6 +85,7 @@ public class PagerReaderFragment extends BaseFragment {
|
|||||||
});
|
});
|
||||||
|
|
||||||
observeStatus();
|
observeStatus();
|
||||||
|
isReady = true;
|
||||||
return view;
|
return view;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -97,6 +99,9 @@ public class PagerReaderFragment extends BaseFragment {
|
|||||||
|
|
||||||
public void setPage(Page page) {
|
public void setPage(Page page) {
|
||||||
this.page = page;
|
this.page = page;
|
||||||
|
if (isReady) {
|
||||||
|
observeStatus();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void showImage() {
|
private void showImage() {
|
||||||
@ -185,8 +190,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
|
||||||
|
@ -8,8 +8,8 @@ import android.view.MotionEvent;
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener;
|
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener;
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener;
|
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener;
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerGestureListener;
|
|
||||||
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 {
|
||||||
@ -47,7 +47,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,7 +82,7 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -7,8 +7,8 @@ import android.view.MotionEvent;
|
|||||||
|
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener;
|
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener;
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener;
|
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener;
|
||||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerGestureListener;
|
|
||||||
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 {
|
||||||
@ -32,7 +32,7 @@ public class VerticalPager extends VerticalViewPagerImpl implements Pager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private void init(Context context) {
|
private void init(Context context) {
|
||||||
gestureDetector = new GestureDetector(context, new VerticalPagerGestureListener(this));
|
gestureDetector = new GestureDetector(context, new PagerGestureListener(this));
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@ -46,7 +46,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,7 +81,7 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -119,20 +119,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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -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> {
|
||||||
|
|
||||||
@ -47,7 +48,6 @@ public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> {
|
|||||||
|
|
||||||
public void setPages(List<Page> pages) {
|
public void setPages(List<Page> pages) {
|
||||||
this.pages = pages;
|
this.pages = pages;
|
||||||
notifyDataSetChanged();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void clear() {
|
public void clear() {
|
||||||
@ -65,4 +65,8 @@ public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> {
|
|||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public ReaderActivity getReaderActivity() {
|
||||||
|
return (ReaderActivity) fragment.getActivity();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@ -4,8 +4,6 @@ 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;
|
||||||
|
|
||||||
@ -24,7 +22,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,20 +30,32 @@ 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) -> {
|
||||||
@ -90,7 +99,6 @@ 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()));
|
imageView.setImage(ImageSource.uri(page.getImagePath()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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;
|
||||||
@ -30,12 +29,26 @@ public class WebtoonReader extends BaseReader {
|
|||||||
private Subscription decoderSubscription;
|
private Subscription decoderSubscription;
|
||||||
private GestureDetector gestureDetector;
|
private GestureDetector gestureDetector;
|
||||||
|
|
||||||
@Nullable
|
private boolean isReady;
|
||||||
|
private int scrollDistance;
|
||||||
|
|
||||||
|
private static final String SCROLL_STATE = "scroll_state";
|
||||||
|
|
||||||
|
private static final float LEFT_REGION = 0.33f;
|
||||||
|
private static final float RIGHT_REGION = 0.66f;
|
||||||
|
|
||||||
@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.onRestoreInstanceState(savedState.getParcelable(SCROLL_STATE));
|
||||||
|
}
|
||||||
|
|
||||||
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,28 +58,29 @@ 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) {
|
||||||
|
recycler.smoothScrollBy(0, -scrollDistance);
|
||||||
|
} else if (positionX > recycler.getWidth() * RIGHT_REGION) {
|
||||||
|
recycler.smoothScrollBy(0, scrollDistance);
|
||||||
|
} 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();
|
||||||
|
isReady = true;
|
||||||
|
|
||||||
return recycler;
|
return recycler;
|
||||||
}
|
}
|
||||||
@ -83,6 +97,12 @@ public class WebtoonReader extends BaseReader {
|
|||||||
super.onPause();
|
super.onPause();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onSaveInstanceState(Bundle outState) {
|
||||||
|
super.onSaveInstanceState(outState);
|
||||||
|
outState.putParcelable(SCROLL_STATE, layoutManager.onSaveInstanceState());
|
||||||
|
}
|
||||||
|
|
||||||
private void unsubscribeStatus() {
|
private void unsubscribeStatus() {
|
||||||
if (subscription != null && !subscription.isUnsubscribed())
|
if (subscription != null && !subscription.isUnsubscribed())
|
||||||
subscription.unsubscribe();
|
subscription.unsubscribe();
|
||||||
@ -97,7 +117,9 @@ public class WebtoonReader extends BaseReader {
|
|||||||
public void onPageListReady(List<Page> pages, int currentPage) {
|
public void onPageListReady(List<Page> pages, int currentPage) {
|
||||||
if (this.pages != pages) {
|
if (this.pages != pages) {
|
||||||
this.pages = pages;
|
this.pages = pages;
|
||||||
if (isResumed()) {
|
// Restoring current page is not supported. It's getting weird scrolling jumps
|
||||||
|
// this.currentPage = currentPage;
|
||||||
|
if (isReady) {
|
||||||
setPages();
|
setPages();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -107,9 +129,9 @@ public class WebtoonReader extends BaseReader {
|
|||||||
if (pages != null) {
|
if (pages != null) {
|
||||||
unsubscribeStatus();
|
unsubscribeStatus();
|
||||||
recycler.clearOnScrollListeners();
|
recycler.clearOnScrollListeners();
|
||||||
adapter.clear();
|
|
||||||
recycler.scrollTo(0, 0);
|
|
||||||
adapter.setPages(pages);
|
adapter.setPages(pages);
|
||||||
|
recycler.setAdapter(adapter);
|
||||||
|
updatePageNumber();
|
||||||
setScrollListener();
|
setScrollListener();
|
||||||
observeStatus(0);
|
observeStatus(0);
|
||||||
}
|
}
|
||||||
|
@ -0,0 +1,108 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.recent;
|
||||||
|
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.text.format.DateUtils;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
import android.widget.TextView;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import butterknife.Bind;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||||
|
import eu.kanade.tachiyomi.R;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
|
||||||
|
|
||||||
|
public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHolder, Object> {
|
||||||
|
|
||||||
|
private RecentChaptersFragment fragment;
|
||||||
|
|
||||||
|
private static final int VIEW_TYPE_CHAPTER = 0;
|
||||||
|
private static final int VIEW_TYPE_SECTION = 1;
|
||||||
|
|
||||||
|
public RecentChaptersAdapter(RecentChaptersFragment fragment) {
|
||||||
|
this.fragment = fragment;
|
||||||
|
setHasStableIds(true);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public long getItemId(int position) {
|
||||||
|
Object item = getItem(position);
|
||||||
|
if (item instanceof MangaChapter)
|
||||||
|
return ((MangaChapter) item).chapter.id;
|
||||||
|
else
|
||||||
|
return item.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setItems(List<Object> items) {
|
||||||
|
mItems = items;
|
||||||
|
notifyDataSetChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void updateDataSet(String param) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int getItemViewType(int position) {
|
||||||
|
return getItem(position) instanceof MangaChapter ? VIEW_TYPE_CHAPTER : VIEW_TYPE_SECTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||||
|
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||||
|
View v;
|
||||||
|
switch (viewType) {
|
||||||
|
case VIEW_TYPE_CHAPTER:
|
||||||
|
v = inflater.inflate(R.layout.item_recent_chapter, parent, false);
|
||||||
|
return new RecentChaptersHolder(v, this, fragment);
|
||||||
|
case VIEW_TYPE_SECTION:
|
||||||
|
v = inflater.inflate(R.layout.item_recent_chapter_section, parent, false);
|
||||||
|
return new SectionViewHolder(v);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||||
|
switch (holder.getItemViewType()) {
|
||||||
|
case VIEW_TYPE_CHAPTER:
|
||||||
|
final MangaChapter chapter = (MangaChapter) getItem(position);
|
||||||
|
((RecentChaptersHolder) holder).onSetValues(chapter);
|
||||||
|
break;
|
||||||
|
case VIEW_TYPE_SECTION:
|
||||||
|
final Date date = (Date) getItem(position);
|
||||||
|
((SectionViewHolder) holder).onSetValues(date);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
//When user scrolls this bind the correct selection status
|
||||||
|
holder.itemView.setActivated(isSelected(position));
|
||||||
|
}
|
||||||
|
|
||||||
|
public RecentChaptersFragment getFragment() {
|
||||||
|
return fragment;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class SectionViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
|
@Bind(R.id.section_text) TextView section;
|
||||||
|
|
||||||
|
private final long now = new Date().getTime();
|
||||||
|
|
||||||
|
public SectionViewHolder(View view) {
|
||||||
|
super(view);
|
||||||
|
ButterKnife.bind(this, view);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSetValues(Date date) {
|
||||||
|
CharSequence s = DateUtils.getRelativeTimeSpanString(
|
||||||
|
date.getTime(), now, DateUtils.DAY_IN_MILLIS);
|
||||||
|
section.setText(s);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,76 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.recent;
|
||||||
|
|
||||||
|
import android.content.Intent;
|
||||||
|
import android.os.Bundle;
|
||||||
|
import android.support.v4.content.ContextCompat;
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import butterknife.Bind;
|
||||||
|
import butterknife.ButterKnife;
|
||||||
|
import eu.kanade.tachiyomi.R;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
|
||||||
|
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||||
|
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
|
||||||
|
import eu.kanade.tachiyomi.ui.decoration.DividerItemDecoration;
|
||||||
|
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
|
||||||
|
import nucleus.factory.RequiresPresenter;
|
||||||
|
|
||||||
|
@RequiresPresenter(RecentChaptersPresenter.class)
|
||||||
|
public class RecentChaptersFragment extends BaseRxFragment<RecentChaptersPresenter> implements FlexibleViewHolder.OnListItemClickListener {
|
||||||
|
|
||||||
|
@Bind(R.id.chapter_list) RecyclerView recyclerView;
|
||||||
|
|
||||||
|
private RecentChaptersAdapter adapter;
|
||||||
|
|
||||||
|
public static RecentChaptersFragment newInstance() {
|
||||||
|
return new RecentChaptersFragment();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
|
||||||
|
// Inflate the layout for this fragment
|
||||||
|
View view = inflater.inflate(R.layout.fragment_recent_chapters, container, false);
|
||||||
|
ButterKnife.bind(this, view);
|
||||||
|
|
||||||
|
// Init RecyclerView and adapter
|
||||||
|
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
|
||||||
|
recyclerView.addItemDecoration(new DividerItemDecoration(ContextCompat.getDrawable(
|
||||||
|
getContext(), R.drawable.line_divider)));
|
||||||
|
recyclerView.setHasFixedSize(true);
|
||||||
|
adapter = new RecentChaptersAdapter(this);
|
||||||
|
recyclerView.setAdapter(adapter);
|
||||||
|
|
||||||
|
setToolbarTitle(R.string.label_recent_updates);
|
||||||
|
return view;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onNextMangaChapters(List<Object> chapters) {
|
||||||
|
adapter.setItems(chapters);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean onListItemClick(int position) {
|
||||||
|
Object item = adapter.getItem(position);
|
||||||
|
if (item instanceof MangaChapter) {
|
||||||
|
openChapter((MangaChapter) item);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onListItemLongClick(int position) {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
protected void openChapter(MangaChapter chapter) {
|
||||||
|
getPresenter().onOpenChapter(chapter);
|
||||||
|
Intent intent = ReaderActivity.newIntent(getActivity());
|
||||||
|
startActivity(intent);
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,42 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.recent;
|
||||||
|
|
||||||
|
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.MangaChapter;
|
||||||
|
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||||
|
|
||||||
|
public class RecentChaptersHolder extends FlexibleViewHolder {
|
||||||
|
|
||||||
|
@Bind(R.id.chapter_title) TextView chapterTitle;
|
||||||
|
@Bind(R.id.manga_title) TextView mangaTitle;
|
||||||
|
|
||||||
|
private final int readColor;
|
||||||
|
private final int unreadColor;
|
||||||
|
|
||||||
|
public RecentChaptersHolder(View view, RecentChaptersAdapter adapter, OnListItemClickListener onListItemClickListener) {
|
||||||
|
super(view, adapter, onListItemClickListener);
|
||||||
|
ButterKnife.bind(this, view);
|
||||||
|
|
||||||
|
readColor = ContextCompat.getColor(view.getContext(), R.color.hint_text);
|
||||||
|
unreadColor = ContextCompat.getColor(view.getContext(), R.color.primary_text);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onSetValues(MangaChapter item) {
|
||||||
|
chapterTitle.setText(item.chapter.name);
|
||||||
|
mangaTitle.setText(item.manga.title);
|
||||||
|
|
||||||
|
if (item.chapter.read) {
|
||||||
|
chapterTitle.setTextColor(readColor);
|
||||||
|
mangaTitle.setTextColor(readColor);
|
||||||
|
} else {
|
||||||
|
chapterTitle.setTextColor(unreadColor);
|
||||||
|
mangaTitle.setTextColor(unreadColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -0,0 +1,82 @@
|
|||||||
|
package eu.kanade.tachiyomi.ui.recent;
|
||||||
|
|
||||||
|
import android.os.Bundle;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Calendar;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.TreeMap;
|
||||||
|
|
||||||
|
import javax.inject.Inject;
|
||||||
|
|
||||||
|
import de.greenrobot.event.EventBus;
|
||||||
|
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
|
||||||
|
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||||
|
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||||
|
import eu.kanade.tachiyomi.event.ReaderEvent;
|
||||||
|
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||||
|
import rx.Observable;
|
||||||
|
import rx.android.schedulers.AndroidSchedulers;
|
||||||
|
|
||||||
|
public class RecentChaptersPresenter extends BasePresenter<RecentChaptersFragment> {
|
||||||
|
|
||||||
|
@Inject DatabaseHelper db;
|
||||||
|
@Inject SourceManager sourceManager;
|
||||||
|
|
||||||
|
private static final int GET_RECENT_CHAPTERS = 1;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
protected void onCreate(Bundle savedState) {
|
||||||
|
super.onCreate(savedState);
|
||||||
|
|
||||||
|
restartableLatestCache(GET_RECENT_CHAPTERS,
|
||||||
|
this::getRecentChaptersObservable,
|
||||||
|
RecentChaptersFragment::onNextMangaChapters);
|
||||||
|
|
||||||
|
if (savedState == null)
|
||||||
|
start(GET_RECENT_CHAPTERS);
|
||||||
|
}
|
||||||
|
|
||||||
|
private Observable<List<Object>> getRecentChaptersObservable() {
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
cal.setTime(new Date());
|
||||||
|
cal.add(Calendar.MONTH, -1);
|
||||||
|
|
||||||
|
return db.getRecentChapters(cal.getTime()).asRxObservable()
|
||||||
|
// group chapters by the date they were fetched on a ordered map
|
||||||
|
.flatMap(recents -> Observable.from(recents)
|
||||||
|
.toMultimap(
|
||||||
|
recent -> getMapKey(recent.chapter.date_fetch),
|
||||||
|
recent -> recent,
|
||||||
|
() -> new TreeMap<>((d1, d2) -> d2.compareTo(d1))))
|
||||||
|
// add every day and all its chapters to a single list
|
||||||
|
.map(recents -> {
|
||||||
|
List<Object> items = new ArrayList<>();
|
||||||
|
for (Map.Entry<Date, Collection<MangaChapter>> recent : recents.entrySet()) {
|
||||||
|
items.add(recent.getKey());
|
||||||
|
items.addAll(recent.getValue());
|
||||||
|
}
|
||||||
|
return items;
|
||||||
|
})
|
||||||
|
.observeOn(AndroidSchedulers.mainThread());
|
||||||
|
}
|
||||||
|
|
||||||
|
private Date getMapKey(long date) {
|
||||||
|
Calendar cal = Calendar.getInstance();
|
||||||
|
cal.setTime(new Date(date));
|
||||||
|
cal.set(Calendar.HOUR_OF_DAY, 0);
|
||||||
|
cal.set(Calendar.MINUTE, 0);
|
||||||
|
cal.set(Calendar.SECOND, 0);
|
||||||
|
cal.set(Calendar.MILLISECOND, 0);
|
||||||
|
return cal.getTime();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void onOpenChapter(MangaChapter item) {
|
||||||
|
Source source = sourceManager.get(item.manga.source);
|
||||||
|
EventBus.getDefault().postSticky(new ReaderEvent(source, item.manga, item.chapter));
|
||||||
|
}
|
||||||
|
}
|
@ -12,8 +12,8 @@ import java.util.List;
|
|||||||
import javax.inject.Inject;
|
import javax.inject.Inject;
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.App;
|
import eu.kanade.tachiyomi.App;
|
||||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
|
||||||
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.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.widget.preference.MangaSyncLoginDialog;
|
import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog;
|
||||||
|
@ -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);
|
||||||
|
@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -5,7 +5,7 @@ import android.support.v7.widget.RecyclerView;
|
|||||||
|
|
||||||
import rx.functions.Action0;
|
import rx.functions.Action0;
|
||||||
|
|
||||||
public class EndlessRecyclerScrollListener extends RecyclerView.OnScrollListener {
|
public class EndlessGridScrollListener extends RecyclerView.OnScrollListener {
|
||||||
|
|
||||||
private int previousTotal = 0; // The total number of items in the dataset after the last load
|
private int previousTotal = 0; // The total number of items in the dataset after the last load
|
||||||
private boolean loading = true; // True if we are still waiting for the last set of data to load.
|
private boolean loading = true; // True if we are still waiting for the last set of data to load.
|
||||||
@ -16,7 +16,7 @@ public class EndlessRecyclerScrollListener extends RecyclerView.OnScrollListener
|
|||||||
|
|
||||||
private Action0 requestNext;
|
private Action0 requestNext;
|
||||||
|
|
||||||
public EndlessRecyclerScrollListener(GridLayoutManager layoutManager, Action0 requestNext) {
|
public EndlessGridScrollListener(GridLayoutManager layoutManager, Action0 requestNext) {
|
||||||
this.layoutManager = layoutManager;
|
this.layoutManager = layoutManager;
|
||||||
this.requestNext = requestNext;
|
this.requestNext = requestNext;
|
||||||
}
|
}
|
@ -0,0 +1,49 @@
|
|||||||
|
package eu.kanade.tachiyomi.widget;
|
||||||
|
|
||||||
|
import android.support.v7.widget.LinearLayoutManager;
|
||||||
|
import android.support.v7.widget.RecyclerView;
|
||||||
|
|
||||||
|
import rx.functions.Action0;
|
||||||
|
|
||||||
|
public class EndlessListScrollListener extends RecyclerView.OnScrollListener {
|
||||||
|
|
||||||
|
private int previousTotal = 0; // The total number of items in the dataset after the last load
|
||||||
|
private boolean loading = true; // True if we are still waiting for the last set of data to load.
|
||||||
|
private int visibleThreshold = 5; // The minimum amount of items to have below your current scroll position before loading more.
|
||||||
|
int firstVisibleItem, visibleItemCount, totalItemCount;
|
||||||
|
|
||||||
|
private LinearLayoutManager layoutManager;
|
||||||
|
|
||||||
|
private Action0 requestNext;
|
||||||
|
|
||||||
|
public EndlessListScrollListener(LinearLayoutManager layoutManager, Action0 requestNext) {
|
||||||
|
this.layoutManager = layoutManager;
|
||||||
|
this.requestNext = requestNext;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void resetScroll() {
|
||||||
|
previousTotal = 0;
|
||||||
|
loading = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||||
|
super.onScrolled(recyclerView, dx, dy);
|
||||||
|
|
||||||
|
visibleItemCount = recyclerView.getChildCount();
|
||||||
|
totalItemCount = layoutManager.getItemCount();
|
||||||
|
firstVisibleItem = layoutManager.findFirstVisibleItemPosition();
|
||||||
|
|
||||||
|
if (loading && (totalItemCount > previousTotal)) {
|
||||||
|
loading = false;
|
||||||
|
previousTotal = totalItemCount;
|
||||||
|
}
|
||||||
|
if (!loading && (totalItemCount - visibleItemCount)
|
||||||
|
<= (firstVisibleItem + visibleThreshold)) {
|
||||||
|
// End has been reached
|
||||||
|
requestNext.call();
|
||||||
|
loading = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
BIN
app/src/main/res/drawable-hdpi/ic_view_list_white_24dp.png
Normal file
After Width: | Height: | Size: 125 B |
BIN
app/src/main/res/drawable-hdpi/ic_view_module_white_24dp.png
Normal file
After Width: | Height: | Size: 115 B |
BIN
app/src/main/res/drawable-hdpi/ic_zoom_out_map_white_24dp.png
Normal file
After Width: | Height: | Size: 326 B |
BIN
app/src/main/res/drawable-ldpi/ic_view_list_white_24dp.png
Normal file
After Width: | Height: | Size: 187 B |
BIN
app/src/main/res/drawable-ldpi/ic_view_module_white_24dp.png
Normal file
After Width: | Height: | Size: 278 B |
BIN
app/src/main/res/drawable-mdpi/ic_view_list_white_24dp.png
Normal file
After Width: | Height: | Size: 89 B |
BIN
app/src/main/res/drawable-mdpi/ic_view_module_white_24dp.png
Normal file
After Width: | Height: | Size: 87 B |
BIN
app/src/main/res/drawable-mdpi/ic_zoom_out_map_white_24dp.png
Normal file
After Width: | Height: | Size: 235 B |
BIN
app/src/main/res/drawable-xhdpi/ic_view_list_white_24dp.png
Normal file
After Width: | Height: | Size: 117 B |
BIN
app/src/main/res/drawable-xhdpi/ic_view_module_white_24dp.png
Normal file
After Width: | Height: | Size: 115 B |
BIN
app/src/main/res/drawable-xhdpi/ic_zoom_out_map_white_24dp.png
Normal file
After Width: | Height: | Size: 332 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_view_list_white_24dp.png
Normal file
After Width: | Height: | Size: 144 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_view_module_white_24dp.png
Normal file
After Width: | Height: | Size: 139 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_zoom_out_map_white_24dp.png
Normal file
After Width: | Height: | Size: 510 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_view_list_white_24dp.png
Normal file
After Width: | Height: | Size: 176 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_view_module_white_24dp.png
Normal file
After Width: | Height: | Size: 171 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_zoom_out_map_white_24dp.png
Normal file
After Width: | Height: | Size: 584 B |
12
app/src/main/res/drawable/gradient_shape.xml
Normal file
@ -0,0 +1,12 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<shape xmlns:android="http://schemas.android.com/apk/res/android"
|
||||||
|
android:shape="rectangle" >
|
||||||
|
|
||||||
|
<gradient
|
||||||
|
android:angle="90"
|
||||||
|
android:startColor="#aa000000"
|
||||||
|
android:centerColor="#00000000"
|
||||||
|
android:endColor="#00ffffff"/>
|
||||||
|
|
||||||
|
<corners android:radius="0dp" />
|
||||||
|
</shape>
|
@ -23,6 +23,7 @@
|
|||||||
android:layout_gravity="bottom|right"
|
android:layout_gravity="bottom|right"
|
||||||
android:layout_margin="@dimen/fab_margin"
|
android:layout_margin="@dimen/fab_margin"
|
||||||
android:src="@drawable/ic_action_add_18dp"
|
android:src="@drawable/ic_action_add_18dp"
|
||||||
|
app:backgroundTint="@color/colorPrimary"
|
||||||
app:layout_anchor="@id/categories_list"
|
app:layout_anchor="@id/categories_list"
|
||||||
app:layout_anchorGravity="bottom|right|end"
|
app:layout_anchorGravity="bottom|right|end"
|
||||||
app:layout_behavior="eu.kanade.tachiyomi.ui.base.fab.ScrollAwareFABBehavior"/>
|
app:layout_behavior="eu.kanade.tachiyomi.ui.base.fab.ScrollAwareFABBehavior"/>
|
||||||
|
@ -11,17 +11,28 @@
|
|||||||
android:id="@+id/progress"
|
android:id="@+id/progress"
|
||||||
style="?android:attr/progressBarStyleLarge"
|
style="?android:attr/progressBarStyleLarge"
|
||||||
android:layout_width="wrap_content"
|
android:layout_width="wrap_content"
|
||||||
android:layout_height="fill_parent"
|
android:layout_height="match_parent"
|
||||||
android:layout_gravity="center_vertical|center_horizontal"
|
android:layout_gravity="center_vertical|center_horizontal"
|
||||||
android:visibility="gone"/>
|
android:visibility="gone"/>
|
||||||
|
|
||||||
<eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
<ViewSwitcher
|
||||||
android:id="@+id/recycler"
|
android:layout_width="match_parent"
|
||||||
style="@style/AppTheme.GridView"
|
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:columnWidth="140dp"
|
android:id="@+id/switcher">
|
||||||
tools:listitem="@layout/item_catalogue" />
|
|
||||||
|
<eu.kanade.tachiyomi.widget.AutofitRecyclerView
|
||||||
|
android:id="@+id/catalogue_grid"
|
||||||
|
style="@style/AppTheme.GridView"
|
||||||
|
android:columnWidth="140dp"
|
||||||
|
tools:listitem="@layout/item_catalogue_grid" />
|
||||||
|
|
||||||
|
<android.support.v7.widget.RecyclerView
|
||||||
|
android:layout_width="match_parent"
|
||||||
|
android:layout_height="match_parent"
|
||||||
|
android:id="@+id/catalogue_list"/>
|
||||||
|
|
||||||
|
</ViewSwitcher>
|
||||||
|
|
||||||
<ProgressBar
|
<ProgressBar
|
||||||
android:id="@+id/progress_grid"
|
android:id="@+id/progress_grid"
|
||||||
|