mirror of
https://github.com/mihonapp/mihon.git
synced 2025-06-26 19:17:51 +02:00
Compare commits
40 Commits
Author | SHA1 | Date | |
---|---|---|---|
c204548df5 | |||
4d47f5a387 | |||
7944bb8479 | |||
c4ae88a8ff | |||
ad953b7bf6 | |||
d799ae5d72 | |||
a3ec057384 | |||
486f129e62 | |||
e6c3864c71 | |||
7461f12066 | |||
e53b05feba | |||
bcefc176c1 | |||
d0580d0df1 | |||
28fd22dfe0 | |||
742924625d | |||
78a2eae719 | |||
38bb0b61d4 | |||
8b52fea602 | |||
c03495be94 | |||
f19889c222 | |||
af0ab5ec86 | |||
ea4fa60e01 | |||
4b60560a9f | |||
733b0da461 | |||
db074a371d | |||
bb110ce353 | |||
74c32f9e16 | |||
d8ab8f297f | |||
ec7df6b1f2 | |||
ef03ca22d1 | |||
82865dd3fd | |||
ba5d13936c | |||
23a6f76c37 | |||
0c9bc97fe8 | |||
c6ecfb2f67 | |||
8ca0814aff | |||
eceb4c3682 | |||
e7ecd5a5c2 | |||
f7c20a5517 | |||
6f409c0e3b |
30
CONTRIBUTING.md
Normal file
30
CONTRIBUTING.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Bugs
|
||||
* Include version (Setting > About > Version)
|
||||
* If not latest, try updating, it may have already been solved
|
||||
* Debug version is equal to the number of commits as seen in the main page
|
||||
* Include steps to reproduce (if not obvious from description)
|
||||
* Include screenshot (if needed)
|
||||
* If it could be device-dependent, try reproducing on another device (if possible), include results and device names, OS, modifications (root, Xposed)
|
||||
* **Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened [issues](https://github.com/inorichi/tachiyomi/issues).**
|
||||
* For large logs use http://pastebin.com/ (or similar)
|
||||
* For multipart issues use list like this:
|
||||
* [x] Done
|
||||
* [ ] Not done
|
||||
```
|
||||
* [x] Done
|
||||
* [ ] Not done
|
||||
```
|
||||
|
||||
DO: https://github.com/inorichi/tachiyomi/issues/24 https://github.com/inorichi/tachiyomi/issues/71
|
||||
|
||||
DON'T: https://github.com/inorichi/tachiyomi/issues/75
|
||||
|
||||
# Feature requests
|
||||
|
||||
* Write a detailed issue, explaning what it should do or how. Avoid writing just "like X app does"
|
||||
* Include screenshot (if needed)
|
||||
|
||||
# Translations
|
||||
|
||||
File `app/src/main/res/values/strings.xml` should be copied over to appropriate directories and then translated.
|
||||
Consult [Android.com](http://developer.android.com/training/basics/supporting-devices/languages.html#CreateDirs)
|
13
README.md
13
README.md
@ -1,8 +1,14 @@
|
||||
[](https://github.com/inorichi/tachiyomi/releases)
|
||||
[](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi)
|
||||
[](http://tachiyomi.kanade.eu/latest/app-debug.apk)
|
||||
|
||||
Tachiyomi is a free and open source manga reader for Android.
|
||||
|
||||
Keep in mind it's still a beta, so expect it to crash sometimes.
|
||||
|
||||
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
|
||||
|
||||
[](https://github.com/inorichi/tachiyomi/releases/download/v0.1.2/tachiyomi-v0.1.2.apk)
|
||||
[](http://tachiyomi.kanade.eu/latest/app-debug.apk)
|
||||
|
||||
## License
|
||||
|
||||
Copyright 2015 Javier Tomás
|
||||
|
@ -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"
|
||||
|
6
app/proguard-rules.pro
vendored
6
app/proguard-rules.pro
vendored
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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)
|
||||
|
@ -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!
|
||||
}
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
|
110
app/src/main/java/eu/kanade/tachiyomi/data/io/IOHandler.java
Normal file
110
app/src/main/java/eu/kanade/tachiyomi/data/io/IOHandler.java
Normal file
@ -0,0 +1,110 @@
|
||||
package eu.kanade.tachiyomi.data.io;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.DocumentsContract;
|
||||
import android.provider.MediaStore;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileDescriptor;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
|
||||
public class IOHandler {
|
||||
/**
|
||||
* Get full filepath of build in Android File picker.
|
||||
* If Google Drive (or other Cloud service) throw exception and download before loading
|
||||
*/
|
||||
public static String getFilePath(Uri uri, ContentResolver resolver, Context context) {
|
||||
try {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
String filePath = "";
|
||||
String wholeID = DocumentsContract.getDocumentId(uri);
|
||||
|
||||
//Ugly work around. In sdk version Kitkat or higher external getDocumentId request will have no content://
|
||||
if (wholeID.split(":").length == 1)
|
||||
throw new IllegalArgumentException();
|
||||
|
||||
// Split at colon, use second item in the array
|
||||
String id = wholeID.split(":")[1];
|
||||
|
||||
String[] column = {MediaStore.Images.Media.DATA};
|
||||
|
||||
// where id is equal to
|
||||
String sel = MediaStore.Images.Media._ID + "=?";
|
||||
|
||||
Cursor cursor = context.getContentResolver().query(MediaStore.Images.Media.EXTERNAL_CONTENT_URI,
|
||||
column, sel, new String[]{id}, null);
|
||||
|
||||
int columnIndex = cursor != null ? cursor.getColumnIndex(column[0]) : 0;
|
||||
|
||||
if (cursor != null ? cursor.moveToFirst() : false) {
|
||||
filePath = cursor.getString(columnIndex);
|
||||
}
|
||||
cursor.close();
|
||||
return filePath;
|
||||
} else {
|
||||
String[] fields = {MediaStore.Images.Media.DATA};
|
||||
|
||||
Cursor cursor = resolver.query(uri, fields, null, null, null);
|
||||
|
||||
if (cursor == null)
|
||||
return null;
|
||||
|
||||
cursor.moveToFirst();
|
||||
String path = cursor.getString(cursor.getColumnIndex(MediaStore.Images.Media.DATA));
|
||||
cursor.close();
|
||||
|
||||
return path;
|
||||
}
|
||||
} catch (IllegalArgumentException e) {
|
||||
//This exception is thrown when Google Drive. Try to download file
|
||||
return downloadMediaAndReturnPath(uri, resolver, context);
|
||||
}
|
||||
}
|
||||
|
||||
private static String getTempFilename(Context context) throws IOException {
|
||||
File outputDir = context.getCacheDir();
|
||||
File outputFile = File.createTempFile("temp_cover", "0", outputDir);
|
||||
return outputFile.getAbsolutePath();
|
||||
}
|
||||
|
||||
private static String downloadMediaAndReturnPath(Uri uri, ContentResolver resolver, Context context) {
|
||||
if (uri == null) return null;
|
||||
FileInputStream input = null;
|
||||
FileOutputStream output = null;
|
||||
try {
|
||||
ParcelFileDescriptor pfd = resolver.openFileDescriptor(uri, "r");
|
||||
FileDescriptor fd = pfd != null ? pfd.getFileDescriptor() : null;
|
||||
input = new FileInputStream(fd);
|
||||
|
||||
String tempFilename = getTempFilename(context);
|
||||
output = new FileOutputStream(tempFilename);
|
||||
|
||||
int read;
|
||||
byte[] bytes = new byte[4096];
|
||||
while ((read = input.read(bytes)) != -1) {
|
||||
output.write(bytes, 0, read);
|
||||
}
|
||||
return tempFilename;
|
||||
} catch (IOException ignored) {
|
||||
} finally {
|
||||
if (input != null) try {
|
||||
input.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
if (output != null) try {
|
||||
output.close();
|
||||
} catch (Exception ignored) {
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -1,8 +1,7 @@
|
||||
package eu.kanade.tachiyomi.data.mangasync.base;
|
||||
|
||||
import com.squareup.okhttp.Response;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import okhttp3.Response;
|
||||
import rx.Observable;
|
||||
|
||||
public abstract class MangaSyncService {
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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() {
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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) {
|
||||
|
@ -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 {
|
||||
|
@ -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", "");
|
||||
|
@ -59,7 +59,6 @@ public interface AppComponent {
|
||||
void inject(LibraryUpdateService libraryUpdateService);
|
||||
void inject(DownloadService downloadService);
|
||||
void inject(UpdateMangaSyncService updateMangaSyncService);
|
||||
|
||||
Application application();
|
||||
|
||||
}
|
||||
|
@ -47,8 +47,8 @@ public class DataModule {
|
||||
|
||||
@Provides
|
||||
@Singleton
|
||||
NetworkHelper provideNetworkHelper() {
|
||||
return new NetworkHelper();
|
||||
NetworkHelper provideNetworkHelper(Application app) {
|
||||
return new NetworkHelper(app);
|
||||
}
|
||||
|
||||
@Provides
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -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));
|
||||
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -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())
|
||||
|
@ -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() {
|
||||
|
@ -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
|
||||
|
@ -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();
|
||||
}
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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()));
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
@ -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);
|
||||
|
BIN
app/src/main/res/drawable-hdpi/ic_zoom_out_map_white_24dp.png
Normal file
BIN
app/src/main/res/drawable-hdpi/ic_zoom_out_map_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 326 B |
BIN
app/src/main/res/drawable-mdpi/ic_zoom_out_map_white_24dp.png
Normal file
BIN
app/src/main/res/drawable-mdpi/ic_zoom_out_map_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 235 B |
BIN
app/src/main/res/drawable-xhdpi/ic_zoom_out_map_white_24dp.png
Normal file
BIN
app/src/main/res/drawable-xhdpi/ic_zoom_out_map_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 332 B |
BIN
app/src/main/res/drawable-xxhdpi/ic_zoom_out_map_white_24dp.png
Normal file
BIN
app/src/main/res/drawable-xxhdpi/ic_zoom_out_map_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 510 B |
BIN
app/src/main/res/drawable-xxxhdpi/ic_zoom_out_map_white_24dp.png
Normal file
BIN
app/src/main/res/drawable-xxxhdpi/ic_zoom_out_map_white_24dp.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 584 B |
@ -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>
|
@ -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
|
||||
|
@ -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>
|
@ -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"
|
||||
|
@ -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>
|
9
app/src/main/res/menu/chapters.xml
Normal file
9
app/src/main/res/menu/chapters.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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>
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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>
|
||||
|
@ -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"
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
Reference in New Issue
Block a user