Compare commits

...

88 Commits

Author SHA1 Message Date
c204548df5 Release 0.1.3 2016-02-03 12:56:12 +01:00
4d47f5a387 Show brigthness preference in reader settings. #106 2016-02-03 00:32:16 +01:00
7944bb8479 Fix #100 2016-02-01 20:53:06 +01:00
c4ae88a8ff Use Rapid only for regions. Fixes #97 (probably) 2016-01-31 22:41:45 +01:00
ad953b7bf6 Ask for external storage permissions on Marshmallow. Fixes #76 and #36 2016-01-31 22:38:54 +01:00
d799ae5d72 Webtoon reader "restores" position on rotation. Fixes #93 2016-01-31 18:48:13 +01:00
a3ec057384 Now tap on edges of webtoon reader scrolls by 3/4 screen 2016-01-31 02:40:05 +01:00
486f129e62 Merge pull request #86 from j2ghz/patch-1
CONTRIBUTING.md
2016-01-30 18:29:30 +01:00
e6c3864c71 Create CONTRIBUTING.md 2016-01-30 18:01:10 +01:00
7461f12066 Merge pull request #90 from cyalins/patch-1
Reworded and shortened some strings
2016-01-30 16:55:07 +01:00
e53b05feba Fix gestures on vertical readers 2016-01-30 16:40:41 +01:00
bcefc176c1 Use Rapid decoder also when no regions are required 2016-01-30 16:10:53 +01:00
d0580d0df1 Merge pull request #94 from NoodleMage/local_cover_small_fix
Small fix for local cover loading
2016-01-30 13:59:53 +01:00
28fd22dfe0 Manga initialized check. Now takes network cover image if something went
wrong
2016-01-30 13:46:18 +01:00
742924625d Update strings.xml 2016-01-30 11:57:27 +11:00
78a2eae719 Minor changes 2016-01-30 00:41:39 +01:00
38bb0b61d4 Merge pull request #91 from NoodleMage/change_cover
Can now manually set cover pictures. #79
2016-01-30 00:12:54 +01:00
8b52fea602 Can now manually set cover pictures. #79
Forgot to add IOHandler

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

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

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

@Bind is onelined

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

Settings now inline with rest of menu

@Bind is onelined

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

Code optimization. Added comments. Few comment mistake fixes

Few comments

Added classes because of renaming

Fixed refactor mistakes :(.

typo + removed todo empty class

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

Removed redundant null check

Update ChapterCache.java

Another o to 0 change. Damn this .o! :)
2016-01-27 17:37:36 +01:00
0c9bc97fe8 Initial support for custom images scaling (#40) 2016-01-27 01:48:40 +01:00
c6ecfb2f67 Trying to fix some crashes 2016-01-26 19:18:31 +01:00
8ca0814aff Add a way to search in MAL only from the user's list 2016-01-26 16:33:19 +01:00
eceb4c3682 Reorganize readme 2016-01-26 16:19:55 +01:00
e7ecd5a5c2 Add F-Droid badge 2016-01-26 15:52:14 +01:00
f7c20a5517 Update readme 2016-01-26 15:19:17 +01:00
6f409c0e3b Add an alternative way to display the chapter title (#54) 2016-01-25 19:57:13 +01:00
0a31c223e3 Don't lint release builds 2016-01-25 15:15:47 +01:00
0f42956f3f Update readme 2016-01-25 14:01:37 +01:00
ac2485d4a7 Release 0.1.2 2016-01-25 13:56:58 +01:00
7993ec5074 Make toolbar always visible 2016-01-25 13:54:23 +01:00
4521174138 Fix layout overlapping 2016-01-25 13:43:21 +01:00
27b95e9d73 Minor changes 2016-01-25 13:19:03 +01:00
a54425f47d Merge pull request #69 from icewind1991/info-show-source
Show manga source in info panel
2016-01-25 13:01:44 +01:00
4918e67fda Show manga source in info panel 2016-01-25 12:49:56 +01:00
b174adbab0 Use a gradient at the bottom of the cover. Remove external repositories from gradle 2016-01-24 23:41:21 +01:00
59cc87c583 Fix #58 and #59 2016-01-24 13:57:20 +01:00
0e87dc995a Add backpressure buffer for downloads 2016-01-24 13:23:29 +01:00
fad7b75b96 Place reload button above the image 2016-01-24 12:52:41 +01:00
c99c90fc4c Merge pull request #57 from icewind1991/chapter-list-ellipsize
elipsize chapter list in the middle
2016-01-24 12:42:49 +01:00
594219848d Fix number of simultaneous downloads ignored (again) 2016-01-24 12:37:41 +01:00
fa301bfbd2 elipsize chapter list in the middle 2016-01-24 12:15:43 +01:00
50306f6ea3 Merge pull request #53 from icewind1991/sort-order
save per-manga sort order
2016-01-24 00:10:02 +01:00
9b90ad0a3b save per-manga sort order 2016-01-24 00:01:24 +01:00
5c854984e4 Fix #52 2016-01-23 21:58:36 +01:00
6c844cfd9c Merge pull request #51 from icewind1991/last-page
Load the last page when switching to the previous chapter (Fix #48)
2016-01-23 19:13:21 +01:00
9e666dcdb3 Load the last page when switching to the previous chapter 2016-01-23 17:10:56 +01:00
e81f98a975 Fix an UI refresh issue 2016-01-23 14:17:01 +01:00
11dc0d7e9e Change filename for downloaded chapters, using the last path from the url is not reliable. This will break compatibility with previously downloaded chapters, they have to be deleted and downloaded again.
Disable download progress in the chapters view, it will avoid some crashes.
2016-01-23 13:58:53 +01:00
07ed2e2ebb Hold the same manga instance (allowing to refresh manga state from the catalogue). Other minor changes. 2016-01-22 20:22:16 +01:00
e1aa460106 Allow to display manga from catalogue as a simple list (#35) 2016-01-22 17:37:23 +01:00
75a77566cf Trying switches instead of checkboxes 2016-01-21 16:55:18 +01:00
dd0a2d842a Improve recent chapters layout 2016-01-21 16:38:25 +01:00
fa71e906c9 Change recent chapters query, now it shows last month updates. Download manager now uses a thread pool. 2016-01-21 02:26:34 +01:00
e6a17e25a9 Tint navigation bar on Lollipop and higher 2016-01-20 22:06:22 +01:00
d88513de56 Reenable recent updates tab 2016-01-20 19:43:44 +01:00
ad97d03f1d Change toolbar color (Fix #43). Allow to also remove from library (Fix #44). Rewrite RxPager. 2016-01-20 19:21:17 +01:00
7fc23d526b Update readme 2016-01-20 14:46:05 +01:00
0210fd8828 Fix a big issue with the download threads. Release 0.1.1 2016-01-20 14:38:45 +01:00
0332d8dd79 Fix #39 2016-01-19 21:07:32 +01:00
111ec5541f Fix an error with empty pages from downloaded chapters (images not found) 2016-01-19 19:30:55 +01:00
4bf15a5a2c Allow to mark all previous chapters as read 2016-01-19 16:35:36 +01:00
416fd128ba Upgrade StorIO to 1.8.0 2016-01-19 15:49:13 +01:00
dda0c50a1c Show only recent chapters of the library 2016-01-18 20:18:46 +01:00
f0a3c9c2dc Don't reset library adapter if it's not needed 2016-01-18 19:41:11 +01:00
8520a47286 Sort sources alphabetically. Fix #31 2016-01-18 19:28:13 +01:00
522e900b5a Initial support for recent updates. #20 2016-01-18 18:04:07 +01:00
b9bb41164f Merge pull request #33 from wopian/patch-2
Update readme
2016-01-18 00:23:57 +01:00
2b2fa0de2f Merge pull request #32 from wopian/patch-1
Fix some grammatical issues
2016-01-18 00:23:43 +01:00
e747083b06 Update README.md 2016-01-17 21:11:53 +00:00
e08dd95435 Fix some grammatical issues 2016-01-17 20:59:21 +00:00
173e86320b Allow to add a manga to the library with a long click 2016-01-17 16:11:30 +01:00
b2e579173b Update readme 2016-01-17 15:29:31 +01:00
79229d9c6a Prepare for dev releases 2016-01-17 15:03:04 +01:00
d25cbe9005 Update readme 2016-01-16 21:48:46 +01:00
133 changed files with 2525 additions and 856 deletions

30
CONTRIBUTING.md Normal file
View File

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

View File

@ -1,8 +1,14 @@
Tachiyomi is a manga reader for Android free and open source.
[![stable release](https://img.shields.io/badge/release-v0.1.2-blue.svg)](https://github.com/inorichi/tachiyomi/releases)
[![fdroid release](https://img.shields.io/badge/release-F--Droid-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi)
[![latest debug build](https://img.shields.io/badge/debug-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk)
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.
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
* Configurable reader with multiple viewers and settings

View File

@ -39,8 +39,8 @@ android {
minSdkVersion 16
targetSdkVersion 23
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 1
versionName "0.1.0"
versionCode 4
versionName "0.1.3"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -53,6 +53,9 @@ android {
}
buildTypes {
debug {
applicationIdSuffix ".debug"
}
release {
minifyEnabled true
shrinkResources true
@ -70,6 +73,7 @@ android {
lintOptions {
abortOnError false
checkReleaseBuilds false
}
}
@ -77,8 +81,9 @@ android {
dependencies {
final SUPPORT_LIBRARY_VERSION = '23.1.1'
final DAGGER_VERSION = '2.0.2'
final OKHTTP_VERSION = '3.0.1'
final MOCKITO_VERSION = '1.10.19'
final STORIO_VERSION = '1.7.0'
final STORIO_VERSION = '1.8.0'
final ICEPICK_VERSION = '3.1.0'
compile fileTree(dir: 'libs', include: ['*.jar'])
@ -90,8 +95,9 @@ dependencies {
compile "com.android.support:design:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
compile 'com.squareup.okhttp:okhttp-urlconnection:2.7.2'
compile 'com.squareup.okhttp:okhttp:2.7.2'
compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION"
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.google.code.gson:gson:2.5'
compile 'com.jakewharton:disklrucache:2.0.2'
@ -111,9 +117,9 @@ dependencies {
compile "frankiesardo:icepick:$ICEPICK_VERSION"
provided "frankiesardo:icepick-processor:$ICEPICK_VERSION"
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.amulyakhare:com.amulyakhare.textdrawable:1.0.1'
compile 'com.github.amulyakhare:TextDrawable:558677e'
compile 'com.github.pwittchen:reactivenetwork:0.1.5'
compile "com.google.dagger:dagger:$DAGGER_VERSION"
@ -124,10 +130,15 @@ dependencies {
compile('com.mikepenz:materialdrawer:4.6.4@aar') {
transitive = true
}
//Google material icons SVG.
compile 'com.mikepenz:google-material-typeface:2.1.0.1.original@aar'
compile('com.github.afollestad.material-dialogs:core:0.8.5.3@aar') {
transitive = true
}
testCompile 'junit:junit:4.12'
testCompile 'org.assertj:assertj-core:2.3.0'
testCompile "org.mockito:mockito-core:$MOCKITO_VERSION"

View File

@ -8,9 +8,9 @@
# OkHttp
-keepattributes Signature
-keepattributes *Annotation*
-keep class com.squareup.okhttp.** { *; }
-keep interface com.squareup.okhttp.** { *; }
-dontwarn com.squareup.okhttp.**
-keep class okhttp3.** { *; }
-keep interface okhttp3.** { *; }
-dontwarn okhttp3.**
-dontwarn okio.**
# Okio

View File

@ -13,7 +13,7 @@ import eu.kanade.tachiyomi.injection.module.AppModule;
import timber.log.Timber;
@ReportsCrashes(
formUri = "http://mangafeed.kanade.eu/crash_report",
formUri = "http://tachiyomi.kanade.eu/crash_report",
reportType = org.acra.sender.HttpSender.Type.JSON,
httpMethod = org.acra.sender.HttpSender.Method.PUT,
excludeMatchingSharedPreferencesKeys={".*username.*",".*password.*"}

View File

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

View File

@ -1,6 +1,7 @@
package eu.kanade.tachiyomi.data.cache;
import android.content.Context;
import android.support.annotation.Nullable;
import android.text.TextUtils;
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.request.animation.GlideAnimation;
import com.bumptech.glide.request.target.SimpleTarget;
import com.bumptech.glide.signature.StringSignature;
import java.io.File;
import java.io.FileInputStream;
@ -20,33 +22,76 @@ import java.io.OutputStream;
import eu.kanade.tachiyomi.util.DiskUtils;
/**
* Class used to create cover cache
* It is used to store the covers of the library.
* Makes use of Glide (which can avoid repeating requests) to download covers.
* Names of files are created with the md5 of the thumbnail URL
*/
public class CoverCache {
/**
* Name of cache directory.
*/
private static final String PARAMETER_CACHE_DIRECTORY = "cover_disk_cache";
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) {
this.context = context;
// Get cache directory from parameter.
cacheDir = new File(context.getCacheDir(), PARAMETER_CACHE_DIRECTORY);
// Create cache directory.
createCacheDir();
}
/**
* Create cache directory if it doesn't exist
*
* @return true if cache dir is created otherwise false.
*/
private boolean createCacheDir() {
return !cacheDir.exists() && cacheDir.mkdirs();
}
/**
* Download the cover with Glide and save the file in this cache.
*
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
*/
public void save(String thumbnailUrl, LazyHeaders headers) {
save(thumbnailUrl, headers, null);
}
// Download the cover with Glide (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
public void save(String thumbnailUrl, LazyHeaders headers, ImageView imageView) {
/**
* Download the cover with Glide and save the file.
*
* @param thumbnailUrl url of thumbnail.
* @param headers headers included in Glide request.
* @param imageView imageView where picture should be displayed.
*/
private void save(String thumbnailUrl, LazyHeaders headers, @Nullable ImageView imageView) {
// Check if url is empty.
if (TextUtils.isEmpty(thumbnailUrl))
return;
// Download the cover with Glide and save the file.
GlideUrl url = new GlideUrl(thumbnailUrl, headers);
Glide.with(context)
.load(url)
@ -54,29 +99,44 @@ public class CoverCache {
@Override
public void onResourceReady(File resource, GlideAnimation<? super File> anim) {
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) {
loadFromCache(imageView, resource);
}
} 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();
// Get destination file.
File dest = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
// Delete the current file if it exists.
if (dest.exists())
dest.delete();
// Write thumbnail image to file.
InputStream in = new FileInputStream(source);
try {
OutputStream out = new FileOutputStream(dest);
try {
// Transfer bytes from in to out
// Transfer bytes from in to out.
byte[] buf = new byte[1024];
int len;
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));
}
// 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))
return false;
// Remove file.
File file = new File(cacheDir, DiskUtils.hashKeyForDisk(thumbnailUrl));
return file.exists() && file.delete();
}
// Save and load the image from cache
public void saveAndLoadFromCache(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
File localCover = get(thumbnailUrl);
/**
* Save or load the image from cache
*
* @param imageView imageView where picture should be displayed.
* @param thumbnailUrl the thumbnail url.
* @param headers headers included in Glide request.
*/
public void saveOrLoadFromCache(ImageView imageView, String thumbnailUrl, LazyHeaders headers) {
// If file exist load it otherwise save it.
File localCover = getCoverFromCache(thumbnailUrl);
if (localCover.exists()) {
loadFromCache(imageView, localCover);
} else {
@ -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) {
File localCover = get(thumbnailUrl);
if (localCover.exists()) {
loadFromCache(imageView, localCover);
} else {
loadFromNetwork(imageView, thumbnailUrl, headers);
}
}
// Helper method to load the cover from the cache directory into the specified image view
// The file must exist
/**
* Helper method to load the cover from the cache directory into the specified image view.
* Glide stores the resized image in its cache to improve performance.
*
* @param imageView imageView where picture should be displayed.
* @param file file to load. Must exist!.
*/
private void loadFromCache(ImageView imageView, File file) {
Glide.with(context)
.load(file)
.diskCacheStrategy(DiskCacheStrategy.NONE)
.diskCacheStrategy(DiskCacheStrategy.RESULT)
.centerCrop()
.signature(new StringSignature(String.valueOf(file.lastModified())))
.into(imageView);
}
// Helper method to load the cover from network into the specified image view.
// 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) {
// Check if url is empty.
if (TextUtils.isEmpty(thumbnailUrl))
return;
GlideUrl url = new GlideUrl(thumbnailUrl, headers);
Glide.with(context)
.load(url)

View File

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

View File

@ -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.MangaCategory;
import eu.kanade.tachiyomi.data.database.models.MangaCategorySQLiteTypeMapping;
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
import eu.kanade.tachiyomi.data.database.models.MangaSQLiteTypeMapping;
import eu.kanade.tachiyomi.data.database.models.MangaSync;
import eu.kanade.tachiyomi.data.database.models.MangaSyncSQLiteTypeMapping;
import eu.kanade.tachiyomi.data.database.resolvers.LibraryMangaGetResolver;
import eu.kanade.tachiyomi.data.database.resolvers.MangaChapterGetResolver;
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
@ -160,23 +162,14 @@ public class DatabaseHelper {
.prepare();
}
public PreparedGetListOfObjects<Chapter> getChapters(long manga_id, boolean sortAToZ, boolean onlyUnread) {
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);
}
public PreparedGetListOfObjects<MangaChapter> getRecentChapters(Date date) {
return db.get()
.listOfObjects(Chapter.class)
.withQuery(query.build())
.listOfObjects(MangaChapter.class)
.withQuery(RawQuery.builder()
.query(MangaChapterGetResolver.getRecentChaptersQuery(date))
.observesTables(ChapterTable.TABLE)
.build())
.withGetResolver(MangaChapterGetResolver.INSTANCE)
.prepare();
}

View File

@ -6,9 +6,9 @@ import android.database.sqlite.SQLiteOpenHelper;
import android.support.annotation.NonNull;
import eu.kanade.tachiyomi.data.database.tables.CategoryTable;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
import eu.kanade.tachiyomi.data.database.tables.MangaCategoryTable;
import eu.kanade.tachiyomi.data.database.tables.MangaSyncTable;
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
import eu.kanade.tachiyomi.data.database.tables.MangaTable;
public class DbOpenHelper extends SQLiteOpenHelper {

View File

@ -68,6 +68,25 @@ public class Manga implements Serializable {
public static final int COMPLETED = 2;
public static final int LICENSED = 3;
public static final int SORT_AZ = 0x00000000;
public static final int SORT_ZA = 0x00000001;
public static final int SORT_MASK = 0x00000001;
public static final int SHOW_UNREAD = 0x00000002;
public static final int SHOW_READ = 0x00000004;
public static final int READ_MASK = 0x00000006;
public static final int SHOW_DOWNLOADED = 0x00000008;
public static final int SHOW_NOT_DOWNLOADED = 0x00000010;
public static final int DOWNLOADED_MASK = 0x00000018;
// Generic filter that does not filter anything
public static final int SHOW_ALL = 0x00000000;
public static final int DISPLAY_NAME = 0x00000000;
public static final int DISPLAY_NUMBER = 0x00100000;
public static final int DISPLAY_MASK = 0x00100000;
public Manga() {}
public static Manga create(String pathUrl) {
@ -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
public boolean equals(Object o) {
if (this == o) return true;

View File

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

View File

@ -5,8 +5,8 @@ import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
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.mangasync.base.MangaSyncService;
@StorIOSQLiteType(table = MangaSyncTable.TABLE)
public class MangaSync implements Serializable {

View File

@ -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);
}
}

View File

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

View File

@ -8,7 +8,6 @@ import com.google.gson.reflect.TypeToken;
import com.google.gson.stream.JsonReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.FileReader;
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.util.DiskUtils;
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator;
import eu.kanade.tachiyomi.util.UrlUtil;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
@ -42,10 +42,13 @@ public class DownloadManager {
private PreferencesHelper preferences;
private Gson gson;
private PublishSubject<Download> downloadsQueueSubject;
private PublishSubject<List<Download>> downloadsQueueSubject;
private BehaviorSubject<Boolean> runningSubject;
private Subscription downloadsSubscription;
private BehaviorSubject<Integer> threadsSubject;
private Subscription threadsSubscription;
private DownloadQueue queue;
private volatile boolean isRunning;
@ -61,14 +64,19 @@ public class DownloadManager {
downloadsQueueSubject = PublishSubject.create();
runningSubject = BehaviorSubject.create();
threadsSubject = BehaviorSubject.create();
}
private void initializeSubscriptions() {
if (downloadsSubscription != null && !downloadsSubscription.isUnsubscribed())
downloadsSubscription.unsubscribe();
threadsSubscription = preferences.downloadThreads().asObservable()
.subscribe(threadsSubject::onNext);
downloadsSubscription = downloadsQueueSubject
.flatMap(this::downloadChapter, preferences.downloadThreads())
.flatMap(Observable::from)
.lift(new DynamicConcurrentMergeOperator<>(this::downloadChapter, threadsSubject))
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread())
.map(download -> areAllDownloadsFinished())
@ -94,6 +102,11 @@ public class DownloadManager {
downloadsSubscription.unsubscribe();
downloadsSubscription = null;
}
if (threadsSubscription != null && !threadsSubscription.isUnsubscribed()) {
threadsSubscription.unsubscribe();
}
}
// Create a download object for every chapter in the event and add them to the downloads queue
@ -103,6 +116,7 @@ public class DownloadManager {
// Used to avoid downloading chapters with the same name
final List<String> addedChapters = new ArrayList<>();
final List<Download> pending = new ArrayList<>();
for (Chapter chapter : event.getChapters()) {
if (addedChapters.contains(chapter.name))
@ -113,9 +127,10 @@ public class DownloadManager {
if (!prepareDownload(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
@ -179,8 +194,7 @@ public class DownloadManager {
// Or if the page list already exists, start from the file
Observable.just(download.pages);
return pageListObservable
.subscribeOn(Schedulers.io())
return Observable.defer(() -> pageListObservable
.doOnNext(pages -> {
download.downloadedImages = 0;
download.setStatus(Download.DOWNLOADING);
@ -197,7 +211,8 @@ public class DownloadManager {
.onErrorResumeNext(error -> {
download.setStatus(Download.ERROR);
return Observable.just(download);
});
}))
.subscribeOn(Schedulers.io());
}
// Get the image from the filesystem if it exists or download from network
@ -269,7 +284,16 @@ public class DownloadManager {
// Get the filename for an image given the page
private String getImageFilename(Page page) {
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) {
@ -304,7 +328,6 @@ public class DownloadManager {
// Return the page list from the chapter's directory if it exists, null otherwise
public List<Page> getSavedPageList(Source source, Manga manga, Chapter chapter) {
List<Page> pages = null;
File chapterDir = getAbsoluteChapterDirectory(source, manga, chapter);
File pagesFile = new File(chapterDir, PAGE_LIST_FILE);
@ -313,14 +336,14 @@ public class DownloadManager {
if (pagesFile.exists()) {
reader = new JsonReader(new FileReader(pagesFile.getAbsolutePath()));
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());
} finally {
if (reader != null) try { reader.close(); } catch (IOException e) { /* Do nothing */ }
}
return pages;
return null;
}
// Shortcut for the method above
@ -387,18 +410,19 @@ public class DownloadManager {
if (queue.isEmpty())
return false;
boolean hasPendingDownloads = false;
if (downloadsSubscription == null)
initializeSubscriptions();
final List<Download> pending = new ArrayList<>();
for (Download download : queue) {
if (download.getStatus() != Download.DOWNLOADED) {
if (download.getStatus() != Download.QUEUE) download.setStatus(Download.QUEUE);
if (!hasPendingDownloads) hasPendingDownloads = true;
downloadsQueueSubject.onNext(download);
pending.add(download);
}
}
return hasPendingDownloads;
downloadsQueueSubject.onNext(pending);
return !pending.isEmpty();
}
public void stopDownloads() {

View File

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

View File

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

View File

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

View File

@ -4,12 +4,6 @@ import android.content.Context;
import android.net.Uri;
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.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.network.NetworkHelper;
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;
public class MyAnimeList extends MangaSyncService {
@ -209,7 +208,7 @@ public class MyAnimeList extends MangaSyncService {
xml.endTag("", ENTRY_TAG);
xml.endDocument();
FormEncodingBuilder form = new FormEncodingBuilder();
FormBody.Builder form = new FormBody.Builder();
form.add("data", writer.toString());
return form.build();
}

View File

@ -1,34 +1,47 @@
package eu.kanade.tachiyomi.data.network;
import com.squareup.okhttp.CacheControl;
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 android.content.Context;
import java.io.File;
import java.net.CookieManager;
import java.net.CookiePolicy;
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;
public final class NetworkHelper {
private OkHttpClient client;
private CookieManager cookieManager;
public final CacheControl NULL_CACHE_CONTROL = new CacheControl.Builder().noCache().build();
public final Headers NULL_HEADERS = new Headers.Builder().build();
public final 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.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) {
@ -86,19 +99,20 @@ public final class NetworkHelper {
.headers(headers != null ? headers : NULL_HEADERS)
.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());
} catch (Throwable e) {
return Observable.error(e);
}
}).retry(2);
}).retry(1);
}
public CookieStore getCookies() {

View File

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

View File

@ -88,6 +88,10 @@ public class PreferencesHelper {
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() {
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);
}
public Preference<Boolean> catalogueAsList() {
return rxPrefs.getBoolean(getKey(R.string.pref_display_catalogue_as_list), false);
}
public String getSourceUsername(Source source) {
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();
}
public int downloadThreads() {
return prefs.getInt(getKey(R.string.pref_download_slots_key), 1);
public Preference<Integer> downloadThreads() {
return rxPrefs.getInteger(getKey(R.string.pref_download_slots_key), 1);
}
public boolean downloadOnlyOverWifi() {

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.data.source;
import android.content.Context;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
@ -59,7 +60,9 @@ public class SourceManager {
}
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;
}
}

View File

@ -1,8 +1,5 @@
package eu.kanade.tachiyomi.data.source.base;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Response;
import org.jsoup.nodes.Document;
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.Manga;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
import okhttp3.Headers;
import okhttp3.Response;
import rx.Observable;
public abstract class BaseSource {

View File

@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.data.source.base;
import android.content.Context;
import com.bumptech.glide.load.model.LazyHeaders;
import com.squareup.okhttp.Headers;
import com.squareup.okhttp.Response;
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.source.model.MangasPage;
import eu.kanade.tachiyomi.data.source.model.Page;
import okhttp3.Headers;
import okhttp3.Response;
import rx.Observable;
import rx.schedulers.Schedulers;
@ -93,7 +93,7 @@ public abstract class Source extends BaseSource {
}
public Observable<List<Page>> getCachedPageListOrPullFromNetwork(final String chapterUrl) {
return chapterCache.getPageUrlsFromDiskCache(getChapterCacheKey(chapterUrl))
return chapterCache.getPageListFromCache(getChapterCacheKey(chapterUrl))
.onErrorResumeNext(throwable -> {
return pullPageListFromNetwork(chapterUrl);
})
@ -168,7 +168,7 @@ public abstract class Source extends BaseSource {
return getImageProgressResponse(page)
.flatMap(resp -> {
try {
chapterCache.putImageToDiskCache(page.getImageUrl(), resp);
chapterCache.putImageToCache(page.getImageUrl(), resp);
} catch (IOException e) {
return Observable.error(e);
}
@ -182,7 +182,7 @@ public abstract class Source extends BaseSource {
public void savePageList(String chapterUrl, List<Page> pages) {
if (pages != null)
chapterCache.putPageUrlsToDiskCache(getChapterCacheKey(chapterUrl), pages);
chapterCache.putPageListToCache(getChapterCacheKey(chapterUrl), pages);
}
protected List<Page> convertToPages(List<String> pageUrls) {

View File

@ -4,10 +4,6 @@ import android.content.Context;
import android.net.Uri;
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.nodes.Document;
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.Page;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Response;
import rx.Observable;
public class Batoto extends LoginSource {
@ -320,7 +319,7 @@ public class Batoto extends LoginSource {
Element form = doc.select("#login").first();
String postUrl = form.attr("action");
FormEncodingBuilder formBody = new FormEncodingBuilder();
FormBody.Builder formBody = new FormBody.Builder();
Element authKey = form.select("input[name=auth_key]").first();
formBody.add(authKey.attr("name"), authKey.attr("value"));
@ -354,8 +353,13 @@ public class Batoto extends LoginSource {
@Override
public Observable<List<Chapter>> pullChaptersFromNetwork(String mangaUrl) {
Observable<List<Chapter>> observable;
if (!isLogged()) {
observable = login(prefs.getSourceUsername(this), prefs.getSourcePassword(this))
String username = prefs.getSourceUsername(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));
}
else {

View File

@ -3,10 +3,6 @@ package eu.kanade.tachiyomi.data.source.online.english;
import android.content.Context;
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.nodes.Document;
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.Page;
import eu.kanade.tachiyomi.util.Parser;
import okhttp3.FormBody;
import okhttp3.Headers;
import okhttp3.Response;
import rx.Observable;
public class Kissmanga extends Source {
@ -109,7 +108,7 @@ public class Kissmanga extends Source {
if (page.page == 1)
page.url = getInitialSearchUrl(query);
FormEncodingBuilder form = new FormEncodingBuilder();
FormBody.Builder form = new FormBody.Builder();
form.add("authorArtist", "");
form.add("mangaName", query);
form.add("status", "");

View File

@ -228,12 +228,8 @@ public class Mangafox extends Source {
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", "");
int counter = 1;
for (Element pageUrlElement : pageUrlElements) {
if(counter < pageUrlElements.size()) {
pageUrlList.add(baseUrl + pageUrlElement.attr("value") + ".html");
}
counter++;
pageUrlList.add(baseUrl + pageUrlElement.attr("value") + ".html");
}
return pageUrlList;

View File

@ -63,7 +63,7 @@ public class UpdateMangaSyncService extends Service {
subscriptions.add(Observable.defer(() -> sync.update(mangaSync))
.flatMap(response -> {
if (response.isSuccessful()) {
return db.insertMangaSync(mangaSync).createObservable();
return db.insertMangaSync(mangaSync).asRxObservable();
}
return Observable.error(new Exception("Could not update MAL"));
})

View File

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

View File

@ -14,8 +14,8 @@ import eu.kanade.tachiyomi.injection.module.AppModule;
import eu.kanade.tachiyomi.injection.module.DataModule;
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter;
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.category.CategoryPresenter;
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
import eu.kanade.tachiyomi.ui.manga.MangaPresenter;
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.reader.ReaderActivity;
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.SettingsActivity;
@ -44,6 +45,7 @@ public interface AppComponent {
void inject(DownloadPresenter downloadPresenter);
void inject(MyAnimeListPresenter myAnimeListPresenter);
void inject(CategoryPresenter categoryPresenter);
void inject(RecentChaptersPresenter recentChaptersPresenter);
void inject(ReaderActivity readerActivity);
void inject(MangaActivity mangaActivity);
@ -57,7 +59,6 @@ public interface AppComponent {
void inject(LibraryUpdateService libraryUpdateService);
void inject(DownloadService downloadService);
void inject(UpdateMangaSyncService updateMangaSyncService);
Application application();
}

View File

@ -8,9 +8,9 @@ import dagger.Module;
import dagger.Provides;
import eu.kanade.tachiyomi.data.cache.ChapterCache;
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.download.DownloadManager;
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
import eu.kanade.tachiyomi.data.network.NetworkHelper;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.data.source.SourceManager;
@ -47,8 +47,8 @@ public class DataModule {
@Provides
@Singleton
NetworkHelper provideNetworkHelper() {
return new NetworkHelper();
NetworkHelper provideNetworkHelper(Application app) {
return new NetworkHelper(app);
}
@Provides

View File

@ -107,14 +107,14 @@ public class RxPresenter<View> extends Presenter<View> {
}
/**
* Checks if a restartable is subscribed.
* Checks if a restartable is unsubscribed.
*
* @param restartableId id of a restartable.
* @return True if the restartable is subscribed, false otherwise.
* @param restartableId id of the restartable.
* @return true if the subscription is null or unsubscribed, false otherwise.
*/
public boolean isSubscribed(int restartableId) {
Subscription s = restartableSubscriptions.get(restartableId);
return s != null && !s.isUnsubscribed();
public boolean isUnsubscribed(int restartableId) {
Subscription subscription = restartableSubscriptions.get(restartableId);
return subscription == null || subscription.isUnsubscribed();
}
/**

View File

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

View File

@ -4,7 +4,10 @@ import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
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.Toolbar;
import android.text.TextUtils;
@ -14,9 +17,14 @@ import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.AdapterView;
import android.widget.ProgressBar;
import android.widget.Spinner;
import android.widget.ViewSwitcher;
import com.afollestad.materialdialogs.MaterialDialog;
import java.util.List;
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.ui.base.adapter.FlexibleViewHolder;
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.manga.MangaActivity;
import eu.kanade.tachiyomi.util.ToastUtil;
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 nucleus.factory.RequiresPresenter;
import rx.Subscription;
@ -43,14 +53,17 @@ import rx.subjects.PublishSubject;
public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
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_grid) ProgressBar progressGrid;
private Toolbar toolbar;
private Spinner spinner;
private CatalogueAdapter adapter;
private EndlessRecyclerScrollListener scrollListener;
private EndlessGridScrollListener gridScrollListener;
private EndlessListScrollListener listScrollListener;
@State String query = "";
@State int selectedIndex = -1;
@ -59,6 +72,9 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
private PublishSubject<String> queryDebouncerSubject;
private Subscription queryDebouncerSubscription;
private MenuItem displayMode;
private MenuItem searchItem;
public static CatalogueFragment newInstance() {
return new CatalogueFragment();
}
@ -75,13 +91,32 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
View view = inflater.inflate(R.layout.fragment_catalogue, container, false);
ButterKnife.bind(this, view);
// Initialize adapter and scroll listener
GridLayoutManager layoutManager = (GridLayoutManager) recycler.getLayoutManager();
// Initialize adapter, scroll listener and recycler views
adapter = new CatalogueAdapter(this);
scrollListener = new EndlessRecyclerScrollListener(layoutManager, this::requestNextPage);
recycler.setHasFixedSize(true);
recycler.setAdapter(adapter);
recycler.addOnScrollListener(scrollListener);
GridLayoutManager glm = (GridLayoutManager) catalogueGrid.getLayoutManager();
gridScrollListener = new EndlessGridScrollListener(glm, this::requestNextPage);
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
Context themedContext = getBaseActivity().getSupportActionBar() != null ?
@ -107,6 +142,8 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
} else {
selectedIndex = position;
showProgressBar();
glm.scrollToPositionWithOffset(0, 0);
llm.scrollToPositionWithOffset(0, 0);
getPresenter().startRequesting(source);
}
}
@ -128,7 +165,7 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
inflater.inflate(R.menu.catalogue_list, 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();
if (!TextUtils.isEmpty(query)) {
@ -149,6 +186,22 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
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
@ -165,6 +218,9 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
@Override
public void onDestroyView() {
if (searchItem != null && searchItem.isActionViewExpanded()) {
searchItem.collapseActionView();
}
toolbar.removeView(spinner);
super.onDestroyView();
}
@ -191,11 +247,13 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
private void restartRequest(String newQuery) {
// If text didn't change, do nothing
if (query.equals(newQuery)) return;
if (query.equals(newQuery) || getPresenter().getSource() == null)
return;
query = newQuery;
showProgressBar();
recycler.getLayoutManager().scrollToPosition(0);
catalogueGrid.getLayoutManager().scrollToPosition(0);
catalogueList.getLayoutManager().scrollToPosition(0);
getPresenter().restartRequest(query);
}
@ -209,9 +267,10 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
public void onAddPage(int page, List<Manga> mangas) {
hideProgressBar();
if (page == 1) {
if (page == 0) {
adapter.clear();
scrollListener.resetScroll();
gridScrollListener.resetScroll();
listScrollListener.resetScroll();
}
adapter.addItems(mangas);
}
@ -221,15 +280,28 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
}
public void updateImage(Manga manga) {
CatalogueHolder holder = getHolder(manga);
CatalogueGridHolder holder = getHolder(manga);
if (holder != null) {
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
private CatalogueHolder getHolder(Manga manga) {
return (CatalogueHolder) recycler.findViewHolderForItemId(manga.id);
private CatalogueGridHolder getHolder(Manga manga) {
return (CatalogueGridHolder) catalogueGrid.findViewHolderForItemId(manga.id);
}
private void showProgressBar() {
@ -257,6 +329,20 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
@Override
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();
}
}

View File

@ -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);
}
}
}

View File

@ -1,38 +1,15 @@
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;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
public class CatalogueHolder extends FlexibleViewHolder {
@Bind(R.id.title) TextView title;
@Bind(R.id.thumbnail) ImageView thumbnail;
@Bind(R.id.favorite_sticker) ImageView favoriteSticker;
public abstract class CatalogueHolder extends FlexibleViewHolder {
public CatalogueHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) {
super(view, adapter, listener);
ButterKnife.bind(this, view);
}
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);
}
}
}
abstract void onSetValues(Manga manga, CataloguePresenter presenter);
}

View File

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

View File

@ -2,7 +2,6 @@ package eu.kanade.tachiyomi.ui.catalogue;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Pair;
import com.pushtorefresh.storio.sqlite.operations.put.PutResult;
@ -38,14 +37,16 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
private String query;
private int currentPage;
private RxPager pager;
private RxPager<Manga> pager;
private MangasPage lastMangasPage;
private PublishSubject<List<Manga>> mangaDetailSubject;
private boolean isListMode;
private static final int GET_MANGA_LIST = 1;
private static final int GET_MANGA_DETAIL = 2;
private static final int GET_MANGA_PAGE = 3;
@Override
protected void onCreate(Bundle savedState) {
@ -57,31 +58,46 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
mangaDetailSubject = PublishSubject.create();
pager = new RxPager<>();
restartableReplay(GET_MANGA_LIST,
() -> pager.pages().concatMap(page -> getMangasPageObservable(page + 1)),
(view, pair) -> view.onAddPage(pair.first, pair.second),
(view, error) -> {
view.onAddPageError();
Timber.e(error.getMessage());
});
pager::results,
(view, pair) -> view.onAddPage(pair.first, pair.second));
restartableFirst(GET_MANGA_PAGE,
() -> pager.request(page -> getMangasPageObservable(page + 1)),
(view, next) -> {},
(view, error) -> view.onAddPageError());
restartableLatestCache(GET_MANGA_DETAIL,
() -> mangaDetailSubject
.observeOn(Schedulers.io())
.flatMap(Observable::from)
.filter(manga -> !manga.initialized)
.window(3)
.concatMap(pack -> pack.concatMap(this::getMangaDetails))
.concatMap(this::getMangaDetails)
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread()),
CatalogueFragment::updateImage,
(view, error) -> Timber.e(error.getMessage()));
add(prefs.catalogueAsList().asObservable()
.subscribe(this::setDisplayMode));
}
private void onProcessRestart() {
source = sourceManager.get(sourceId);
stop(GET_MANGA_LIST);
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) {
@ -92,20 +108,23 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
public void restartRequest(String query) {
this.query = query;
stop(GET_MANGA_LIST);
currentPage = 1;
pager = new RxPager();
stop(GET_MANGA_PAGE);
lastMangasPage = null;
start(GET_MANGA_DETAIL);
if (!isListMode) {
start(GET_MANGA_DETAIL);
}
start(GET_MANGA_LIST);
start(GET_MANGA_PAGE);
}
public void requestNext() {
if (hasNextPage())
pager.requestNext(++currentPage);
if (hasNextPage()) {
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);
if (page != 1) {
nextMangasPage.url = lastMangasPage.nextPageUrl;
@ -120,11 +139,7 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
.flatMap(mangasPage -> Observable.from(mangasPage.mangas))
.map(this::networkToLocalManga)
.toList()
.map(mangas -> Pair.create(page, mangas))
.doOnNext(pair -> {
if (mangaDetailSubject != null)
mangaDetailSubject.onNext(pair.second);
})
.doOnNext(this::initializeMangas)
.observeOn(AndroidSchedulers.mainThread());
}
@ -138,9 +153,12 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
return localManga;
}
public void initializeMangas(List<Manga> mangas) {
mangaDetailSubject.onNext(mangas);
}
private Observable<Manga> getMangaDetails(final Manga manga) {
return source.pullMangaFromNetwork(manga.url)
.subscribeOn(Schedulers.io())
.flatMap(networkManga -> {
manga.copyFrom(networkManga);
db.insertManga(manga).executeAsBlocking();
@ -170,4 +188,17 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
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);
}
}

View File

@ -2,13 +2,13 @@ package eu.kanade.tachiyomi.ui.decoration;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.LinearLayoutManager;
import android.view.View;
import android.graphics.Canvas;
import android.graphics.Rect;
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 {

View File

@ -90,6 +90,7 @@ public class DownloadPresenter extends BasePresenter<DownloadFragment> {
.flatMap(tick -> Observable.from(download.pages)
.map(Page::getProgress)
.reduce((x, y) -> x + y))
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(progress -> {
if (download.totalProgress != progress) {

View File

@ -32,8 +32,10 @@ public class LibraryAdapter extends SmartFragmentStatePagerAdapter {
}
public void setCategories(List<Category> categories) {
this.categories = categories;
notifyDataSetChanged();
if (this.categories != categories) {
this.categories = categories;
notifyDataSetChanged();
}
}
public void setSelectionMode(int mode) {

View File

@ -52,7 +52,7 @@ public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga
@Override
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);
}
@ -67,7 +67,7 @@ public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga
}
public int getCoverHeight() {
return fragment.recycler.getItemWidth() / 9 * 12;
return fragment.recycler.getItemWidth() / 3 * 4;
}
@Override

View File

@ -34,6 +34,7 @@ public class LibraryCategoryFragment extends BaseFragment
@State int position;
private LibraryCategoryAdapter adapter;
private List<Manga> mangas;
private Subscription numColumnsSubscription;
@ -112,10 +113,13 @@ public class LibraryCategoryFragment extends BaseFragment
Category category = categories.get(position);
List<Manga> mangas = event.getMangasForCategory(category);
if (mangas == null) {
mangas = new ArrayList<>();
if (this.mangas != mangas) {
this.mangas = mangas;
if (mangas == null) {
mangas = new ArrayList<>();
}
setMangas(mangas);
}
setMangas(mangas);
}
protected void openManga(Manga manga) {

View File

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

View File

@ -58,7 +58,7 @@ public class LibraryPresenter extends BasePresenter<LibraryFragment> {
@Override
protected void onTakeView(LibraryFragment libraryFragment) {
super.onTakeView(libraryFragment);
if (!isSubscribed(GET_LIBRARY)) {
if (isUnsubscribed(GET_LIBRARY)) {
start(GET_LIBRARY);
}
}
@ -70,12 +70,12 @@ public class LibraryPresenter extends BasePresenter<LibraryFragment> {
}
private Observable<List<Category>> getCategoriesObservable() {
return db.getCategories().createObservable()
return db.getCategories().asRxObservable()
.doOnNext(categories -> this.categories = categories);
}
private Observable<Map<Integer, List<Manga>>> getLibraryMangasObservable() {
return db.getLibraryMangas().createObservable()
return db.getLibraryMangas().asRxObservable()
.flatMap(mangas -> Observable.from(mangas)
.groupBy(manga -> manga.category)
.flatMap(group -> group.toList()

View File

@ -24,7 +24,7 @@ public class CategoryPresenter extends BasePresenter<CategoryActivity> {
super.onCreate(savedState);
restartableLatestCache(GET_CATEGORIES,
() -> db.getCategories().createObservable()
() -> db.getCategories().asRxObservable()
.doOnNext(categories -> this.categories = categories)
.observeOn(AndroidSchedulers.mainThread()),
CategoryActivity::setCategories);
@ -46,11 +46,11 @@ public class CategoryPresenter extends BasePresenter<CategoryActivity> {
}
cat.order = max;
db.insertCategory(cat).createObservable().subscribe();
db.insertCategory(cat).asRxObservable().subscribe();
}
public void deleteCategories(List<Category> categories) {
db.deleteCategories(categories).createObservable().subscribe();
db.deleteCategories(categories).asRxObservable().subscribe();
}
public void reorderCategories(List<Category> categories) {
@ -58,11 +58,11 @@ public class CategoryPresenter extends BasePresenter<CategoryActivity> {
categories.get(i).order = i;
}
db.insertCategories(categories).createObservable().subscribe();
db.insertCategories(categories).asRxObservable().subscribe();
}
public void renameCategory(Category category, String name) {
category.name = name;
db.insertCategory(category).createObservable().subscribe();
db.insertCategory(category).asRxObservable().subscribe();
}
}

View File

@ -8,8 +8,10 @@ import android.support.v4.widget.DrawerLayout;
import android.support.v7.widget.Toolbar;
import android.widget.FrameLayout;
import com.mikepenz.google_material_typeface_library.GoogleMaterial;
import com.mikepenz.materialdrawer.Drawer;
import com.mikepenz.materialdrawer.DrawerBuilder;
import com.mikepenz.materialdrawer.model.DividerDrawerItem;
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
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.download.DownloadFragment;
import eu.kanade.tachiyomi.ui.library.LibraryFragment;
import eu.kanade.tachiyomi.ui.recent.RecentChaptersFragment;
import eu.kanade.tachiyomi.ui.setting.SettingsActivity;
import icepick.State;
import nucleus.view.ViewWithPresenter;
@ -28,12 +31,11 @@ public class MainActivity extends BaseActivity {
@Bind(R.id.appbar) AppBarLayout appBar;
@Bind(R.id.toolbar) Toolbar toolbar;
@Bind(R.id.drawer_container) FrameLayout container;
@State
int selectedItem;
private Drawer drawer;
private FragmentStack fragmentStack;
@State int selectedItem;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
@ -52,7 +54,7 @@ public class MainActivity extends BaseActivity {
fragmentStack = new FragmentStack(this, getSupportFragmentManager(), R.id.content_layout,
fragment -> {
if (fragment instanceof ViewWithPresenter)
((ViewWithPresenter)fragment).getPresenter().destroy();
((ViewWithPresenter) fragment).getPresenter().destroy();
});
drawer = new DrawerBuilder()
@ -70,20 +72,27 @@ public class MainActivity extends BaseActivity {
.addDrawerItems(
new PrimaryDrawerItem()
.withName(R.string.label_library)
.withIdentifier(R.id.nav_drawer_library),
// new PrimaryDrawerItem()
// .withName(R.string.recent_updates_title)
// .withIdentifier(R.id.nav_drawer_recent_updates),
.withIdentifier(R.id.nav_drawer_library)
.withIcon(GoogleMaterial.Icon.gmd_book),
new PrimaryDrawerItem()
.withName(R.string.label_recent_updates)
.withIdentifier(R.id.nav_drawer_recent_updates)
.withIcon(GoogleMaterial.Icon.gmd_update),
new PrimaryDrawerItem()
.withName(R.string.label_catalogues)
.withIdentifier(R.id.nav_drawer_catalogues),
.withIdentifier(R.id.nav_drawer_catalogues)
.withIcon(GoogleMaterial.Icon.gmd_explore),
new PrimaryDrawerItem()
.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()
.withName(R.string.label_settings)
.withIdentifier(R.id.nav_drawer_settings)
.withSelectable(false)
.withIcon(GoogleMaterial.Icon.gmd_settings)
)
.withSavedInstance(savedState)
.withOnDrawerItemClickListener(
@ -95,6 +104,7 @@ public class MainActivity extends BaseActivity {
setFragment(LibraryFragment.newInstance());
break;
case R.id.nav_drawer_recent_updates:
setFragment(RecentChaptersFragment.newInstance());
break;
case R.id.nav_drawer_catalogues:
setFragment(CatalogueFragment.newInstance());
@ -177,4 +187,4 @@ public class MainActivity extends BaseActivity {
return appBar;
}
}
}

View File

@ -1,12 +1,17 @@
package eu.kanade.tachiyomi.ui.manga;
import android.Manifest;
import android.content.Context;
import android.content.Intent;
import android.content.pm.PackageManager;
import android.os.Build;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.ActivityCompat;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.content.ContextCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.Toolbar;
@ -14,6 +19,7 @@ import javax.inject.Inject;
import butterknife.Bind;
import butterknife.ButterKnife;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.R;
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.tabs) TabLayout tabs;
@Bind(R.id.view_pager) ViewPager view_pager;
@Bind(R.id.view_pager) ViewPager viewPager;
@Inject PreferencesHelper preferences;
@Inject MangaSyncManager mangaSyncManager;
private MangaDetailAdapter adapter;
private long manga_id;
private boolean is_online;
private boolean isOnline;
public final static String MANGA_ID = "manga_id";
public final static String MANGA_ONLINE = "manga_online";
public static Intent newIntent(Context context, Manga manga) {
Intent intent = new Intent(context, MangaActivity.class);
intent.putExtra(MANGA_ID, manga.id);
if (manga != null) {
EventBus.getDefault().postSticky(manga);
}
return intent;
}
@ -59,23 +65,21 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
Intent intent = getIntent();
manga_id = intent.getLongExtra(MANGA_ID, -1);
is_online = intent.getBooleanExtra(MANGA_ONLINE, false);
isOnline = intent.getBooleanExtra(MANGA_ONLINE, false);
setupViewPager();
if (savedState == null)
getPresenter().queryManga(manga_id);
requestPermissionsOnMarshmallow();
}
private void setupViewPager() {
adapter = new MangaDetailAdapter(getSupportFragmentManager(), this);
view_pager.setAdapter(adapter);
tabs.setupWithViewPager(view_pager);
viewPager.setAdapter(adapter);
tabs.setupWithViewPager(viewPager);
if (!is_online)
view_pager.setCurrentItem(MangaDetailAdapter.CHAPTERS_FRAGMENT);
if (!isOnline)
viewPager.setCurrentItem(MangaDetailAdapter.CHAPTERS_FRAGMENT);
}
public void setManga(Manga manga) {
@ -83,7 +87,22 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
}
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 {
@ -104,7 +123,7 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
};
pageCount = 2;
if (!is_online && mangaSyncManager.getMyAnimeList().isLogged())
if (!isOnline && mangaSyncManager.getMyAnimeList().isLogged())
pageCount++;
}

View File

@ -7,44 +7,48 @@ import javax.inject.Inject;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
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.util.EventBusHook;
import icepick.State;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class MangaPresenter extends BasePresenter<MangaActivity> {
@Inject DatabaseHelper db;
@State long mangaId;
@State Manga manga;
private static final int DB_MANGA = 1;
private static final int GET_MANGA = 1;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
restartableLatestCache(DB_MANGA, this::getDbMangaObservable, MangaActivity::setManga);
restartableLatestCache(GET_MANGA, this::getMangaObservable, MangaActivity::setManga);
if (savedState == null)
registerForStickyEvents();
}
@Override
protected void onDestroy() {
super.onDestroy();
// Avoid new instances receiving wrong manga
EventBus.getDefault().removeStickyEvent(Manga.class);
EventBus.getDefault().removeStickyEvent(MangaEvent.class);
}
private Observable<Manga> getDbMangaObservable() {
return db.getManga(mangaId).createObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(manga -> EventBus.getDefault().postSticky(manga));
private Observable<Manga> getMangaObservable() {
return Observable.just(manga)
.doOnNext(manga -> EventBus.getDefault().postSticky(new MangaEvent(manga)));
}
public void queryManga(long mangaId) {
this.mangaId = mangaId;
start(DB_MANGA);
@EventBusHook
public void onEventMainThread(Manga manga) {
EventBus.getDefault().removeStickyEvent(manga);
unregisterForEvents();
this.manga = manga;
start(GET_MANGA);
}
}

View File

@ -10,6 +10,7 @@ import java.util.List;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
public class ChaptersAdapter extends FlexibleAdapter<ChaptersHolder, Chapter> {
@ -33,7 +34,8 @@ public class ChaptersAdapter extends FlexibleAdapter<ChaptersHolder, Chapter> {
@Override
public void onBindViewHolder(ChaptersHolder holder, int 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
holder.itemView.setActivated(isSelected(position));

View File

@ -10,6 +10,7 @@ import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
@ -24,6 +25,7 @@ import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.download.DownloadService;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
@ -61,6 +63,12 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
return new ChaptersFragment();
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@ -71,26 +79,14 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
// Init RecyclerView and adapter
linearLayout = new LinearLayoutManager(getActivity());
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);
adapter = new ChaptersAdapter(this);
recyclerView.setAdapter(adapter);
// Set initial values
setReadFilter();
setDownloadedFilter();
setSortIcon();
// Init listeners
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 -> {
Chapter chapter = getPresenter().getNextUnreadChapter();
if (chapter != null) {
@ -104,15 +100,40 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
}
@Override
public void onResume() {
super.onResume();
observeChapterDownloadProgress();
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.chapters, menu);
}
@Override
public void onPause() {
unsubscribeChapterDownloadProgress();
super.onPause();
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
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) {
@ -158,6 +179,29 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
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() {
downloadProgressSubscription = getPresenter().getDownloadProgressObs()
.subscribe(this::onDownloadProgressChange,
@ -175,10 +219,10 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
holder.onProgressChange(getContext(), download.downloadedImages, download.pages.size());
}
public void onChapterStatusChange(Chapter chapter) {
ChaptersHolder holder = getHolder(chapter);
public void onChapterStatusChange(Download download) {
ChaptersHolder holder = getHolder(download.chapter);
if (holder != null)
holder.onStatusChange(chapter.status);
holder.onStatusChange(download.getStatus());
}
@Nullable
@ -254,6 +298,11 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
return true;
}
public boolean onMarkPreviousAsRead(Chapter chapter) {
getPresenter().markPreviousChaptersAsRead(chapter);
return true;
}
protected boolean onDownload(Observable<Chapter> chapters) {
DownloadService.start(getActivity());
@ -337,13 +386,13 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
public void setReadFilter() {
if (readCb != null) {
readCb.setChecked(getPresenter().getReadFilter());
readCb.setChecked(getPresenter().onlyUnread());
}
}
public void setDownloadedFilter() {
if (downloadedCb != null) {
downloadedCb.setChecked(getPresenter().getDownloadedFilter());
downloadedCb.setChecked(getPresenter().onlyDownloaded());
}
}

View File

@ -7,6 +7,8 @@ import android.widget.PopupMenu;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.text.DecimalFormat;
import java.text.DecimalFormatSymbols;
import java.text.SimpleDateFormat;
import java.util.Date;
@ -14,40 +16,61 @@ import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import rx.Observable;
public class ChaptersHolder extends FlexibleViewHolder {
private final ChaptersAdapter adapter;
private Chapter item;
@Bind(R.id.chapter_title) TextView title;
@Bind(R.id.download_text) TextView downloadText;
@Bind(R.id.chapter_menu) RelativeLayout chapterMenu;
@Bind(R.id.chapter_pages) TextView pages;
@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) {
super(view, adapter, listener);
this.adapter = adapter;
context = view.getContext();
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)));
}
public void onSetValues(Context context, Chapter chapter) {
public void onSetValues(Chapter chapter, Manga manga) {
this.item = chapter;
title.setText(chapter.name);
if (chapter.read) {
title.setTextColor(ContextCompat.getColor(context, R.color.hint_text));
} else {
title.setTextColor(ContextCompat.getColor(context, R.color.primary_text));
String name;
switch (manga.getDisplayMode()) {
case Manga.DISPLAY_NAME:
default:
name = chapter.name;
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) {
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);
case R.id.action_delete:
return adapter.getFragment().onDelete(chapter);
case R.id.action_mark_previous_as_read:
return adapter.getFragment().onMarkPreviousAsRead(item);
}
return false;
});

View File

@ -18,6 +18,7 @@ import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.event.ChapterCountEvent;
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
import eu.kanade.tachiyomi.event.MangaEvent;
import eu.kanade.tachiyomi.event.ReaderEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import eu.kanade.tachiyomi.util.EventBusHook;
@ -38,16 +39,14 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
private Manga manga;
private Source source;
private List<Chapter> chapters;
private boolean sortOrderAToZ = true;
private boolean onlyUnread = true;
private boolean onlyDownloaded;
@State boolean hasRequested;
private PublishSubject<List<Chapter>> chaptersSubject;
private static final int DB_CHAPTERS = 1;
private static final int FETCH_CHAPTERS = 2;
private static final int CHAPTER_STATUS_CHANGES = 3;
private static final int GET_MANGA = 1;
private static final int DB_CHAPTERS = 2;
private static final int FETCH_CHAPTERS = 3;
private static final int CHAPTER_STATUS_CHANGES = 4;
@Override
protected void onCreate(Bundle savedState) {
@ -59,6 +58,10 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
chaptersSubject = PublishSubject.create();
restartableLatestCache(GET_MANGA,
() -> Observable.just(manga),
ChaptersFragment::onNextManga);
restartableLatestCache(DB_CHAPTERS,
this::getDbChaptersObs,
ChaptersFragment::onNextChapters);
@ -70,13 +73,14 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
restartableLatestCache(CHAPTER_STATUS_CHANGES,
this::getChapterStatusObs,
(view, download) -> view.onChapterStatusChange(download.chapter),
(view, download) -> view.onChapterStatusChange(download),
(view, error) -> Timber.e(error.getCause(), error.getMessage()));
registerForStickyEvents();
}
private void onProcessRestart() {
stop(GET_MANGA);
stop(DB_CHAPTERS);
stop(FETCH_CHAPTERS);
stop(CHAPTER_STATUS_CHANGES);
@ -90,14 +94,15 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
}
@EventBusHook
public void onEventMainThread(Manga manga) {
this.manga = manga;
public void onEventMainThread(MangaEvent event) {
this.manga = event.manga;
start(GET_MANGA);
if (!isSubscribed(DB_CHAPTERS)) {
if (isUnsubscribed(DB_CHAPTERS)) {
source = sourceManager.get(manga.source);
start(DB_CHAPTERS);
add(db.getChapters(manga).createObservable()
add(db.getChapters(manga).asRxObservable()
.subscribeOn(Schedulers.io())
.doOnNext(chapters -> {
this.chapters = chapters;
@ -135,13 +140,13 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
private Observable<List<Chapter>> applyChapterFilters(List<Chapter> chapters) {
Observable<Chapter> observable = Observable.from(chapters)
.subscribeOn(Schedulers.io());
if (onlyUnread) {
if (onlyUnread()) {
observable = observable.filter(chapter -> !chapter.read);
}
if (onlyDownloaded) {
if (onlyDownloaded()) {
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(chapter.chapter_number, chapter2.chapter_number));
}
@ -175,7 +180,7 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
break;
}
}
if (onlyDownloaded && download.getStatus() == Download.DOWNLOADED)
if (onlyDownloaded() && download.getStatus() == Download.DOWNLOADED)
refreshChapters();
}
@ -202,11 +207,20 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
return chapter;
})
.toList()
.flatMap(chapters -> db.insertChapters(chapters).createObservable())
.flatMap(chapters -> db.insertChapters(chapters).asRxObservable())
.observeOn(AndroidSchedulers.mainThread())
.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) {
add(selectedChapters
.toList()
@ -222,7 +236,7 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
}, error -> {
Timber.e(error.getMessage());
}, () -> {
if (onlyDownloaded)
if (onlyDownloaded())
refreshChapters();
}));
}
@ -232,32 +246,38 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
}
public void revertSortOrder() {
//TODO manga.chapter_order
sortOrderAToZ = !sortOrderAToZ;
manga.setChapterOrder(getSortOrder() ? Manga.SORT_ZA : Manga.SORT_AZ);
db.insertManga(manga).executeAsBlocking();
refreshChapters();
}
public void setReadFilter(boolean onlyUnread) {
//TODO do we need save filter for manga?
this.onlyUnread = onlyUnread;
manga.setReadFilter(onlyUnread ? Manga.SHOW_UNREAD : Manga.SHOW_ALL);
db.insertManga(manga).executeAsBlocking();
refreshChapters();
}
public void setDownloadedFilter(boolean onlyDownloaded) {
this.onlyDownloaded = onlyDownloaded;
manga.setDownloadedFilter(onlyDownloaded ? Manga.SHOW_DOWNLOADED : Manga.SHOW_ALL);
db.insertManga(manga).executeAsBlocking();
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() {
return sortOrderAToZ;
}
public boolean getReadFilter() {
return onlyUnread;
}
public boolean getDownloadedFilter() {
return onlyDownloaded;
return manga.sortChaptersAZ();
}
public Manga getManga() {

View File

@ -1,6 +1,11 @@
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.support.design.widget.FloatingActionButton;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.LayoutInflater;
import android.view.View;
@ -10,30 +15,38 @@ import android.widget.ImageView;
import android.widget.TextView;
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.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.cache.CoverCache;
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.util.ToastUtil;
import nucleus.factory.RequiresPresenter;
@RequiresPresenter(MangaInfoPresenter.class)
public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
private static final int REQUEST_IMAGE_OPEN = 101;
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
@Bind(R.id.manga_artist) TextView artist;
@Bind(R.id.manga_author) TextView author;
@Bind(R.id.manga_chapters) TextView chapterCount;
@Bind(R.id.manga_genres) TextView genres;
@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_cover) ImageView cover;
@Bind(R.id.action_favorite) Button favoriteBtn;
@Bind(R.id.fab_edit) FloatingActionButton fabEdit;
public static MangaInfoFragment newInstance() {
return new MangaInfoFragment();
@ -52,26 +65,47 @@ public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
View view = inflater.inflate(R.layout.fragment_manga_info, container, false);
ButterKnife.bind(this, view);
favoriteBtn.setOnClickListener(v -> {
getPresenter().toggleFavorite();
});
//Create edit drawable with size 24dp (google guidelines)
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);
return view;
}
public void onNextManga(Manga manga) {
public void onNextManga(Manga manga, Source source) {
if (manga.initialized) {
setMangaInfo(manga);
setMangaInfo(manga, source);
} else {
// Initialize manga
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);
author.setText(manga.author);
if (mangaSource != null) {
source.setText(mangaSource.getName());
}
genres.setText(manga.genre);
status.setText(manga.getStatus(getActivity()));
description.setText(manga.description);
@ -82,7 +116,7 @@ public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
LazyHeaders headers = getPresenter().source.getGlideHeaders();
if (manga.thumbnail_url != null && cover.getDrawable() == null) {
if (manga.favorite) {
coverCache.saveAndLoadFromCache(cover, manga.thumbnail_url, headers);
coverCache.saveOrLoadFromCache(cover, manga.thumbnail_url, headers);
} else {
coverCache.loadFromNetwork(cover, manga.thumbnail_url, headers);
}
@ -93,7 +127,7 @@ public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
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);
}
@ -102,6 +136,44 @@ public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
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() {
setRefreshing(false);
}

View File

@ -1,6 +1,10 @@
package eu.kanade.tachiyomi.ui.manga.info;
import android.os.Bundle;
import android.widget.ImageView;
import java.io.File;
import java.io.IOException;
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.base.Source;
import eu.kanade.tachiyomi.event.ChapterCountEvent;
import eu.kanade.tachiyomi.event.MangaEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import eu.kanade.tachiyomi.util.EventBusHook;
import rx.Observable;
@ -18,19 +23,42 @@ import rx.schedulers.Schedulers;
public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
@Inject DatabaseHelper db;
@Inject SourceManager sourceManager;
@Inject CoverCache coverCache;
private Manga manga;
protected Source source;
private int count = -1;
private boolean isFetching;
/**
* The id of the restartable.
*/
private static final int GET_MANGA = 1;
/**
* The id of the restartable.
*/
private static final int GET_CHAPTER_COUNT = 2;
/**
* The id of the restartable.
*/
private static final int FETCH_MANGA_INFO = 3;
/**
* Source information
*/
protected Source source;
/**
* Used to connect to database
*/
@Inject DatabaseHelper db;
/**
* 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
protected void onCreate(Bundle savedState) {
@ -40,22 +68,29 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
onProcessRestart();
}
// Update manga cache
restartableLatestCache(GET_MANGA,
() -> Observable.just(manga),
MangaInfoFragment::onNextManga);
(view, manga) -> view.onNextManga(manga, source));
// Update chapter count
restartableLatestCache(GET_CHAPTER_COUNT,
() -> Observable.just(count),
MangaInfoFragment::setChapterCount);
// Fetch manga info from source
restartableFirst(FETCH_MANGA_INFO,
this::fetchMangaObs,
(view, manga) -> view.onFetchMangaDone(),
(view, error) -> view.onFetchMangaError());
// onEventMainThread receives an event thanks to this line.
registerForStickyEvents();
}
/**
* Called when savedState not null
*/
private void onProcessRestart() {
stop(GET_MANGA);
stop(GET_CHAPTER_COUNT);
@ -69,10 +104,10 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
}
@EventBusHook
public void onEventMainThread(Manga manga) {
this.manga = manga;
public void onEventMainThread(MangaEvent event) {
this.manga = event.manga;
source = sourceManager.get(manga.source);
start(GET_MANGA);
refreshManga();
}
@EventBusHook
@ -83,9 +118,11 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
}
}
/**
* Fetch manga info from source
*/
public void fetchMangaFromSource() {
if (!isFetching) {
isFetching = true;
if (isUnsubscribed(FETCH_MANGA_INFO)) {
start(FETCH_MANGA_INFO);
}
}
@ -97,23 +134,48 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
db.insertManga(manga).executeAsBlocking();
return Observable.just(manga);
})
.finallyDo(() -> isFetching = false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(manga -> refreshManga());
}
public void toggleFavorite() {
manga.favorite = !manga.favorite;
onMangaFavoriteChange(manga.favorite);
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) {
if (isFavorite) {
coverCache.save(manga.thumbnail_url, source.getGlideHeaders());
} 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);
}
}

View File

@ -4,6 +4,8 @@ import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import java.util.List;
import javax.inject.Inject;
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.mangasync.MangaSyncManager;
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.util.EventBusHook;
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 REFRESH = 3;
private static final String PREFIX_MY = "my:";
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
@ -46,16 +51,14 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
myAnimeList = syncManager.getMyAnimeList();
restartableLatestCache(GET_MANGA_SYNC,
() -> db.getMangaSync(manga, myAnimeList).createObservable()
() -> db.getMangaSync(manga, myAnimeList).asRxObservable()
.doOnNext(mangaSync -> this.mangaSync = mangaSync)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()),
MyAnimeListFragment::setMangaSync);
restartableLatestCache(GET_SEARCH_RESULTS,
() -> myAnimeList.search(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()),
this::getSearchResultsObservable,
(view, results) -> {
view.setSearchResults(results);
}, (view, error) -> {
@ -75,7 +78,7 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
}
return Observable.error(new Exception("Could not find manga"));
})
.flatMap(myManga -> db.insertMangaSync(myManga).createObservable())
.flatMap(myManga -> db.insertMangaSync(myManga).asRxObservable())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()),
(view, result) -> view.onRefreshDone(),
@ -102,14 +105,30 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
}
@EventBusHook
public void onEventMainThread(Manga manga) {
this.manga = manga;
public void onEventMainThread(MangaEvent event) {
this.manga = event.manga;
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() {
add(myAnimeList.update(mangaSync)
.flatMap(response -> db.insertMangaSync(mangaSync).createObservable())
.flatMap(response -> db.insertMangaSync(mangaSync).asRxObservable())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(next -> {},
@ -139,7 +158,7 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
add(myAnimeList.bind(manga)
.flatMap(response -> {
if (response.isSuccessful()) {
return db.insertMangaSync(manga).createObservable();
return db.insertMangaSync(manga).asRxObservable();
}
return Observable.error(new Exception("Could not bind manga"));
})

View File

@ -7,6 +7,7 @@ import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.FragmentManager;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
@ -20,11 +21,8 @@ import com.afollestad.materialdialogs.MaterialDialog;
import java.util.List;
import javax.inject.Inject;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
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.toolbar) Toolbar toolbar;
@Inject PreferencesHelper preferences;
private BaseReader viewer;
private ReaderMenu readerMenu;
@ -75,7 +71,6 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
App.get(this).getComponent().inject(this);
setContentView(R.layout.activity_reader);
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) {
if (currentPage == -1) {
currentPage = pages.size() - 1;
}
if (viewer == null) {
viewer = createViewer(manga);
getSupportFragmentManager().beginTransaction().replace(R.id.reader, viewer).commit();
viewer = getOrCreateViewer(manga);
}
viewer.onPageListReady(pages, currentPage);
readerMenu.onChapterReady(pages.size(), manga, chapter, currentPage);
@ -176,19 +174,33 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
readerMenu.onAdjacentChapters(previous, next);
}
private BaseReader createViewer(Manga manga) {
int mangaViewer = manga.viewer == 0 ? preferences.getDefaultViewer() : manga.viewer;
private BaseReader getOrCreateViewer(Manga manga) {
int mangaViewer = manga.viewer == 0 ? getPreferences().getDefaultViewer() : manga.viewer;
switch (mangaViewer) {
case LEFT_TO_RIGHT: default:
return new LeftToRightReader();
case RIGHT_TO_LEFT:
return new RightToLeftReader();
case VERTICAL:
return new VerticalReader();
case WEBTOON:
return new WebtoonReader();
FragmentManager fm = getSupportFragmentManager();
// Try to reuse the viewer using its tag
BaseReader fragment = (BaseReader) fm.findFragmentByTag(manga.viewer + "");
if (fragment == null) {
// Create a new viewer
switch (mangaViewer) {
case LEFT_TO_RIGHT: default:
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) {
@ -221,6 +233,8 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
}
private void initializeSettings() {
PreferencesHelper preferences = getPreferences();
subscriptions.add(preferences.showPageNumber()
.asObservable()
.subscribe(this::setPageNumberVisibility));
@ -286,7 +300,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
private void setCustomBrightness(boolean enabled) {
if (enabled) {
subscriptions.add(customBrightnessSubscription = preferences.customBrightnessValue()
subscriptions.add(customBrightnessSubscription = getPreferences().customBrightnessValue()
.asObservable()
.subscribe(this::setCustomBrightnessValue));
} else {
@ -344,7 +358,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
}
public PreferencesHelper getPreferences() {
return preferences;
return getPresenter().prefs;
}
public BaseReader getViewer() {

View File

@ -7,6 +7,7 @@ import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.animation.Animation;
@ -44,7 +45,7 @@ public class ReaderMenu {
@Bind(R.id.lock_orientation) ImageButton lockOrientation;
@Bind(R.id.reader_selector) ImageButton readerSelector;
@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 prevChapterBtn;
@ -56,7 +57,6 @@ public class ReaderMenu {
@State boolean showing;
private PopupWindow settingsPopup;
private PopupWindow brightnessPopup;
private boolean inverted;
private DecimalFormat decimalFormat;
@ -70,10 +70,10 @@ public class ReaderMenu {
bottomMenu.setOnTouchListener((v, event) -> true);
seekBar.setOnSeekBarChangeListener(new PageSeekBarChangeListener());
decimalFormat = new DecimalFormat("#.##");
decimalFormat = new DecimalFormat("#.###");
inverted = false;
initializeOptions();
initializeMenu();
}
public void add(Subscription subscription) {
@ -110,7 +110,6 @@ public class ReaderMenu {
bottomMenu.startAnimation(bottomMenuAnimation);
settingsPopup.dismiss();
brightnessPopup.dismiss();
showing = false;
}
@ -148,8 +147,8 @@ public class ReaderMenu {
// Set initial values
totalPages.setText("" + numPages);
currentPage.setText("" + (currentPageIndex + 1));
seekBar.setProgress(currentPageIndex);
seekBar.setMax(numPages - 1);
seekBar.setProgress(currentPageIndex);
activity.setToolbarTitle(manga.title);
activity.setToolbarSubtitle(chapter.chapter_number != -1 ?
@ -175,7 +174,7 @@ public class ReaderMenu {
if (nextChapterBtn != null) nextChapterBtn.setVisible(nextChapter != null);
}
private void initializeOptions() {
private void initializeMenu() {
// Orientation changes
add(preferences.lockOrientation().asObservable()
.subscribe(locked -> {
@ -190,6 +189,18 @@ public class ReaderMenu {
lockOrientation.setOnClickListener(v ->
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
readerSelector.setOnClickListener(v -> {
final Manga manga = activity.getPresenter().getManga();
@ -215,17 +226,6 @@ public class ReaderMenu {
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) {
@ -247,8 +247,11 @@ public class ReaderMenu {
@Bind(R.id.hide_status_bar) CheckBox hideStatusBar;
@Bind(R.id.keep_screen_on) CheckBox keepScreenOn;
@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_initial) TextView imageDecoderInitial;
@Bind(R.id.custom_brightness) CheckBox customBrightness;
@Bind(R.id.brightness_seekbar) SeekBar brightnessSeekbar;
public SettingsPopupWindow(View view) {
super(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
@ -282,7 +285,7 @@ public class ReaderMenu {
readerTheme.setOnCheckedChangeListener((view, isChecked) ->
preferences.readerTheme().set(isChecked ? 1 : 0));
imageDecoder.setOnClickListener(v -> {
imageDecoderContainer.setOnClickListener(v -> {
showImmersiveDialog(new MaterialDialog.Builder(activity)
.title(R.string.pref_image_decoder)
.items(R.array.image_decoders)
@ -294,6 +297,21 @@ public class ReaderMenu {
})
.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) {
@ -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 {
@Override

View File

@ -172,8 +172,8 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
private Observable<Pair<Chapter, Chapter>> getAdjacentChaptersObservable() {
return Observable.zip(
db.getPreviousChapter(chapter).createObservable().take(1),
db.getNextChapter(chapter).createObservable().take(1),
db.getPreviousChapter(chapter).asRxObservable().take(1),
db.getNextChapter(chapter).asRxObservable().take(1),
Pair::create)
.doOnNext(pair -> {
previousChapter = pair.first;
@ -211,12 +211,16 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
}
private Observable<List<MangaSync>> getMangaSyncObservable() {
return db.getMangasSync(manga).createObservable()
return db.getMangasSync(manga).asRxObservable()
.doOnNext(mangaSync -> this.mangaSyncList = mangaSync);
}
// Loads the given 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
stopPreloadingNextChapter();
@ -227,7 +231,7 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
if (!chapter.read && chapter.last_page_read != 0)
currentPage = chapter.last_page_read;
else
currentPage = 0;
currentPage = requestedPage;
// Reset next and previous chapter. They have to be fetched again
nextChapter = null;
@ -262,7 +266,7 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
if (isChapterFinished()) {
chapter.read = true;
}
db.insertChapter(chapter).createObservable().subscribe();
db.insertChapter(chapter).asRxObservable().subscribe();
}
// Check whether the chapter has been read
@ -312,7 +316,7 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
public boolean loadNextChapter() {
if (hasNextChapter()) {
onChapterLeft();
loadChapter(nextChapter);
loadChapter(nextChapter, 0);
return true;
}
return false;
@ -321,7 +325,7 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
public boolean loadPreviousChapter() {
if (hasPreviousChapter()) {
onChapterLeft();
loadChapter(previousChapter);
loadChapter(previousChapter, -1);
return true;
}
return false;
@ -342,7 +346,7 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
}
private void stopPreloadingNextChapter() {
if (isSubscribed(PRELOAD_NEXT_CHAPTER)) {
if (!isUnsubscribed(PRELOAD_NEXT_CHAPTER)) {
stop(PRELOAD_NEXT_CHAPTER);
if (nextChapterPageList != null)
source.savePageList(nextChapter.url, nextChapterPageList);

View File

@ -2,8 +2,10 @@ package eu.kanade.tachiyomi.ui.reader.viewer.base;
import android.view.MotionEvent;
import com.davemorrissey.labs.subscaleview.decoder.ImageDecoder;
import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder;
import com.davemorrissey.labs.subscaleview.decoder.RapidImageRegionDecoder;
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageDecoder;
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder;
import java.util.List;
@ -17,6 +19,7 @@ public abstract class BaseReader extends BaseFragment {
protected int currentPage;
protected List<Page> pages;
protected Class<? extends ImageRegionDecoder> regionDecoderClass;
protected Class<? extends ImageDecoder> bitmapDecoderClass;
public static final int RAPID_DECODER = 0;
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 boolean onImageTouch(MotionEvent motionEvent);
public void setRegionDecoderClass(int value) {
public void setDecoderClass(int value) {
switch (value) {
case RAPID_DECODER:
default:
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;
case SKIA_DECODER:
regionDecoderClass = SkiaImageRegionDecoder.class;
bitmapDecoderClass = SkiaImageDecoder.class;
break;
}
}
@ -66,6 +74,10 @@ public abstract class BaseReader extends BaseFragment {
return regionDecoderClass;
}
public Class<? extends ImageDecoder> getBitmapDecoderClass() {
return bitmapDecoderClass;
}
public ReaderActivity getReaderActivity() {
return (ReaderActivity) getActivity();
}

View File

@ -19,9 +19,12 @@ public abstract class PagerReader extends BaseReader {
protected PagerReaderAdapter adapter;
protected Pager pager;
private boolean isReady;
protected boolean transitions;
protected CompositeSubscription subscriptions;
protected int scaleType = 1;
protected void initializePager(Pager pager) {
this.pager = pager;
pager.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
@ -61,7 +64,14 @@ public abstract class PagerReader extends BaseReader {
subscriptions = new CompositeSubscription();
subscriptions.add(getReaderActivity().getPreferences().imageDecoder()
.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)
.distinctUntilChanged()
.subscribe(v -> adapter.notifyDataSetChanged()));
@ -71,6 +81,7 @@ public abstract class PagerReader extends BaseReader {
.subscribe(value -> transitions = value));
setPages();
isReady = true;
}
@Override
@ -84,7 +95,7 @@ public abstract class PagerReader extends BaseReader {
if (this.pages != pages) {
this.pages = pages;
this.currentPage = currentPage;
if (isResumed()) {
if (isReady) {
setPages();
}
}
@ -110,6 +121,10 @@ public abstract class PagerReader extends BaseReader {
return pager.onImageTouch(motionEvent);
}
private void setImageScaleType(int scaleType) {
this.scaleType = scaleType;
}
public abstract void onFirstPageOut();
public abstract void onLastPageOut();

View File

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.view.ViewGroup;
import java.util.List;
@ -23,7 +24,19 @@ public class PagerReaderAdapter extends FragmentStatePagerAdapter {
@Override
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() {
@ -35,9 +48,4 @@ public class PagerReaderAdapter extends FragmentStatePagerAdapter {
notifyDataSetChanged();
}
@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}

View File

@ -25,7 +25,7 @@ import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
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.Subscription;
import rx.android.schedulers.AndroidSchedulers;
@ -41,13 +41,12 @@ public class PagerReaderFragment extends BaseFragment {
@Bind(R.id.retry_button) Button retryButton;
private Page page;
private boolean isReady;
private Subscription progressSubscription;
private Subscription statusSubscription;
public static PagerReaderFragment newInstance(Page page) {
PagerReaderFragment fragment = new PagerReaderFragment();
fragment.setPage(page);
return fragment;
public static PagerReaderFragment newInstance() {
return new PagerReaderFragment();
}
@Override
@ -55,18 +54,20 @@ public class PagerReaderFragment extends BaseFragment {
View view = inflater.inflate(R.layout.item_pager_reader, container, false);
ButterKnife.bind(this, view);
ReaderActivity activity = getReaderActivity();
BaseReader parentFragment = (BaseReader) getParentFragment();
PagerReader parentFragment = (PagerReader) getParentFragment();
if (activity.getReaderTheme() == ReaderActivity.BLACK_THEME) {
progressText.setTextColor(ContextCompat.getColor(getContext(), R.color.light_grey));
}
imageView.setParallelLoadingEnabled(true);
imageView.setMaxDimensions(activity.getMaxBitmapSize(), activity.getMaxBitmapSize());
imageView.setMaxBitmapDimensions(activity.getMaxBitmapSize());
imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED);
imageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE);
imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE);
imageView.setMinimumScaleType(parentFragment.scaleType);
imageView.setRegionDecoderClass(parentFragment.getRegionDecoderClass());
imageView.setBitmapDecoderClass(parentFragment.getBitmapDecoderClass());
imageView.setVerticalScrollingParent(parentFragment instanceof VerticalReader);
imageView.setOnTouchListener((v, motionEvent) -> parentFragment.onImageTouch(motionEvent));
imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() {
@Override
@ -84,6 +85,7 @@ public class PagerReaderFragment extends BaseFragment {
});
observeStatus();
isReady = true;
return view;
}
@ -97,6 +99,9 @@ public class PagerReaderFragment extends BaseFragment {
public void setPage(Page page) {
this.page = page;
if (isReady) {
observeStatus();
}
}
private void showImage() {
@ -185,8 +190,8 @@ public class PagerReaderFragment extends BaseFragment {
final AtomicInteger currentValue = new AtomicInteger(-1);
progressSubscription = Observable.interval(75, TimeUnit.MILLISECONDS, Schedulers.newThread())
.onBackpressureDrop()
progressSubscription = Observable.interval(100, TimeUnit.MILLISECONDS, Schedulers.newThread())
.onBackpressureLatest()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(tick -> {
// Refresh UI only if progress change

View File

@ -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.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.PagerGestureListener;
import rx.functions.Action1;
public class HorizontalPager extends ViewPager implements Pager {
@ -47,7 +47,7 @@ public class HorizontalPager extends ViewPager implements Pager {
return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
return true;
return false;
}
}
@ -82,7 +82,7 @@ public class HorizontalPager extends ViewPager implements Pager {
return super.onTouchEvent(ev);
} catch (IllegalArgumentException e) {
return true;
return false;
}
}

View File

@ -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.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.PagerGestureListener;
import rx.functions.Action1;
public class VerticalPager extends VerticalViewPagerImpl implements Pager {
@ -32,7 +32,7 @@ public class VerticalPager extends VerticalViewPagerImpl implements Pager {
}
private void init(Context context) {
gestureDetector = new GestureDetector(context, new VerticalPagerGestureListener(this));
gestureDetector = new GestureDetector(context, new PagerGestureListener(this));
}
@Override
@ -46,7 +46,7 @@ public class VerticalPager extends VerticalViewPagerImpl implements Pager {
return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
return true;
return false;
}
}
@ -81,7 +81,7 @@ public class VerticalPager extends VerticalViewPagerImpl implements Pager {
return super.onTouchEvent(ev);
} 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;
}
}
}

View File

@ -10,6 +10,7 @@ import java.util.List;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> {
@ -47,7 +48,6 @@ public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> {
public void setPages(List<Page> pages) {
this.pages = pages;
notifyDataSetChanged();
}
public void clear() {
@ -65,4 +65,8 @@ public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> {
return fragment;
}
public ReaderActivity getReaderActivity() {
return (ReaderActivity) fragment.getActivity();
}
}

View File

@ -4,8 +4,6 @@ import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.ProgressBar;
@ -24,7 +22,6 @@ public class WebtoonHolder extends RecyclerView.ViewHolder {
@Bind(R.id.progress) ProgressBar progressBar;
@Bind(R.id.retry_button) Button retryButton;
private Animation fadeInAnimation;
private Page page;
private WebtoonAdapter adapter;
@ -33,20 +30,32 @@ public class WebtoonHolder extends RecyclerView.ViewHolder {
this.adapter = adapter;
ButterKnife.bind(this, view);
fadeInAnimation = AnimationUtils.loadAnimation(view.getContext(), R.anim.fade_in);
imageView.setParallelLoadingEnabled(true);
imageView.setMaxBitmapDimensions(adapter.getReaderActivity().getMaxBitmapSize());
imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED);
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.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() {
@Override
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);
retryButton.setOnTouchListener((v, event) -> {
@ -90,7 +99,6 @@ public class WebtoonHolder extends RecyclerView.ViewHolder {
setErrorButtonVisible(false);
setProgressVisible(false);
setImageVisible(true);
imageView.setRegionDecoderClass(adapter.getReader().getRegionDecoderClass());
imageView.setImage(ImageSource.uri(page.getImagePath()));
}

View File

@ -1,7 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.GestureDetector;
import android.view.LayoutInflater;
@ -30,12 +29,26 @@ public class WebtoonReader extends BaseReader {
private Subscription decoderSubscription;
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
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
adapter = new WebtoonAdapter(this);
int screenHeight = getResources().getDisplayMetrics().heightPixels;
scrollDistance = screenHeight * 3 / 4;
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.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
@ -45,28 +58,29 @@ public class WebtoonReader extends BaseReader {
decoderSubscription = getReaderActivity().getPreferences().imageDecoder()
.asObservable()
.doOnNext(this::setRegionDecoderClass)
.doOnNext(this::setDecoderClass)
.skip(1)
.distinctUntilChanged()
.subscribe(v -> adapter.notifyDataSetChanged());
.subscribe(v -> recycler.setAdapter(adapter));
gestureDetector = new GestureDetector(getActivity(), new SimpleOnGestureListener() {
gestureDetector = new GestureDetector(recycler.getContext(), new SimpleOnGestureListener() {
@Override
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;
}
@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();
isReady = true;
return recycler;
}
@ -83,6 +97,12 @@ public class WebtoonReader extends BaseReader {
super.onPause();
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
outState.putParcelable(SCROLL_STATE, layoutManager.onSaveInstanceState());
}
private void unsubscribeStatus() {
if (subscription != null && !subscription.isUnsubscribed())
subscription.unsubscribe();
@ -97,7 +117,9 @@ public class WebtoonReader extends BaseReader {
public void onPageListReady(List<Page> pages, int currentPage) {
if (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();
}
}
@ -107,9 +129,9 @@ public class WebtoonReader extends BaseReader {
if (pages != null) {
unsubscribeStatus();
recycler.clearOnScrollListeners();
adapter.clear();
recycler.scrollTo(0, 0);
adapter.setPages(pages);
recycler.setAdapter(adapter);
updatePageNumber();
setScrollListener();
observeStatus(0);
}

View File

@ -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);
}
}
}

View File

@ -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);
}
}

View File

@ -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);
}
}
}

View File

@ -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));
}
}

View File

@ -12,8 +12,8 @@ import java.util.List;
import javax.inject.Inject;
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.base.MangaSyncService;
import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog;

View File

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

View File

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

View File

@ -5,6 +5,10 @@ import java.net.URISyntaxException;
public class UrlUtil {
private static final String JPG = ".jpg";
private static final String PNG = ".png";
private static final String GIF = ".gif";
public static String getPath(String s) {
try {
URI uri = new URI(s);
@ -18,4 +22,37 @@ public class UrlUtil {
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;
}
}

View File

@ -5,7 +5,7 @@ import android.support.v7.widget.RecyclerView;
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 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;
public EndlessRecyclerScrollListener(GridLayoutManager layoutManager, Action0 requestNext) {
public EndlessGridScrollListener(GridLayoutManager layoutManager, Action0 requestNext) {
this.layoutManager = layoutManager;
this.requestNext = requestNext;
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 125 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 187 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 278 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 87 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 144 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 139 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

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

View File

@ -23,6 +23,7 @@
android:layout_gravity="bottom|right"
android:layout_margin="@dimen/fab_margin"
android:src="@drawable/ic_action_add_18dp"
app:backgroundTint="@color/colorPrimary"
app:layout_anchor="@id/categories_list"
app:layout_anchorGravity="bottom|right|end"
app:layout_behavior="eu.kanade.tachiyomi.ui.base.fab.ScrollAwareFABBehavior"/>

View File

@ -11,17 +11,28 @@
android:id="@+id/progress"
style="?android:attr/progressBarStyleLarge"
android:layout_width="wrap_content"
android:layout_height="fill_parent"
android:layout_height="match_parent"
android:layout_gravity="center_vertical|center_horizontal"
android:visibility="gone"/>
<eu.kanade.tachiyomi.widget.AutofitRecyclerView
android:id="@+id/recycler"
style="@style/AppTheme.GridView"
<ViewSwitcher
android:layout_width="match_parent"
android:layout_height="0dp"
android:layout_weight="1"
android:columnWidth="140dp"
tools:listitem="@layout/item_catalogue" />
android:id="@+id/switcher">
<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
android:id="@+id/progress_grid"

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