Compare commits

...

40 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
62 changed files with 1277 additions and 463 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 @@
[![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
@ -12,11 +18,6 @@ Current features:
* Schedule searching for updates
* Categories to organize your library
## Download
[![stable release](https://img.shields.io/badge/release-v0.1.2-blue.svg)](https://github.com/inorichi/tachiyomi/releases/download/v0.1.2/tachiyomi-v0.1.2.apk)
[![latest debug build](https://img.shields.io/badge/debug-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk)
## License
Copyright 2015 Javier Tomás

View File

@ -39,8 +39,8 @@ android {
minSdkVersion 16
targetSdkVersion 23
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
versionCode 3
versionName "0.1.2"
versionCode 4
versionName "0.1.3"
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
@ -81,6 +81,7 @@ 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.8.0'
final ICEPICK_VERSION = '3.1.0'
@ -95,8 +96,8 @@ dependencies {
compile "com.android.support:recyclerview-v7:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:support-annotations:$SUPPORT_LIBRARY_VERSION"
compile "com.android.support:percent:$SUPPORT_LIBRARY_VERSION"
compile 'com.squareup.okhttp:okhttp-urlconnection:2.7.2'
compile 'com.squareup.okhttp:okhttp:2.7.2'
compile "com.squareup.okhttp3:okhttp:$OKHTTP_VERSION"
compile "com.squareup.okhttp3:okhttp-urlconnection:$OKHTTP_VERSION"
compile 'com.squareup.okio:okio:1.6.0'
compile 'com.google.code.gson:gson:2.5'
compile 'com.jakewharton:disklrucache:2.0.2'
@ -129,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

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

@ -68,9 +68,24 @@ public class Manga implements Serializable {
public static final int COMPLETED = 2;
public static final int LICENSED = 3;
public static final int SORT_AZ = 0;
public static final int SORT_ZA = 1;
public static final int SORT_MASK = 1;
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() {}
@ -124,16 +139,41 @@ public class Manga implements Serializable {
}
}
public void setFlags(int flag, int mask) {
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 (this.chapter_flags & SORT_MASK) == SORT_AZ;
public boolean sortChaptersAZ() {
return (chapter_flags & SORT_MASK) == SORT_AZ;
}
public void setChapterOrder(int order) {
setFlags(order, SORT_MASK);
// 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

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;
@ -329,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);
@ -338,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

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

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

@ -59,7 +59,6 @@ public interface AppComponent {
void inject(LibraryUpdateService libraryUpdateService);
void inject(DownloadService downloadService);
void inject(UpdateMangaSyncService updateMangaSyncService);
Application application();
}

View File

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

@ -44,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

@ -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;
@ -29,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);
@ -53,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()
@ -71,20 +72,27 @@ public class MainActivity extends BaseActivity {
.addDrawerItems(
new PrimaryDrawerItem()
.withName(R.string.label_library)
.withIdentifier(R.id.nav_drawer_library),
.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),
.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(
@ -179,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;
@ -63,6 +68,8 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
isOnline = intent.getBooleanExtra(MANGA_ONLINE, false);
setupViewPager();
requestPermissionsOnMarshmallow();
}
private void setupViewPager() {
@ -83,6 +90,21 @@ public class MangaActivity extends BaseRxActivity<MangaPresenter> {
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 {
private int pageCount;

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;
@ -62,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) {
@ -92,6 +99,21 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
return view;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.chapters, menu);
}
@Override
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);
@ -157,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,
@ -341,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,60 @@ 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);
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) {

View File

@ -39,8 +39,6 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
private Manga manga;
private Source source;
private List<Chapter> chapters;
private boolean onlyUnread = true;
private boolean onlyDownloaded;
@State boolean hasRequested;
private PublishSubject<List<Chapter>> chaptersSubject;
@ -142,10 +140,10 @@ 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) -> getSortOrder() ?
@ -182,7 +180,7 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
break;
}
}
if (onlyDownloaded && download.getStatus() == Download.DOWNLOADED)
if (onlyDownloaded() && download.getStatus() == Download.DOWNLOADED)
refreshChapters();
}
@ -238,7 +236,7 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
}, error -> {
Timber.e(error.getMessage());
}, () -> {
if (onlyDownloaded)
if (onlyDownloaded())
refreshChapters();
}));
}
@ -254,28 +252,34 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
}
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 manga.sortChaptersAZ();
}
public boolean getReadFilter() {
return onlyUnread;
}
public boolean getDownloadedFilter() {
return onlyDownloaded;
}
public Manga getManga() {
return manga;
}

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,21 +15,28 @@ 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;
@ -33,9 +45,8 @@ public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
@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();
@ -54,9 +65,20 @@ 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;
@ -71,6 +93,12 @@ public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
}
}
/**
* 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);
@ -88,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);
}
@ -99,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);
}
@ -108,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;
@ -19,17 +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;
/**
* 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) {
@ -39,22 +68,29 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
onProcessRestart();
}
// Update manga cache
restartableLatestCache(GET_MANGA,
() -> Observable.just(manga),
(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);
@ -82,6 +118,9 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
}
}
/**
* Fetch manga info from source
*/
public void fetchMangaFromSource() {
if (isUnsubscribed(FETCH_MANGA_INFO)) {
start(FETCH_MANGA_INFO);
@ -107,16 +146,35 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
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
private void refreshManga() {
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;
@ -36,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);
@ -54,9 +58,7 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
MyAnimeListFragment::setMangaSync);
restartableLatestCache(GET_SEARCH_RESULTS,
() -> myAnimeList.search(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()),
this::getSearchResultsObservable,
(view, results) -> {
view.setSearchResults(results);
}, (view, error) -> {
@ -108,6 +110,22 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
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).asRxObservable())

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);
@ -169,8 +164,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
}
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);
@ -180,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) {
@ -225,6 +233,8 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
}
private void initializeSettings() {
PreferencesHelper preferences = getPreferences();
subscriptions.add(preferences.showPageNumber()
.asObservable()
.subscribe(this::setPageNumberVisibility));
@ -290,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 {
@ -348,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

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

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

@ -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> {
@ -64,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();
}
}
@ -109,6 +131,7 @@ public class WebtoonReader extends BaseReader {
recycler.clearOnScrollListeners();
adapter.setPages(pages);
recycler.setAdapter(adapter);
updatePageNumber();
setScrollListener();
observeStatus(0);
}

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 332 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 510 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 584 B

View File

@ -1,10 +1,11 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment">
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
tools:context="eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment">
<!-- It seems I have to wrap everything in SwipeRefreshLayout because it always take the entire height
@ -16,8 +17,8 @@
<android.support.v4.widget.SwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:orientation="vertical"
android:layout_height="match_parent">
android:layout_height="match_parent"
android:orientation="vertical">
<ScrollView
android:layout_width="match_parent"
@ -50,7 +51,7 @@
android:focusable="false"
android:focusableInTouchMode="false"
android:scaleType="fitXY"
android:visibility="visible" />
android:visibility="visible"/>
</RelativeLayout>
@ -72,7 +73,7 @@
android:layout_marginTop="5dp"
android:focusable="false"
android:focusableInTouchMode="false"
android:text="@string/author" />
android:text="@string/author"/>
<TextView
android:id="@+id/manga_author"
@ -85,7 +86,7 @@
android:focusable="false"
android:focusableInTouchMode="false"
android:maxLines="1"
android:singleLine="true" />
android:singleLine="true"/>
<TextView
android:id="@+id/manga_artist_label"
@ -97,7 +98,7 @@
android:layout_below="@id/manga_author_label"
android:focusable="false"
android:focusableInTouchMode="false"
android:text="@string/artist" />
android:text="@string/artist"/>
<TextView
android:id="@+id/manga_artist"
@ -110,7 +111,7 @@
android:focusable="false"
android:focusableInTouchMode="false"
android:maxLines="1"
android:singleLine="true" />
android:singleLine="true"/>
<TextView
android:id="@+id/manga_chapters_label"
@ -121,7 +122,7 @@
android:layout_below="@id/manga_artist_label"
android:focusable="false"
android:focusableInTouchMode="false"
android:text="@string/chapters" />
android:text="@string/chapters"/>
<TextView
android:id="@+id/manga_chapters"
@ -134,7 +135,7 @@
android:focusable="false"
android:focusableInTouchMode="false"
android:maxLines="1"
android:singleLine="true" />
android:singleLine="true"/>
<TextView
android:id="@+id/manga_status_label"
@ -146,7 +147,7 @@
android:layout_below="@id/manga_chapters_label"
android:focusable="false"
android:focusableInTouchMode="false"
android:text="@string/status" />
android:text="@string/status"/>
<TextView
android:id="@+id/manga_status"
@ -159,7 +160,7 @@
android:focusable="false"
android:focusableInTouchMode="false"
android:maxLines="1"
android:singleLine="true" />
android:singleLine="true"/>
<TextView
android:id="@+id/manga_source_label"
@ -170,7 +171,7 @@
android:layout_below="@id/manga_status_label"
android:focusable="false"
android:focusableInTouchMode="false"
android:text="@string/source" />
android:text="@string/source"/>
<TextView
android:id="@+id/manga_source"
@ -183,7 +184,7 @@
android:focusable="false"
android:focusableInTouchMode="false"
android:maxLines="1"
android:singleLine="true" />
android:singleLine="true"/>
<TextView
android:id="@+id/manga_genres_label"
@ -194,7 +195,7 @@
android:layout_below="@id/manga_source_label"
android:focusable="false"
android:focusableInTouchMode="false"
android:text="@string/genres" />
android:text="@string/genres"/>
<TextView
android:id="@+id/manga_genres"
@ -204,7 +205,7 @@
android:layout_below="@id/manga_genres_label"
android:focusable="false"
android:focusableInTouchMode="false"
android:singleLine="false" />
android:singleLine="false"/>
</RelativeLayout>
@ -221,7 +222,7 @@
android:id="@+id/action_favorite"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:text="@string/add_to_library" />
android:text="@string/add_to_library"/>
</LinearLayout>
<LinearLayout
@ -238,24 +239,43 @@
android:focusable="false"
android:focusableInTouchMode="false"
android:singleLine="false"
android:text="@string/description" />
android:text="@string/description"/>
<TextView
android:id="@+id/manga_summary"
style="@style/manga_detail_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:focusableInTouchMode="false"
android:singleLine="false" />
<TextView
android:id="@+id/manga_summary"
style="@style/manga_detail_text"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:focusable="false"
android:focusableInTouchMode="false"
android:singleLine="false"/>
</LinearLayout>
</LinearLayout>
</ScrollView>
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
android:layout_alignParentRight="true"
android:layout_margin="10dp"
android:gravity="bottom">
<android.support.design.widget.FloatingActionButton
android:id="@+id/fab_edit"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="bottom|right"
android:layout_margin="@dimen/fab_margin"
app:backgroundTint="@color/colorPrimary"
app:layout_behavior="eu.kanade.tachiyomi.ui.base.fab.ScrollAwareFABBehavior"/>
</LinearLayout>
</RelativeLayout>

View File

@ -6,7 +6,7 @@
<FrameLayout
android:layout_width="match_parent"
android:layout_height="300dp"
android:layout_height="match_parent"
android:id="@+id/frame_container">
<ProgressBar

View File

@ -1,23 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:orientation="vertical"
android:layout_width="wrap_content"
android:layout_height="match_parent"
android:background="@color/reader_menu_background"
android:paddingRight="10dp"
android:paddingLeft="5dp"
android:paddingTop="5dp"
android:paddingBottom="5dp">
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/reader_menu_settings_item"
android:text="@string/pref_custom_brightness"
android:id="@+id/custom_brightness" />
<SeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/brightness_seekbar" />
</LinearLayout>

View File

@ -74,18 +74,19 @@
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/reader_brightness"
android:src="@drawable/ic_brightness_high"
android:id="@+id/lock_orientation"
android:src="@drawable/ic_screen_rotation"
android:layout_gravity="center_vertical"
android:background="?android:selectableItemBackground" />
<ImageButton
android:layout_width="0dp"
android:layout_height="match_parent"
android:layout_weight="1"
android:id="@+id/lock_orientation"
android:src="@drawable/ic_screen_rotation"
android:id="@+id/reader_scale_type_selector"
android:src="@drawable/ic_zoom_out_map_white_24dp"
android:layout_gravity="center_vertical"
android:background="?android:selectableItemBackground" />
<ImageButton
android:layout_width="0dp"
android:layout_height="match_parent"

View File

@ -10,7 +10,8 @@
<LinearLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content">
android:layout_height="wrap_content"
android:id="@+id/image_decoder_container">
<TextView
android:id="@+id/image_decoder_initial"
@ -53,4 +54,16 @@
style="@style/reader_menu_settings_item"
android:text="@string/pref_keep_screen_on"/>
<CheckBox
android:layout_width="wrap_content"
android:layout_height="wrap_content"
style="@style/reader_menu_settings_item"
android:text="@string/pref_custom_brightness"
android:id="@+id/custom_brightness" />
<SeekBar
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:id="@+id/brightness_seekbar" />
</LinearLayout>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:title="@string/action_display_mode"
android:id="@+id/action_display_mode"
app:showAsAction="never" />
</menu>

View File

@ -48,6 +48,24 @@
<item>1</item>
</string-array>
<string-array name="image_scale_type">
<item>@string/scale_type_fit_screen</item>
<item>@string/scale_type_stretch</item>
<item>@string/scale_type_fit_width</item>
<item>@string/scale_type_fit_height</item>
<item>@string/scale_type_original_size</item>
<item>@string/scale_type_smart_fit</item>
</string-array>
<string-array name="image_scale_type_values">
<item>1</item>
<item>2</item>
<item>3</item>
<item>4</item>
<item>5</item>
<item>6</item>
</string-array>
<string-array name="library_update_interval">
<item>@string/update_never</item>
<item>@string/update_1hour</item>

View File

@ -11,6 +11,7 @@
<color name="primary">@color/colorPrimary</color>
<color name="primary_dark">@color/colorPrimaryDark</color>
<color name="primary_light">@color/colorPrimaryLight</color>
<color name="color_ripple">#E9F1FF</color>
<color name="divider">@color/md_light_dividers</color>

View File

@ -16,6 +16,7 @@
<string name="pref_ask_update_manga_sync_key">pref_ask_update_manga_sync_key</string>
<string name="pref_default_viewer_key">pref_default_viewer_key</string>
<string name="pref_image_scale_type_key">pref_image_scale_type_key</string>
<string name="pref_hide_status_bar_key">pref_hide_status_bar_key</string>
<string name="pref_lock_orientation_key">pref_lock_orientation_key</string>
<string name="pref_enable_transitions_key">pref_enable_transitions_key</string>

View File

@ -65,8 +65,8 @@
<string name="portrait">Portrait</string>
<string name="landscape">Landscape</string>
<string name="default_columns">Default</string>
<string name="pref_library_update_interval">Library update period</string>
<string name="pref_update_only_non_completed">Only update uncomplete manga</string>
<string name="pref_library_update_interval">Library update frequency</string>
<string name="pref_update_only_non_completed">Only update incomplete manga</string>
<string name="update_never">Manual</string>
<string name="update_1hour">Hourly</string>
<string name="update_2hour">Every 2 hours</string>
@ -75,8 +75,8 @@
<string name="update_12hour">Every 12 hours</string>
<string name="update_24hour">Daily</string>
<string name="update_48hour">Every 2 days</string>
<string name="pref_auto_update_manga_sync">Automatically update last chapter read in enabled services</string>
<string name="pref_ask_update_manga_sync">Ask for confirmation before updating</string>
<string name="pref_auto_update_manga_sync">Sync chapters after reading</string>
<string name="pref_ask_update_manga_sync">Confirm before updating</string>
<!-- Reader section -->
<string name="pref_hide_status_bar">Hide status bar</string>
@ -97,6 +97,14 @@
<string name="pref_image_decoder">Image decoder</string>
<string name="rapid_decoder">Rapid</string>
<string name="skia_decoder">Skia</string>
<string name="pref_image_scale_type">Scale type</string>
<string name="scale_type_fit_screen">Fit screen</string>
<string name="scale_type_stretch">Stretch</string>
<string name="scale_type_fit_width">Fit width</string>
<string name="scale_type_fit_height">Fit height</string>
<string name="scale_type_original_size">Original size</string>
<string name="scale_type_smart_fit">Smart fit</string>
<!-- Downloads section -->
<string name="pref_download_directory">Downloads directory</string>
@ -118,7 +126,7 @@
<!-- ACRA -->
<string name="pref_enable_acra">Send crash reports</string>
<string name="pref_acra_summary">Helps fixing bugs. No sensitive data is sent</string>
<string name="pref_acra_summary">Helps fix any bugs. No sensitive data will be sent</string>
<!-- Login dialog -->
@ -136,7 +144,7 @@
<string name="library_selection_title">Selected</string>
<!-- Catalogue fragment -->
<string name="source_requires_login">This source requires login</string>
<string name="source_requires_login">This source requires you to log in</string>
<string name="select_source">Select a source</string>
<!-- Manga info fragment -->
@ -157,12 +165,15 @@
<!-- Manga chapters fragment -->
<string name="manga_chapters_tab">Chapters</string>
<string name="manga_chapter_no_title">No title</string>
<string name="display_mode_chapter">Chapter %1$s</string>
<string name="chapter_downloaded">Downloaded</string>
<string name="chapter_queued">Queued</string>
<string name="chapter_downloading">Downloading</string>
<string name="chapter_downloading_progress">Downloading (%1$d/%2$d)</string>
<string name="chapter_error">Error</string>
<string name="fetch_chapters_error">Error while fetching chapters</string>
<string name="show_title">Show title</string>
<string name="show_chapter_number">Show chapter number</string>
<!-- MyAnimeList fragment -->
<string name="reading">Reading</string>
@ -177,7 +188,7 @@
<string name="downloading">Downloading…</string>
<string name="download_progress">Downloaded %1$d%%</string>
<string name="chapter_progress">Page: %1$d</string>
<string name="page_list_error">Error fetching page list. Is network available?</string>
<string name="page_list_error">Error fetching page list. Check your internet connection.</string>
<string name="chapter_subtitle">Chapter %1$s</string>
<string name="no_next_chapter">Next chapter not found</string>
<string name="no_previous_chapter">Previous chapter not found</string>
@ -185,14 +196,18 @@
<string name="confirm_update_manga_sync">Update last chapter read in enabled services to %1$d?</string>
<!-- Downloads activity and service -->
<string name="download_queue_error">An error occurred while downloading chapters. Restart it from the downloads section</string>
<string name="download_queue_error">An error occurred while downloading chapters. You can try again in the downloads section</string>
<!-- Library update service notifications -->
<string name="notification_update_progress">Update progress: %1$d/%2$d</string>
<string name="notification_update_completed">Update completed</string>
<string name="notification_update_error">An unexpected error occurred while updating the library</string>
<string name="notification_no_new_chapters">No new chapters found</string>
<string name="notification_new_chapters">Found new chapters for:</string>
<string name="notification_manga_update_failed">Failed updating manga:</string>
<string name="notification_new_chapters">New chapters found for:</string>
<string name="notification_manga_update_failed">Failed to update manga:</string>
<string name="notification_first_add_to_library">Please add the manga to your library before doing this</string>
<!-- File Picker Titles -->
<string name="file_select_cover">Select cover image</string>
</resources>

View File

@ -17,6 +17,10 @@
android:key="@string/pref_show_page_number_key"
android:defaultValue="true" />
<SwitchPreference android:title="@string/pref_custom_brightness"
android:key="@string/pref_custom_brightness_key"
android:defaultValue="false" />
<eu.kanade.tachiyomi.widget.preference.IntListPreference
android:title="@string/pref_viewer_type"
android:key="@string/pref_default_viewer_key"
@ -25,6 +29,14 @@
android:defaultValue="1"
android:summary="%s"/>
<eu.kanade.tachiyomi.widget.preference.IntListPreference
android:title="@string/pref_image_scale_type"
android:key="@string/pref_image_scale_type_key"
android:entries="@array/image_scale_type"
android:entryValues="@array/image_scale_type_values"
android:defaultValue="1"
android:summary="%s"/>
<eu.kanade.tachiyomi.widget.preference.IntListPreference
android:title="@string/pref_reader_theme"
android:key="@string/pref_reader_theme_key"

View File

@ -121,10 +121,15 @@ public class SubsamplingScaleImageView extends View {
public static final int SCALE_TYPE_CENTER_INSIDE = 1;
/** Scale the image uniformly so that both dimensions of the image will be equal to or larger than the corresponding dimension of the view. The image is then centered in the view. */
public static final int SCALE_TYPE_CENTER_CROP = 2;
public static final int SCALE_TYPE_FIT_WIDTH = 3;
public static final int SCALE_TYPE_FIT_HEIGHT = 4;
public static final int SCALE_TYPE_ORIGINAL_SIZE = 5;
public static final int SCALE_TYPE_SMART_FIT = 6;
/** Scale the image so that both dimensions of the image will be equal to or less than the maxScale and equal to or larger than minScale. The image is then centered in the view. */
public static final int SCALE_TYPE_CUSTOM = 3;
public static final int SCALE_TYPE_CUSTOM = 7;
private static final List<Integer> VALID_SCALE_TYPES = Arrays.asList(SCALE_TYPE_CENTER_CROP, SCALE_TYPE_CENTER_INSIDE, SCALE_TYPE_CUSTOM);
private static final List<Integer> VALID_SCALE_TYPES = Arrays.asList(SCALE_TYPE_CENTER_CROP, SCALE_TYPE_CENTER_INSIDE, SCALE_TYPE_CUSTOM, SCALE_TYPE_FIT_WIDTH, SCALE_TYPE_FIT_HEIGHT, SCALE_TYPE_SMART_FIT, SCALE_TYPE_ORIGINAL_SIZE);
// Bitmap (preview or full image)
private Bitmap bitmap;
@ -196,8 +201,12 @@ public class SubsamplingScaleImageView extends View {
private int sOrientation;
private Rect sRegion;
private Rect pRegion;
private int cWidth;
private int cHeight;
// Max bitmap dimensions the device can display
private int maxBitmapDimensions;
// Vertical pagers/scrollers should enable this
private boolean isVerticalScrollingParent;
// Is two-finger zooming in progress
private boolean isZooming;
@ -749,16 +758,32 @@ public class SubsamplingScaleImageView extends View {
float lastX = vTranslate.x;
float lastY = vTranslate.y;
fitToBounds(true);
boolean atXEdge = lastX != vTranslate.x;
boolean edgeXSwipe = atXEdge && dx > dy && !isPanning;
boolean yPan = lastY == vTranslate.y && dy > 15;
if (!edgeXSwipe && (!atXEdge || yPan || isPanning)) {
isPanning = true;
} else if (dx > 5) {
// Haven't panned the image, and we're at the left or right edge. Switch to page swipe.
maxTouchCount = 0;
handler.removeMessages(MESSAGE_LONG_CLICK);
getParent().requestDisallowInterceptTouchEvent(false);
if (!isVerticalScrollingParent) {
boolean atXEdge = lastX != vTranslate.x;
boolean edgeXSwipe = atXEdge && dx > dy && !isPanning;
boolean yPan = lastY == vTranslate.y && dy > 15;
if (!edgeXSwipe && (!atXEdge || yPan || isPanning)) {
isPanning = true;
} else if (dx > 5) {
// Haven't panned the image, and we're at the left or right edge. Switch to page swipe.
maxTouchCount = 0;
handler.removeMessages(MESSAGE_LONG_CLICK);
getParent().requestDisallowInterceptTouchEvent(false);
}
} else {
boolean atYEdge = lastY != vTranslate.y;
boolean edgeYSwipe = atYEdge && dy > dx && !isPanning;
boolean xPan = lastX == vTranslate.x && dx > 15;
if (!edgeYSwipe && (!atYEdge || xPan || isPanning)) {
isPanning = true;
} else if (dy > 5) {
// Haven't panned the image, and we're at the top or bottom edge. Switch to page swipe.
maxTouchCount = 0;
handler.removeMessages(MESSAGE_LONG_CLICK);
getParent().requestDisallowInterceptTouchEvent(false);
}
}
if (!panEnabled) {
@ -865,7 +890,7 @@ public class SubsamplingScaleImageView extends View {
}
// When using tiles, on first render with no tile map ready, initialise it and kick off async base image loading.
if (tileMap == null && decoder != null) {
if (tileMap == null && decoder != null && maxBitmapDimensions == 0) {
initialiseBaseLayer(getMaxBitmapDimensions(canvas));
}
@ -1385,11 +1410,6 @@ public class SubsamplingScaleImageView extends View {
}
}
public void setMaxDimensions(int width, int height) {
cWidth = width;
cHeight = height;
}
/**
* Async task used to get image details without blocking the UI thread.
*/
@ -1468,8 +1488,8 @@ public class SubsamplingScaleImageView extends View {
this.sHeight = sHeight;
this.sOrientation = sOrientation;
checkReady();
if (!checkImageLoaded() && cWidth != 0 && cHeight != 0) {
initialiseBaseLayer(new Point(cWidth, cHeight));
if (!checkImageLoaded() && maxBitmapDimensions > 0) {
initialiseBaseLayer(new Point(maxBitmapDimensions, maxBitmapDimensions));
}
invalidate();
requestLayout();
@ -2003,12 +2023,28 @@ public class SubsamplingScaleImageView extends View {
private float minScale() {
int vPadding = getPaddingBottom() + getPaddingTop();
int hPadding = getPaddingLeft() + getPaddingRight();
if (minimumScaleType == SCALE_TYPE_CENTER_CROP) {
return Math.max((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight());
} else if (minimumScaleType == SCALE_TYPE_CUSTOM && minScale > 0) {
return minScale;
} else {
return Math.min((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight());
switch (minimumScaleType) {
case SCALE_TYPE_CENTER_INSIDE:
default:
return Math.min((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight());
case SCALE_TYPE_CENTER_CROP:
return Math.max((getWidth() - hPadding) / (float) sWidth(), (getHeight() - vPadding) / (float) sHeight());
case SCALE_TYPE_FIT_WIDTH:
return (getWidth() - hPadding) / (float) sWidth();
case SCALE_TYPE_FIT_HEIGHT:
return (getHeight() - vPadding) / (float) sHeight();
case SCALE_TYPE_ORIGINAL_SIZE:
return 1;
case SCALE_TYPE_SMART_FIT:
if (sWidth <= sHeight) {
// Fit to width
return (getWidth() - hPadding) / (float) sWidth();
} else {
// Fit to height
return (getHeight() - vPadding) / (float) sHeight();
}
case SCALE_TYPE_CUSTOM:
return minScale;
}
}
@ -2455,6 +2491,21 @@ public class SubsamplingScaleImageView extends View {
this.parallelLoadingEnabled = parallelLoadingEnabled;
}
/**
* Set max bitmap dimensions the device can display
*/
public void setMaxBitmapDimensions(int maxBitmapDimensions) {
this.maxBitmapDimensions = maxBitmapDimensions;
}
/**
* Set vertical scroll mode to fix gestures
*/
public void setVerticalScrollingParent(boolean isVerticalScrollingParent) {
this.isVerticalScrollingParent = isVerticalScrollingParent;
}
/**
* Enables visual debugging, showing tile boundaries and sizes.
*/

View File

@ -0,0 +1,24 @@
package com.davemorrissey.labs.subscaleview.decoder;
import android.content.Context;
import android.graphics.Bitmap;
import android.net.Uri;
import rapid.decoder.BitmapDecoder;
/**
* A very simple implementation of {@link com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder}
* using the RapidDecoder library (https://github.com/suckgamony/RapidDecoder). For PNGs, this can
* give more reliable decoding and better performance. For JPGs, it is slower and can run out of
* memory with large images, but has better support for grayscale and CMYK images.
*
* This is an incomplete and untested implementation provided as an example only.
*/
public class RapidImageDecoder implements ImageDecoder {
@Override
public Bitmap decode(Context context, Uri uri) throws Exception {
return BitmapDecoder.from(context, uri).useBuiltInDecoder(true).config(Bitmap.Config.RGB_565).decode();
}
}