Compare commits
76 Commits
Author | SHA1 | Date | |
---|---|---|---|
110df59197 | |||
75ae4081d8 | |||
2efca050b3 | |||
37e119c4f2 | |||
9786074119 | |||
d4876b426f | |||
8581e4667a | |||
b94f86765d | |||
ba0f3778ce | |||
aac6b242a0 | |||
dec9442a65 | |||
b33da641d9 | |||
96d498e7e5 | |||
eee137a084 | |||
5e834ae3be | |||
dcfda61aba | |||
5ac7f7057a | |||
ff46c61f63 | |||
57b64a412e | |||
1e81f75377 | |||
1dd49a2ab1 | |||
1cd77a97a7 | |||
f820522a69 | |||
3da613dedb | |||
5c329d2314 | |||
4c073e713d | |||
2832f4ae5e | |||
7690e8a53f | |||
3a19f8e40b | |||
a33b525f9e | |||
7d6ce46829 | |||
a90a4bf80c | |||
140bf8caee | |||
56a45f263e | |||
01d6ddfafb | |||
393b4916f6 | |||
cb3c3af865 | |||
5a83976fa5 | |||
a81f6c3ac4 | |||
6846ce5bfb | |||
0c0ebe06e5 | |||
e50c683159 | |||
872af276ea | |||
e6faee9779 | |||
bc1ddd4379 | |||
e348d6c1cf | |||
7835921045 | |||
1611a274b9 | |||
fa4a8204a4 | |||
5977e9f47f | |||
63d0161da5 | |||
d8b46c1969 | |||
f84731c2df | |||
50d71d1395 | |||
4be0b2502e | |||
6c069ad87b | |||
e69011ac5b | |||
ea130a0899 | |||
2566862e0f | |||
16081817c2 | |||
945625d3ad | |||
050b9c9fce | |||
c35184abdc | |||
34c5f0b7ba | |||
6435eeb251 | |||
eec2dcd981 | |||
57ba368ae0 | |||
ed06469885 | |||
79cd8c691e | |||
391550f49a | |||
6aa07dd17e | |||
aada373a0c | |||
3deac86bbe | |||
d7aef2e97a | |||
7953ba6e78 | |||
8aa3c2a260 |
6
.github/ISSUE_TEMPLATE.md
vendored
Normal file
@ -0,0 +1,6 @@
|
||||
**Please read https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md before posting**
|
||||
|
||||
Remove line above and describe your issue here. Fill out version below.
|
||||
|
||||
---
|
||||
Version: r000 or v0.0.0
|
@ -1,13 +1,15 @@
|
||||
[](https://github.com/inorichi/tachiyomi/releases)
|
||||
[](https://github.com/inorichi/tachiyomi/releases)
|
||||
[](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi)
|
||||
[](http://tachiyomi.kanade.eu/latest/app-debug.apk)
|
||||
|
||||
## [Report an issue](https://github.com/inorichi/tachiyomi/blob/master/.github/CONTRIBUTING.md)
|
||||
|
||||
**Before reporting a new issue, take a look at the [FAQ](https://github.com/inorichi/tachiyomi/wiki/FAQ), the [changelog](https://github.com/inorichi/tachiyomi/releases) and the already opened issues.**
|
||||
|
||||
Tachiyomi is a free and open source manga reader for Android.
|
||||
|
||||
Keep in mind it's still a beta, so expect it to crash sometimes.
|
||||
|
||||
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
|
||||
|
@ -1,6 +1,5 @@
|
||||
import java.text.SimpleDateFormat
|
||||
|
||||
apply plugin: 'android-sdk-manager'
|
||||
apply plugin: 'com.android.application'
|
||||
apply plugin: 'com.neenbedankt.android-apt'
|
||||
apply plugin: 'me.tatarka.retrolambda'
|
||||
@ -29,6 +28,10 @@ ext {
|
||||
}
|
||||
}
|
||||
|
||||
def includeUpdater() {
|
||||
return hasProperty("include_updater");
|
||||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 23
|
||||
buildToolsVersion "23.0.2"
|
||||
@ -39,12 +42,13 @@ android {
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 23
|
||||
testInstrumentationRunner "android.support.test.runner.AndroidJUnitRunner"
|
||||
versionCode 4
|
||||
versionName "0.1.3"
|
||||
versionCode 5
|
||||
versionName "0.1.4"
|
||||
|
||||
buildConfigField "String", "COMMIT_COUNT", "\"${getCommitCount()}\""
|
||||
buildConfigField "String", "COMMIT_SHA", "\"${getGitSha()}\""
|
||||
buildConfigField "String", "BUILD_TIME", "\"${getBuildTime()}\""
|
||||
buildConfigField "boolean", "INCLUDE_UPDATER", "${includeUpdater()}"
|
||||
}
|
||||
|
||||
compileOptions {
|
||||
@ -78,16 +82,24 @@ android {
|
||||
|
||||
}
|
||||
|
||||
apt {
|
||||
arguments {
|
||||
eventBusIndex "eu.kanade.tachiyomi.EventBusIndex"
|
||||
}
|
||||
}
|
||||
|
||||
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 EVENTBUS_VERSION = '3.0.0'
|
||||
final OKHTTP_VERSION = '3.1.1'
|
||||
final STORIO_VERSION = '1.8.0'
|
||||
final ICEPICK_VERSION = '3.1.0'
|
||||
final MOCKITO_VERSION = '1.10.19'
|
||||
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
compile project(":SubsamplingScaleImageView")
|
||||
compile project(":ReactiveNetwork")
|
||||
|
||||
compile "com.android.support:support-v4:$SUPPORT_LIBRARY_VERSION"
|
||||
compile "com.android.support:appcompat-v7:$SUPPORT_LIBRARY_VERSION"
|
||||
@ -104,23 +116,24 @@ dependencies {
|
||||
compile 'org.jsoup:jsoup:1.8.3'
|
||||
compile 'io.reactivex:rxandroid:1.1.0'
|
||||
compile 'io.reactivex:rxjava:1.1.0'
|
||||
compile 'com.squareup.retrofit:retrofit:1.9.0'
|
||||
compile 'com.f2prateek.rx.preferences:rx-preferences:1.0.1'
|
||||
compile "com.pushtorefresh.storio:sqlite:$STORIO_VERSION"
|
||||
compile "com.pushtorefresh.storio:sqlite-annotations:$STORIO_VERSION"
|
||||
compile 'info.android15.nucleus:nucleus:2.0.4'
|
||||
compile 'de.greenrobot:eventbus:2.4.0'
|
||||
compile 'com.github.bumptech.glide:glide:3.6.1'
|
||||
compile 'com.jakewharton:butterknife:7.0.1'
|
||||
compile 'com.jakewharton.timber:timber:4.1.0'
|
||||
compile 'uk.co.ribot:easyadapter:1.5.0@aar'
|
||||
compile 'ch.acra:acra:4.7.0'
|
||||
compile 'ch.acra:acra:4.8.1'
|
||||
compile "frankiesardo:icepick:$ICEPICK_VERSION"
|
||||
provided "frankiesardo:icepick-processor:$ICEPICK_VERSION"
|
||||
compile 'com.github.dmytrodanylyk.android-process-button:library:1.0.4'
|
||||
compile 'eu.davidea:flexible-adapter:4.2.0'
|
||||
compile 'com.nononsenseapps:filepicker:2.5.1'
|
||||
compile 'com.github.amulyakhare:TextDrawable:558677e'
|
||||
compile 'com.github.pwittchen:reactivenetwork:0.1.5'
|
||||
|
||||
compile "org.greenrobot:eventbus:$EVENTBUS_VERSION"
|
||||
apt "org.greenrobot:eventbus-annotation-processor:$EVENTBUS_VERSION"
|
||||
|
||||
compile "com.google.dagger:dagger:$DAGGER_VERSION"
|
||||
apt "com.google.dagger:dagger-compiler:$DAGGER_VERSION"
|
||||
@ -131,10 +144,10 @@ dependencies {
|
||||
transitive = true
|
||||
}
|
||||
|
||||
//Google material icons SVG.
|
||||
// Google material icons SVG.
|
||||
compile 'com.mikepenz:google-material-typeface:2.1.0.1.original@aar'
|
||||
|
||||
compile('com.github.afollestad.material-dialogs:core:0.8.5.3@aar') {
|
||||
compile('com.github.afollestad.material-dialogs:core:0.8.5.4@aar') {
|
||||
transitive = true
|
||||
}
|
||||
|
||||
|
34
app/proguard-rules.pro
vendored
@ -39,14 +39,17 @@
|
||||
}
|
||||
|
||||
## GreenRobot EventBus specific rules ##
|
||||
# https://github.com/greenrobot/EventBus/blob/master/HOWTO.md#proguard-configuration
|
||||
# http://greenrobot.org/eventbus/documentation/proguard/
|
||||
-keepattributes *Annotation*
|
||||
-keepclassmembers class ** {
|
||||
public void onEvent*(***);
|
||||
@org.greenrobot.eventbus.Subscribe <methods>;
|
||||
}
|
||||
-keep enum org.greenrobot.eventbus.ThreadMode { *; }
|
||||
|
||||
# Don't warn for missing support classes
|
||||
-dontwarn de.greenrobot.event.util.*$Support
|
||||
-dontwarn de.greenrobot.event.util.*$SupportManagerFragment
|
||||
# Only required if you use AsyncExecutor
|
||||
-keepclassmembers class * extends org.greenrobot.eventbus.util.ThrowableFailureEvent {
|
||||
<init>(java.lang.Throwable);
|
||||
}
|
||||
|
||||
# Glide specific rules #
|
||||
# https://github.com/bumptech/glide
|
||||
@ -73,6 +76,27 @@
|
||||
rx.internal.util.atomic.LinkedQueueNode consumerNode;
|
||||
}
|
||||
|
||||
# Retrofit 1.X
|
||||
|
||||
-keep class com.squareup.okhttp.** { *; }
|
||||
-keep class retrofit.** { *; }
|
||||
-keep interface com.squareup.okhttp.** { *; }
|
||||
|
||||
-dontwarn com.squareup.okhttp.**
|
||||
-dontwarn okio.**
|
||||
-dontwarn retrofit.**
|
||||
-dontwarn rx.**
|
||||
|
||||
-keepclasseswithmembers class * {
|
||||
@retrofit.http.* <methods>;
|
||||
}
|
||||
|
||||
# If in your rest service interface you use methods with Callback argument.
|
||||
-keepattributes Exceptions
|
||||
|
||||
# If your rest service methods throw custom exceptions, because you've defined an ErrorHandler.
|
||||
-keepattributes Signature
|
||||
|
||||
# AppCombat
|
||||
-keep public class android.support.v7.widget.** { *; }
|
||||
-keep public class android.support.v7.internal.widget.** { *; }
|
||||
|
@ -5,6 +5,7 @@ import android.content.Context;
|
||||
|
||||
import org.acra.ACRA;
|
||||
import org.acra.annotation.ReportsCrashes;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import eu.kanade.tachiyomi.injection.ComponentReflectionInjector;
|
||||
import eu.kanade.tachiyomi.injection.component.AppComponent;
|
||||
@ -23,6 +24,10 @@ public class App extends Application {
|
||||
AppComponent applicationComponent;
|
||||
ComponentReflectionInjector<AppComponent> componentInjector;
|
||||
|
||||
public static App get(Context context) {
|
||||
return (App) context.getApplicationContext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate() {
|
||||
super.onCreate();
|
||||
@ -35,23 +40,28 @@ public class App extends Application {
|
||||
componentInjector =
|
||||
new ComponentReflectionInjector<>(AppComponent.class, applicationComponent);
|
||||
|
||||
setupEventBus();
|
||||
|
||||
ACRA.init(this);
|
||||
}
|
||||
|
||||
public static App get(Context context) {
|
||||
return (App) context.getApplicationContext();
|
||||
protected void setupEventBus() {
|
||||
EventBus.builder()
|
||||
.addIndex(new EventBusIndex())
|
||||
.logNoSubscriberMessages(false)
|
||||
.installDefaultEventBus();
|
||||
}
|
||||
|
||||
public AppComponent getComponent() {
|
||||
return applicationComponent;
|
||||
}
|
||||
|
||||
public ComponentReflectionInjector<AppComponent> getComponentReflection() {
|
||||
return componentInjector;
|
||||
}
|
||||
|
||||
// Needed to replace the component with a test specific one
|
||||
public void setComponent(AppComponent applicationComponent) {
|
||||
this.applicationComponent = applicationComponent;
|
||||
}
|
||||
|
||||
public ComponentReflectionInjector<AppComponent> getComponentReflection() {
|
||||
return componentInjector;
|
||||
}
|
||||
}
|
||||
|
@ -19,6 +19,7 @@ import com.pushtorefresh.storio.sqlite.queries.RawQuery;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.TreeSet;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.data.database.models.CategorySQLiteTypeMapping;
|
||||
@ -259,21 +260,32 @@ public class DatabaseHelper {
|
||||
int deleted = 0;
|
||||
db.internal().beginTransaction();
|
||||
try {
|
||||
TreeSet<Float> deletedReadChapterNumbers = new TreeSet<>();
|
||||
if (!toDelete.isEmpty()) {
|
||||
for (Chapter c : toDelete) {
|
||||
if (c.read) {
|
||||
deletedReadChapterNumbers.add(c.chapter_number);
|
||||
}
|
||||
}
|
||||
deleted = deleteChapters(toDelete).executeAsBlocking().results().size();
|
||||
}
|
||||
|
||||
if (!toAdd.isEmpty()) {
|
||||
// Set the date fetch for new items in reverse order to allow another sorting method.
|
||||
// Sources MUST return the chapters from most to less recent, which is common.
|
||||
long now = new Date().getTime();
|
||||
|
||||
for (int i = toAdd.size() - 1; i >= 0; i--) {
|
||||
toAdd.get(i).date_fetch = now++;
|
||||
Chapter c = toAdd.get(i);
|
||||
c.date_fetch = now++;
|
||||
// Try to mark already read chapters as read when the source deletes them
|
||||
if (c.chapter_number != -1 && deletedReadChapterNumbers.contains(c.chapter_number)) {
|
||||
c.read = true;
|
||||
}
|
||||
}
|
||||
added = insertChapters(toAdd).executeAsBlocking().numberOfInserts();
|
||||
}
|
||||
|
||||
if (!toDelete.isEmpty()) {
|
||||
deleted = deleteChapters(toDelete).executeAsBlocking().results().size();
|
||||
}
|
||||
|
||||
db.internal().setTransactionSuccessful();
|
||||
} finally {
|
||||
db.internal().endTransaction();
|
||||
|
@ -4,8 +4,11 @@ import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteColumn;
|
||||
import com.pushtorefresh.storio.sqlite.annotations.StorIOSQLiteType;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.tables.ChapterTable;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.util.UrlUtil;
|
||||
|
||||
@StorIOSQLiteType(table = ChapterTable.TABLE)
|
||||
@ -40,6 +43,8 @@ public class Chapter implements Serializable {
|
||||
|
||||
public int status;
|
||||
|
||||
private transient List<Page> pages;
|
||||
|
||||
public Chapter() {}
|
||||
|
||||
public void setUrl(String url) {
|
||||
@ -68,4 +73,15 @@ public class Chapter implements Serializable {
|
||||
return chapter;
|
||||
}
|
||||
|
||||
public List<Page> getPages() {
|
||||
return pages;
|
||||
}
|
||||
|
||||
public void setPages(List<Page> pages) {
|
||||
this.pages = pages;
|
||||
}
|
||||
|
||||
public boolean isDownloaded() {
|
||||
return status == Download.DOWNLOADED;
|
||||
}
|
||||
}
|
||||
|
@ -84,7 +84,7 @@ public class DownloadManager {
|
||||
if (finished) {
|
||||
DownloadService.stop(context);
|
||||
}
|
||||
}, e -> Timber.e(e.getCause(), e.getMessage()));
|
||||
}, e -> DownloadService.stop(context));
|
||||
|
||||
if (!isRunning) {
|
||||
isRunning = true;
|
||||
|
@ -8,14 +8,16 @@ import android.os.PowerManager;
|
||||
|
||||
import com.github.pwittchen.reactivenetwork.library.ReactiveNetwork;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
@ -47,7 +49,7 @@ public class DownloadService extends Service {
|
||||
createWakeLock();
|
||||
|
||||
listenQueueRunningChanges();
|
||||
EventBus.getDefault().registerSticky(this);
|
||||
EventBus.getDefault().register(this);
|
||||
listenNetworkChanges();
|
||||
}
|
||||
|
||||
@ -71,7 +73,7 @@ public class DownloadService extends Service {
|
||||
return null;
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(DownloadChaptersEvent event) {
|
||||
EventBus.getDefault().removeStickyEvent(event);
|
||||
downloadManager.onDownloadChaptersEvent(event);
|
||||
|
@ -83,7 +83,7 @@ public class MyAnimeList extends MangaSyncService {
|
||||
|
||||
public Observable<Boolean> login(String username, String password) {
|
||||
createHeaders(username, password);
|
||||
return networkService.getResponse(getLoginUrl(), headers, null)
|
||||
return networkService.getResponse(getLoginUrl(), headers, false)
|
||||
.map(response -> response.code() == 200);
|
||||
}
|
||||
|
||||
@ -101,7 +101,7 @@ public class MyAnimeList extends MangaSyncService {
|
||||
}
|
||||
|
||||
public Observable<List<MangaSync>> search(String query) {
|
||||
return networkService.getStringResponse(getSearchUrl(query), headers, null)
|
||||
return networkService.getStringResponse(getSearchUrl(query), headers, true)
|
||||
.map(Jsoup::parse)
|
||||
.flatMap(doc -> Observable.from(doc.select("entry")))
|
||||
.filter(entry -> !entry.select("type").text().equals("Novel"))
|
||||
@ -126,7 +126,7 @@ public class MyAnimeList extends MangaSyncService {
|
||||
|
||||
public Observable<List<MangaSync>> getList() {
|
||||
// TODO cache this list for a few minutes
|
||||
return networkService.getStringResponse(getListUrl(username), headers, null)
|
||||
return networkService.getStringResponse(getListUrl(username), headers, true)
|
||||
.map(Jsoup::parse)
|
||||
.flatMap(doc -> Observable.from(doc.select("manga")))
|
||||
.map(entry -> {
|
||||
|
@ -7,11 +7,13 @@ import java.io.File;
|
||||
import java.net.CookieManager;
|
||||
import java.net.CookiePolicy;
|
||||
import java.net.CookieStore;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import okhttp3.Cache;
|
||||
import okhttp3.CacheControl;
|
||||
import okhttp3.FormBody;
|
||||
import okhttp3.Headers;
|
||||
import okhttp3.Interceptor;
|
||||
import okhttp3.JavaNetCookieJar;
|
||||
import okhttp3.OkHttpClient;
|
||||
import okhttp3.Request;
|
||||
@ -22,12 +24,23 @@ import rx.Observable;
|
||||
public final class NetworkHelper {
|
||||
|
||||
private OkHttpClient client;
|
||||
private OkHttpClient forceCacheClient;
|
||||
|
||||
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 FormBody.Builder().build();
|
||||
public final CacheControl CACHE_CONTROL = new CacheControl.Builder()
|
||||
.maxAge(10, TimeUnit.MINUTES)
|
||||
.build();
|
||||
|
||||
private static final Interceptor REWRITE_CACHE_CONTROL_INTERCEPTOR = chain -> {
|
||||
Response originalResponse = chain.proceed(chain.request());
|
||||
return originalResponse.newBuilder()
|
||||
.removeHeader("Pragma")
|
||||
.header("Cache-Control", "max-age=" + 600)
|
||||
.build();
|
||||
};
|
||||
|
||||
private static final int CACHE_SIZE = 5 * 1024 * 1024; // 5 MiB
|
||||
private static final String CACHE_DIR_NAME = "network_cache";
|
||||
@ -42,18 +55,24 @@ public final class NetworkHelper {
|
||||
.cookieJar(new JavaNetCookieJar(cookieManager))
|
||||
.cache(new Cache(cacheDir, CACHE_SIZE))
|
||||
.build();
|
||||
|
||||
forceCacheClient = client.newBuilder()
|
||||
.addNetworkInterceptor(REWRITE_CACHE_CONTROL_INTERCEPTOR)
|
||||
.build();
|
||||
}
|
||||
|
||||
public Observable<Response> getResponse(final String url, final Headers headers, final CacheControl cacheControl) {
|
||||
public Observable<Response> getResponse(final String url, final Headers headers, boolean forceCache) {
|
||||
return Observable.defer(() -> {
|
||||
try {
|
||||
OkHttpClient c = forceCache ? forceCacheClient : client;
|
||||
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.cacheControl(cacheControl != null ? cacheControl : NULL_CACHE_CONTROL)
|
||||
.headers(headers != null ? headers : NULL_HEADERS)
|
||||
.cacheControl(CACHE_CONTROL)
|
||||
.build();
|
||||
|
||||
return Observable.just(client.newCall(request).execute());
|
||||
return Observable.just(c.newCall(request).execute());
|
||||
} catch (Throwable e) {
|
||||
return Observable.error(e);
|
||||
}
|
||||
@ -70,8 +89,8 @@ public final class NetworkHelper {
|
||||
});
|
||||
}
|
||||
|
||||
public Observable<String> getStringResponse(final String url, final Headers headers, final CacheControl cacheControl) {
|
||||
return getResponse(url, headers, cacheControl)
|
||||
public Observable<String> getStringResponse(final String url, final Headers headers, boolean forceCache) {
|
||||
return getResponse(url, headers, forceCache)
|
||||
.flatMap(this::mapResponseToString);
|
||||
}
|
||||
|
||||
@ -95,7 +114,7 @@ public final class NetworkHelper {
|
||||
try {
|
||||
Request request = new Request.Builder()
|
||||
.url(url)
|
||||
.cacheControl(NULL_CACHE_CONTROL)
|
||||
.cacheControl(CacheControl.FORCE_NETWORK)
|
||||
.headers(headers != null ? headers : NULL_HEADERS)
|
||||
.build();
|
||||
|
||||
|
@ -42,10 +42,12 @@ public class PreferencesHelper {
|
||||
if (getDownloadsDirectory().equals(defaultDownloadsDir.getAbsolutePath()) &&
|
||||
!defaultDownloadsDir.exists()) {
|
||||
defaultDownloadsDir.mkdirs();
|
||||
try {
|
||||
new File(defaultDownloadsDir, ".nomedia").createNewFile();
|
||||
} catch (IOException e) { /* Ignore */ }
|
||||
}
|
||||
|
||||
// Don't display downloaded chapters in gallery apps creating a ".nomedia" file
|
||||
try {
|
||||
new File(getDownloadsDirectory(), ".nomedia").createNewFile();
|
||||
} catch (IOException e) { /* Ignore */ }
|
||||
}
|
||||
|
||||
private String getKey(int keyResource) {
|
||||
@ -56,8 +58,8 @@ public class PreferencesHelper {
|
||||
prefs.edit().clear().apply();
|
||||
}
|
||||
|
||||
public Preference<Boolean> lockOrientation() {
|
||||
return rxPrefs.getBoolean(getKey(R.string.pref_lock_orientation_key), true);
|
||||
public Preference<Integer> rotation() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_rotation_type_key), 1);
|
||||
}
|
||||
|
||||
public Preference<Boolean> enableTransitions() {
|
||||
@ -92,6 +94,18 @@ public class PreferencesHelper {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_image_scale_type_key), 1);
|
||||
}
|
||||
|
||||
public Preference<Integer> imageDecoder() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_image_decoder_key), 0);
|
||||
}
|
||||
|
||||
public Preference<Integer> zoomStart() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_zoom_start_key), 1);
|
||||
}
|
||||
|
||||
public Preference<Integer> readerTheme() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0);
|
||||
}
|
||||
|
||||
public Preference<Integer> portraitColumns() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_library_columns_portrait_key), 0);
|
||||
}
|
||||
@ -112,12 +126,12 @@ public class PreferencesHelper {
|
||||
return prefs.getBoolean(getKey(R.string.pref_ask_update_manga_sync_key), false);
|
||||
}
|
||||
|
||||
public Preference<Integer> imageDecoder() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_image_decoder_key), 0);
|
||||
public Preference<Integer> lastUsedCatalogueSource() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_last_catalogue_source_key), -1);
|
||||
}
|
||||
|
||||
public Preference<Integer> readerTheme() {
|
||||
return rxPrefs.getInteger(getKey(R.string.pref_reader_theme_key), 0);
|
||||
public boolean seamlessMode() {
|
||||
return prefs.getBoolean(getKey(R.string.pref_seamless_mode_key), true);
|
||||
}
|
||||
|
||||
public Preference<Boolean> catalogueAsList() {
|
||||
|
@ -0,0 +1,15 @@
|
||||
package eu.kanade.tachiyomi.data.rest;
|
||||
|
||||
import retrofit.http.GET;
|
||||
import rx.Observable;
|
||||
|
||||
|
||||
/**
|
||||
* Used to connect with the Github API
|
||||
*/
|
||||
public interface GithubService {
|
||||
String SERVICE_ENDPOINT = "https://api.github.com";
|
||||
|
||||
@GET("/repos/inorichi/tachiyomi/releases/latest") Observable<Release> getLatestVersion();
|
||||
|
||||
}
|
93
app/src/main/java/eu/kanade/tachiyomi/data/rest/Release.java
Normal file
@ -0,0 +1,93 @@
|
||||
package eu.kanade.tachiyomi.data.rest;
|
||||
|
||||
import com.google.gson.annotations.SerializedName;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Release object
|
||||
* Contains information about the latest release
|
||||
*/
|
||||
public class Release {
|
||||
/**
|
||||
* Version name V0.0.0
|
||||
*/
|
||||
@SerializedName("tag_name")
|
||||
private final String version;
|
||||
|
||||
/** Change Log */
|
||||
@SerializedName("body")
|
||||
private final String log;
|
||||
|
||||
/** Assets containing download url */
|
||||
@SerializedName("assets")
|
||||
private final List<Assets> assets;
|
||||
|
||||
/**
|
||||
* Release constructor
|
||||
*
|
||||
* @param version version of latest release
|
||||
* @param log log of latest release
|
||||
* @param assets assets of latest release
|
||||
*/
|
||||
public Release(String version, String log, List<Assets> assets) {
|
||||
this.version = version;
|
||||
this.log = log;
|
||||
this.assets = assets;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get latest release version
|
||||
*
|
||||
* @return latest release version
|
||||
*/
|
||||
public String getVersion() {
|
||||
return version;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get change log of latest release
|
||||
*
|
||||
* @return change log of latest release
|
||||
*/
|
||||
public String getChangeLog() {
|
||||
return log;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get download link of latest release
|
||||
*
|
||||
* @return download link of latest release
|
||||
*/
|
||||
public String getDownloadLink() {
|
||||
return assets.get(0).getDownloadLink();
|
||||
}
|
||||
|
||||
/**
|
||||
* Assets class containing download url
|
||||
*/
|
||||
class Assets {
|
||||
@SerializedName("browser_download_url")
|
||||
private final String download_url;
|
||||
|
||||
|
||||
/**
|
||||
* Assets Constructor
|
||||
*
|
||||
* @param download_url download url
|
||||
*/
|
||||
@SuppressWarnings("unused") public Assets(String download_url) {
|
||||
this.download_url = download_url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get download link of latest release
|
||||
*
|
||||
* @return download link of latest release
|
||||
*/
|
||||
public String getDownloadLink() {
|
||||
return download_url;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -0,0 +1,21 @@
|
||||
package eu.kanade.tachiyomi.data.rest;
|
||||
|
||||
import retrofit.RestAdapter;
|
||||
|
||||
public class ServiceFactory {
|
||||
|
||||
/**
|
||||
* Creates a retrofit service from an arbitrary class (clazz)
|
||||
*
|
||||
* @param clazz Java interface of the retrofit service
|
||||
* @param endPoint REST endpoint url
|
||||
* @return retrofit service with defined endpoint
|
||||
*/
|
||||
public static <T> T createRetrofitService(final Class<T> clazz, final String endPoint) {
|
||||
final RestAdapter restAdapter = new RestAdapter.Builder()
|
||||
.setEndpoint(endPoint)
|
||||
.build();
|
||||
|
||||
return restAdapter.create(clazz);
|
||||
}
|
||||
}
|
@ -53,7 +53,7 @@ public abstract class Source extends BaseSource {
|
||||
page.url = getInitialPopularMangasUrl();
|
||||
|
||||
return networkService
|
||||
.getStringResponse(page.url, requestHeaders, null)
|
||||
.getStringResponse(page.url, requestHeaders, true)
|
||||
.map(Jsoup::parse)
|
||||
.doOnNext(doc -> page.mangas = parsePopularMangasFromHtml(doc))
|
||||
.doOnNext(doc -> page.nextPageUrl = parseNextPopularMangasUrl(doc, page))
|
||||
@ -66,7 +66,7 @@ public abstract class Source extends BaseSource {
|
||||
page.url = getInitialSearchUrl(query);
|
||||
|
||||
return networkService
|
||||
.getStringResponse(page.url, requestHeaders, null)
|
||||
.getStringResponse(page.url, requestHeaders, true)
|
||||
.map(Jsoup::parse)
|
||||
.doOnNext(doc -> page.mangas = parseSearchFromHtml(doc))
|
||||
.doOnNext(doc -> page.nextPageUrl = parseNextSearchUrl(doc, page, query))
|
||||
@ -76,14 +76,14 @@ public abstract class Source extends BaseSource {
|
||||
// Get manga details from the source
|
||||
public Observable<Manga> pullMangaFromNetwork(final String mangaUrl) {
|
||||
return networkService
|
||||
.getStringResponse(getBaseUrl() + overrideMangaUrl(mangaUrl), requestHeaders, null)
|
||||
.getStringResponse(getBaseUrl() + overrideMangaUrl(mangaUrl), requestHeaders, true)
|
||||
.flatMap(unparsedHtml -> Observable.just(parseHtmlToManga(mangaUrl, unparsedHtml)));
|
||||
}
|
||||
|
||||
// Get chapter list of a manga from the source
|
||||
public Observable<List<Chapter>> pullChaptersFromNetwork(final String mangaUrl) {
|
||||
return networkService
|
||||
.getStringResponse(getBaseUrl() + mangaUrl, requestHeaders, null)
|
||||
.getStringResponse(getBaseUrl() + mangaUrl, requestHeaders, false)
|
||||
.flatMap(unparsedHtml -> {
|
||||
List<Chapter> chapters = parseHtmlToChapters(unparsedHtml);
|
||||
return !chapters.isEmpty() ?
|
||||
@ -102,7 +102,7 @@ public abstract class Source extends BaseSource {
|
||||
|
||||
public Observable<List<Page>> pullPageListFromNetwork(final String chapterUrl) {
|
||||
return networkService
|
||||
.getStringResponse(getBaseUrl() + overrideChapterUrl(chapterUrl), requestHeaders, null)
|
||||
.getStringResponse(getBaseUrl() + overrideChapterUrl(chapterUrl), requestHeaders, false)
|
||||
.flatMap(unparsedHtml -> {
|
||||
List<Page> pages = convertToPages(parseHtmlToPageUrls(unparsedHtml));
|
||||
return !pages.isEmpty() ?
|
||||
@ -127,7 +127,7 @@ public abstract class Source extends BaseSource {
|
||||
public Observable<Page> getImageUrlFromPage(final Page page) {
|
||||
page.setStatus(Page.LOAD_PAGE);
|
||||
return networkService
|
||||
.getStringResponse(overridePageUrl(page.getUrl()), requestHeaders, null)
|
||||
.getStringResponse(overridePageUrl(page.getUrl()), requestHeaders, false)
|
||||
.flatMap(unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml)))
|
||||
.onErrorResumeNext(e -> {
|
||||
page.setStatus(Page.ERROR);
|
||||
|
@ -1,5 +1,9 @@
|
||||
package eu.kanade.tachiyomi.data.source.model;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.network.ProgressListener;
|
||||
import rx.subjects.PublishSubject;
|
||||
|
||||
@ -8,6 +12,7 @@ public class Page implements ProgressListener {
|
||||
private int pageNumber;
|
||||
private String url;
|
||||
private String imageUrl;
|
||||
private transient Chapter chapter;
|
||||
private transient String imagePath;
|
||||
private transient volatile int status;
|
||||
private transient volatile int progress;
|
||||
@ -82,4 +87,16 @@ public class Page implements ProgressListener {
|
||||
this.statusSubject = subject;
|
||||
}
|
||||
|
||||
public Chapter getChapter() {
|
||||
return chapter;
|
||||
}
|
||||
|
||||
public void setChapter(Chapter chapter) {
|
||||
this.chapter = chapter;
|
||||
}
|
||||
|
||||
public boolean isLastPage() {
|
||||
List<Page> chapterPages = chapter.getPages();
|
||||
return chapterPages.size() -1 == pageNumber;
|
||||
}
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.data.source.online.english;
|
||||
|
||||
import android.content.Context;
|
||||
import android.net.Uri;
|
||||
import android.text.Html;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.jsoup.Jsoup;
|
||||
@ -47,6 +48,8 @@ public class Batoto extends LoginSource {
|
||||
public static final String MANGA_URL = "/comic_pop?id=%s";
|
||||
public static final String LOGIN_URL = BASE_URL + "/forums/index.php?app=core&module=global§ion=login";
|
||||
|
||||
public static final Pattern staffNotice = Pattern.compile("=+Batoto Staff Notice=+([^=]+)==+", Pattern.CASE_INSENSITIVE);
|
||||
|
||||
private Pattern datePattern;
|
||||
private Map<String, Integer> dateFields;
|
||||
|
||||
@ -204,6 +207,12 @@ public class Batoto extends LoginSource {
|
||||
|
||||
@Override
|
||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||
Matcher matcher = staffNotice.matcher(unparsedHtml);
|
||||
if (matcher.find()) {
|
||||
String notice = Html.fromHtml(matcher.group(1)).toString().trim();
|
||||
throw new RuntimeException(notice);
|
||||
}
|
||||
|
||||
Document parsedDocument = Jsoup.parse(unparsedHtml);
|
||||
|
||||
List<Chapter> chapterList = new ArrayList<>();
|
||||
@ -234,6 +243,7 @@ public class Batoto extends LoginSource {
|
||||
return chapter;
|
||||
}
|
||||
|
||||
@SuppressWarnings("WrongConstant")
|
||||
private long parseDateFromElement(Element dateElement) {
|
||||
String dateAsString = dateElement.text();
|
||||
|
||||
@ -249,7 +259,6 @@ public class Batoto extends LoginSource {
|
||||
String unit = m.group(2);
|
||||
|
||||
Calendar cal = Calendar.getInstance();
|
||||
// Not an error
|
||||
cal.add(dateFields.get(unit), -amount);
|
||||
date = cal.getTime();
|
||||
} else {
|
||||
@ -309,7 +318,7 @@ public class Batoto extends LoginSource {
|
||||
|
||||
@Override
|
||||
public Observable<Boolean> login(String username, String password) {
|
||||
return networkService.getStringResponse(LOGIN_URL, requestHeaders, null)
|
||||
return networkService.getStringResponse(LOGIN_URL, requestHeaders, false)
|
||||
.flatMap(response -> doLogin(response, username, password))
|
||||
.map(this::isAuthenticationSuccessful);
|
||||
}
|
||||
|
@ -29,7 +29,7 @@ public class Mangafox extends Source {
|
||||
public static final String BASE_URL = "http://mangafox.me";
|
||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
|
||||
public static final String SEARCH_URL =
|
||||
BASE_URL + "/search.php?name_method=cw&advopts=1&order=az&sort=name&name=%s&page=%s";
|
||||
BASE_URL + "/search.php?name_method=cw&advopts=1&order=za&sort=views&name=%s&page=%s";
|
||||
|
||||
public Mangafox(Context context) {
|
||||
super(context);
|
||||
|
@ -28,7 +28,7 @@ public class Mangahere extends Source {
|
||||
public static final String NAME = "Mangahere (EN)";
|
||||
public static final String BASE_URL = "http://www.mangahere.co";
|
||||
public static final String POPULAR_MANGAS_URL = BASE_URL + "/directory/%s";
|
||||
public static final String SEARCH_URL = BASE_URL + "/search.php?name=%s&page=%s";
|
||||
public static final String SEARCH_URL = BASE_URL + "/search.php?name=%s&page=%s&sort=views&order=za";
|
||||
|
||||
public Mangahere(Context context) {
|
||||
super(context);
|
||||
|
@ -111,7 +111,7 @@ public class LibraryUpdateService extends Service {
|
||||
.toList().toBlocking().single();
|
||||
|
||||
return Observable.from(mangas)
|
||||
.doOnNext(manga -> showNotification(
|
||||
.doOnNext(manga -> showProgressNotification(
|
||||
getString(R.string.notification_update_progress,
|
||||
count.incrementAndGet(), mangas.size()), manga.title))
|
||||
.concatMap(manga -> updateManga(manga)
|
||||
@ -123,8 +123,14 @@ public class LibraryUpdateService extends Service {
|
||||
.filter(pair -> pair.first > 0)
|
||||
.map(pair -> new MangaUpdate(manga, pair.first)))
|
||||
.doOnNext(updates::add)
|
||||
.doOnCompleted(() -> showBigNotification(getString(R.string.notification_update_completed),
|
||||
getUpdatedMangas(updates, failedUpdates)));
|
||||
.doOnCompleted(() -> {
|
||||
if (updates.isEmpty()) {
|
||||
cancelNotification();
|
||||
} else {
|
||||
showResultNotification(getString(R.string.notification_update_completed),
|
||||
getUpdatedMangasResult(updates, failedUpdates));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private Observable<Pair<Integer, Integer>> updateManga(Manga manga) {
|
||||
@ -133,7 +139,7 @@ public class LibraryUpdateService extends Service {
|
||||
.flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters));
|
||||
}
|
||||
|
||||
private String getUpdatedMangas(List<MangaUpdate> updates, List<Manga> failedUpdates) {
|
||||
private String getUpdatedMangasResult(List<MangaUpdate> updates, List<Manga> failedUpdates) {
|
||||
final StringBuilder result = new StringBuilder();
|
||||
if (updates.isEmpty()) {
|
||||
result.append(getString(R.string.notification_no_new_chapters)).append("\n");
|
||||
@ -185,7 +191,20 @@ public class LibraryUpdateService extends Service {
|
||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
private void showBigNotification(String title, String body) {
|
||||
private void showProgressNotification(String title, String body) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
|
||||
.setSmallIcon(R.drawable.ic_action_refresh)
|
||||
.setContentTitle(title)
|
||||
.setContentText(body)
|
||||
.setOngoing(true);
|
||||
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
private void showResultNotification(String title, String body) {
|
||||
NotificationCompat.Builder builder = new NotificationCompat.Builder(this)
|
||||
.setSmallIcon(R.drawable.ic_action_refresh)
|
||||
.setContentTitle(title)
|
||||
@ -199,6 +218,13 @@ public class LibraryUpdateService extends Service {
|
||||
notificationManager.notify(UPDATE_NOTIFICATION_ID, builder.build());
|
||||
}
|
||||
|
||||
private void cancelNotification() {
|
||||
NotificationManager notificationManager =
|
||||
(NotificationManager) getSystemService(Context.NOTIFICATION_SERVICE);
|
||||
|
||||
notificationManager.cancel(UPDATE_NOTIFICATION_ID);
|
||||
}
|
||||
|
||||
private PendingIntent getNotificationIntent() {
|
||||
Intent intent = new Intent(this, MainActivity.class);
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP | Intent.FLAG_ACTIVITY_SINGLE_TOP);
|
||||
|
@ -0,0 +1,31 @@
|
||||
package eu.kanade.tachiyomi.data.updater;
|
||||
|
||||
|
||||
import android.content.Context;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.rest.GithubService;
|
||||
import eu.kanade.tachiyomi.data.rest.Release;
|
||||
import eu.kanade.tachiyomi.data.rest.ServiceFactory;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import rx.Observable;
|
||||
|
||||
|
||||
public class UpdateChecker {
|
||||
private final Context context;
|
||||
|
||||
public UpdateChecker(Context context) {
|
||||
this.context = context;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns observable containing release information
|
||||
*
|
||||
*/
|
||||
public Observable<Release> checkForApplicationUpdate() {
|
||||
ToastUtil.showShort(context, context.getString(R.string.update_check_look_for_updates));
|
||||
//Create Github service to retrieve Github data
|
||||
GithubService service = ServiceFactory.createRetrofitService(GithubService.class, GithubService.SERVICE_ENDPOINT);
|
||||
return service.getLatestVersion();
|
||||
}
|
||||
}
|
@ -0,0 +1,99 @@
|
||||
package eu.kanade.tachiyomi.data.updater;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.net.URL;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
|
||||
public class UpdateDownloader extends AsyncTask<String, Void, Void> {
|
||||
/**
|
||||
* Name of cache directory.
|
||||
*/
|
||||
private static final String PARAMETER_CACHE_DIRECTORY = "apk_downloads";
|
||||
/**
|
||||
* Interface to global information about an application environment.
|
||||
*/
|
||||
private final Context context;
|
||||
/**
|
||||
* Cache directory used for cache management.
|
||||
*/
|
||||
private final File cacheDir;
|
||||
@Inject PreferencesHelper preferencesHelper;
|
||||
|
||||
/**
|
||||
* Constructor of UpdaterCache.
|
||||
*
|
||||
* @param context application environment interface.
|
||||
*/
|
||||
public UpdateDownloader(Context context) {
|
||||
App.get(context).getComponent().inject(this);
|
||||
this.context = context;
|
||||
|
||||
// Get cache directory from parameter.
|
||||
cacheDir = new File(preferencesHelper.getDownloadsDirectory(), PARAMETER_CACHE_DIRECTORY);
|
||||
|
||||
// Create cache directory.
|
||||
createCacheDir();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create cache directory if it doesn't exist
|
||||
*
|
||||
* @return true if cache dir is created otherwise false.
|
||||
*/
|
||||
@SuppressWarnings("UnusedReturnValue")
|
||||
private boolean createCacheDir() {
|
||||
return !cacheDir.exists() && cacheDir.mkdirs();
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(String... args) {
|
||||
try {
|
||||
createCacheDir();
|
||||
|
||||
URL url = new URL(args[0]);
|
||||
HttpURLConnection c = (HttpURLConnection) url.openConnection();
|
||||
c.connect();
|
||||
|
||||
File outputFile = new File(cacheDir, "update.apk");
|
||||
if (outputFile.exists()) {
|
||||
//noinspection ResultOfMethodCallIgnored
|
||||
outputFile.delete();
|
||||
}
|
||||
FileOutputStream fos = new FileOutputStream(outputFile);
|
||||
|
||||
InputStream is = c.getInputStream();
|
||||
|
||||
byte[] buffer = new byte[1024];
|
||||
int len1;
|
||||
while ((len1 = is.read(buffer)) != -1) {
|
||||
fos.write(buffer, 0, len1);
|
||||
}
|
||||
fos.close();
|
||||
is.close();
|
||||
|
||||
// Prompt install interface
|
||||
Intent intent = new Intent(Intent.ACTION_VIEW);
|
||||
intent.setDataAndType(Uri.fromFile(outputFile), "application/vnd.android.package-archive");
|
||||
intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK); // without this flag android returned a intent error!
|
||||
context.startActivity(intent);
|
||||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
@ -10,6 +10,7 @@ import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.sync.LibraryUpdateService;
|
||||
import eu.kanade.tachiyomi.data.sync.UpdateMangaSyncService;
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateDownloader;
|
||||
import eu.kanade.tachiyomi.injection.module.AppModule;
|
||||
import eu.kanade.tachiyomi.injection.module.DataModule;
|
||||
import eu.kanade.tachiyomi.ui.catalogue.CataloguePresenter;
|
||||
@ -50,6 +51,7 @@ public interface AppComponent {
|
||||
void inject(ReaderActivity readerActivity);
|
||||
void inject(MangaActivity mangaActivity);
|
||||
void inject(SettingsAccountsFragment settingsAccountsFragment);
|
||||
|
||||
void inject(SettingsActivity settingsActivity);
|
||||
|
||||
void inject(Source source);
|
||||
@ -59,6 +61,8 @@ public interface AppComponent {
|
||||
void inject(LibraryUpdateService libraryUpdateService);
|
||||
void inject(DownloadService downloadService);
|
||||
void inject(UpdateMangaSyncService updateMangaSyncService);
|
||||
|
||||
void inject(UpdateDownloader updateDownloader);
|
||||
Application application();
|
||||
|
||||
}
|
||||
|
@ -5,7 +5,8 @@ import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import icepick.Icepick;
|
||||
|
||||
public class BaseActivity extends AppCompatActivity {
|
||||
@ -58,20 +59,8 @@ public class BaseActivity extends AppCompatActivity {
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
public void registerForStickyEvents() {
|
||||
registerForStickyEvents(0);
|
||||
}
|
||||
|
||||
public void registerForStickyEvents(int priority) {
|
||||
EventBus.getDefault().registerSticky(this, priority);
|
||||
}
|
||||
|
||||
public void registerForEvents() {
|
||||
registerForEvents(0);
|
||||
}
|
||||
|
||||
public void registerForEvents(int priority) {
|
||||
EventBus.getDefault().register(this, priority);
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
public void unregisterForEvents() {
|
||||
|
@ -19,10 +19,19 @@ import android.content.Context;
|
||||
import android.support.design.widget.CoordinatorLayout;
|
||||
import android.support.design.widget.FloatingActionButton;
|
||||
import android.support.v4.view.ViewCompat;
|
||||
import android.support.v4.view.animation.FastOutSlowInInterpolator;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.View;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.view.animation.Interpolator;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
|
||||
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
|
||||
private static final Interpolator INTERPOLATOR = new FastOutSlowInInterpolator();
|
||||
private boolean mIsAnimatingOut = false;
|
||||
|
||||
public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
|
||||
super();
|
||||
}
|
||||
@ -40,12 +49,43 @@ public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
|
||||
final View target, final int dxConsumed, final int dyConsumed,
|
||||
final int dxUnconsumed, final int dyUnconsumed) {
|
||||
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
|
||||
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
|
||||
if (dyConsumed > 0 && !this.mIsAnimatingOut && child.getVisibility() == View.VISIBLE) {
|
||||
// User scrolled down and the FAB is currently visible -> hide the FAB
|
||||
child.hide();
|
||||
animateOut(child);
|
||||
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
|
||||
// User scrolled up and the FAB is currently not visible -> show the FAB
|
||||
child.show();
|
||||
animateIn(child);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Same animation that FloatingActionButton.Behavior uses to hide the FAB when the AppBarLayout exits
|
||||
private void animateOut(final FloatingActionButton button) {
|
||||
Animation anim = AnimationUtils.loadAnimation(button.getContext(), R.anim.fab_hide_to_bottom);
|
||||
anim.setInterpolator(INTERPOLATOR);
|
||||
anim.setDuration(200L);
|
||||
anim.setAnimationListener(new Animation.AnimationListener() {
|
||||
public void onAnimationStart(Animation animation) {
|
||||
ScrollAwareFABBehavior.this.mIsAnimatingOut = true;
|
||||
}
|
||||
|
||||
public void onAnimationEnd(Animation animation) {
|
||||
ScrollAwareFABBehavior.this.mIsAnimatingOut = false;
|
||||
button.setVisibility(View.GONE);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAnimationRepeat(final Animation animation) {
|
||||
}
|
||||
});
|
||||
button.startAnimation(anim);
|
||||
}
|
||||
|
||||
// Same animation that FloatingActionButton.Behavior uses to show the FAB when the AppBarLayout enters
|
||||
private void animateIn(FloatingActionButton button) {
|
||||
button.setVisibility(View.VISIBLE);
|
||||
Animation anim = AnimationUtils.loadAnimation(button.getContext(), R.anim.fab_show_from_bottom);
|
||||
anim.setDuration(200L);
|
||||
anim.setInterpolator(INTERPOLATOR);
|
||||
button.startAnimation(anim);
|
||||
}
|
||||
}
|
@ -3,7 +3,8 @@ package eu.kanade.tachiyomi.ui.base.fragment;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.app.Fragment;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity;
|
||||
import icepick.Icepick;
|
||||
|
||||
@ -33,20 +34,8 @@ public class BaseFragment extends Fragment {
|
||||
return (BaseActivity) getActivity();
|
||||
}
|
||||
|
||||
public void registerForStickyEvents() {
|
||||
registerForStickyEvents(0);
|
||||
}
|
||||
|
||||
public void registerForStickyEvents(int priority) {
|
||||
EventBus.getDefault().registerSticky(this, priority);
|
||||
}
|
||||
|
||||
public void registerForEvents() {
|
||||
registerForEvents(0);
|
||||
}
|
||||
|
||||
public void registerForEvents(int priority) {
|
||||
EventBus.getDefault().register(this, priority);
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
||||
public void unregisterForEvents() {
|
||||
|
@ -4,7 +4,8 @@ import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import icepick.Icepick;
|
||||
import nucleus.view.ViewWithPresenter;
|
||||
|
||||
@ -24,10 +25,6 @@ public class BasePresenter<V extends ViewWithPresenter> extends RxPresenter<V> {
|
||||
Icepick.saveInstanceState(this, state);
|
||||
}
|
||||
|
||||
public void registerForStickyEvents() {
|
||||
EventBus.getDefault().registerSticky(this);
|
||||
}
|
||||
|
||||
public void registerForEvents() {
|
||||
EventBus.getDefault().register(this);
|
||||
}
|
||||
|
@ -213,6 +213,137 @@ public class RxPresenter<View> extends Presenter<View> {
|
||||
restartableReplay(restartableId, observableFactory, onNext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* A startable behaves the same as a restartable but it does not resubscribe on process restart
|
||||
*
|
||||
* @param startableId an id of the restartable.
|
||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
||||
*/
|
||||
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory) {
|
||||
restartables.put(startableId, () -> observableFactory.call().subscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
* A startable behaves the same as a restartable but it does not resubscribe on process restart
|
||||
*
|
||||
* @param startableId an id of the restartable.
|
||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
||||
* @param onError a callback that will be called if the source observable emits onError.
|
||||
*/
|
||||
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory,
|
||||
final Action1<T> onNext, final Action1<Throwable> onError) {
|
||||
|
||||
restartables.put(startableId, () -> observableFactory.call().subscribe(onNext, onError));
|
||||
}
|
||||
|
||||
/**
|
||||
* A startable behaves the same as a restartable but it does not resubscribe on process restart
|
||||
*
|
||||
* @param startableId an id of the restartable.
|
||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
||||
*/
|
||||
public <T> void startable(int startableId, final Func0<Observable<T>> observableFactory, final Action1<T> onNext) {
|
||||
restartables.put(startableId, () -> observableFactory.call().subscribe(onNext));
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut that can be used instead of combining together
|
||||
* {@link #startable(int, Func0)},
|
||||
* {@link #deliverFirst()},
|
||||
* {@link #split(Action2, Action2)}.
|
||||
*
|
||||
* @param startableId an id of the startable.
|
||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
||||
* @param onError a callback that will be called if the source observable emits onError.
|
||||
* @param <T> the type of the observable.
|
||||
*/
|
||||
public <T> void startableFirst(int startableId, final Func0<Observable<T>> observableFactory,
|
||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
||||
|
||||
restartables.put(startableId, new Func0<Subscription>() {
|
||||
@Override
|
||||
public Subscription call() {
|
||||
return observableFactory.call()
|
||||
.compose(RxPresenter.this.<T>deliverFirst())
|
||||
.subscribe(split(onNext, onError));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut for calling {@link #startableFirst(int, Func0, Action2, Action2)} with the last parameter = null.
|
||||
*/
|
||||
public <T> void startableFirst(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
||||
startableFirst(startableId, observableFactory, onNext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut that can be used instead of combining together
|
||||
* {@link #startable(int, Func0)},
|
||||
* {@link #deliverLatestCache()},
|
||||
* {@link #split(Action2, Action2)}.
|
||||
*
|
||||
* @param startableId an id of the startable.
|
||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
||||
* @param onError a callback that will be called if the source observable emits onError.
|
||||
* @param <T> the type of the observable.
|
||||
*/
|
||||
public <T> void startableLatestCache(int startableId, final Func0<Observable<T>> observableFactory,
|
||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
||||
|
||||
restartables.put(startableId, new Func0<Subscription>() {
|
||||
@Override
|
||||
public Subscription call() {
|
||||
return observableFactory.call()
|
||||
.compose(RxPresenter.this.<T>deliverLatestCache())
|
||||
.subscribe(split(onNext, onError));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut for calling {@link #startableLatestCache(int, Func0, Action2, Action2)} with the last parameter = null.
|
||||
*/
|
||||
public <T> void startableLatestCache(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
||||
startableLatestCache(startableId, observableFactory, onNext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut that can be used instead of combining together
|
||||
* {@link #startable(int, Func0)},
|
||||
* {@link #deliverReplay()},
|
||||
* {@link #split(Action2, Action2)}.
|
||||
*
|
||||
* @param startableId an id of the startable.
|
||||
* @param observableFactory a factory that should return an Observable when the startable should run.
|
||||
* @param onNext a callback that will be called when received data should be delivered to view.
|
||||
* @param onError a callback that will be called if the source observable emits onError.
|
||||
* @param <T> the type of the observable.
|
||||
*/
|
||||
public <T> void startableReplay(int startableId, final Func0<Observable<T>> observableFactory,
|
||||
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
|
||||
|
||||
restartables.put(startableId, new Func0<Subscription>() {
|
||||
@Override
|
||||
public Subscription call() {
|
||||
return observableFactory.call()
|
||||
.compose(RxPresenter.this.<T>deliverReplay())
|
||||
.subscribe(split(onNext, onError));
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* This is a shortcut for calling {@link #startableReplay(int, Func0, Action2, Action2)} with the last parameter = null.
|
||||
*/
|
||||
public <T> void startableReplay(int startableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
|
||||
startableReplay(startableId, observableFactory, onNext, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
|
||||
* the source {@link rx.Observable}.
|
||||
|
@ -20,6 +20,7 @@ import android.view.ViewGroup;
|
||||
import android.view.animation.Animation;
|
||||
import android.view.animation.AnimationUtils;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.Spinner;
|
||||
import android.widget.ViewSwitcher;
|
||||
@ -66,7 +67,7 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
|
||||
private EndlessListScrollListener listScrollListener;
|
||||
|
||||
@State String query = "";
|
||||
@State int selectedIndex = -1;
|
||||
@State int selectedIndex;
|
||||
private final int SEARCH_TIMEOUT = 1000;
|
||||
|
||||
private PublishSubject<String> queryDebouncerSubject;
|
||||
@ -122,25 +123,27 @@ public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
|
||||
Context themedContext = getBaseActivity().getSupportActionBar() != null ?
|
||||
getBaseActivity().getSupportActionBar().getThemedContext() : getActivity();
|
||||
spinner = new Spinner(themedContext);
|
||||
CatalogueSpinnerAdapter spinnerAdapter = new CatalogueSpinnerAdapter(themedContext,
|
||||
ArrayAdapter<Source> spinnerAdapter = new ArrayAdapter<>(themedContext,
|
||||
android.R.layout.simple_spinner_item, getPresenter().getEnabledSources());
|
||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
||||
if (savedState == null) selectedIndex = spinnerAdapter.getEmptyIndex();
|
||||
|
||||
if (savedState == null) {
|
||||
selectedIndex = getPresenter().getLastUsedSourceIndex();
|
||||
}
|
||||
spinner.setAdapter(spinnerAdapter);
|
||||
spinner.setSelection(selectedIndex);
|
||||
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
||||
@Override
|
||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
||||
Source source = spinnerAdapter.getItem(position);
|
||||
// We add an empty source with id -1 that acts as a placeholder to show a hint
|
||||
// that asks to select a source
|
||||
if (source.getId() != -1 && (selectedIndex != position || adapter.isEmpty())) {
|
||||
if (selectedIndex != position || adapter.isEmpty()) {
|
||||
// Set previous selection if it's not a valid source and notify the user
|
||||
if (!getPresenter().isValidSource(source)) {
|
||||
spinner.setSelection(spinnerAdapter.getEmptyIndex());
|
||||
spinner.setSelection(getPresenter().findFirstValidSource());
|
||||
ToastUtil.showShort(getActivity(), R.string.source_requires_login);
|
||||
} else {
|
||||
selectedIndex = position;
|
||||
getPresenter().setEnabledSource(selectedIndex);
|
||||
showProgressBar();
|
||||
glm.scrollToPositionWithOffset(0, 0);
|
||||
llm.scrollToPositionWithOffset(0, 0);
|
||||
|
@ -4,6 +4,8 @@ import android.view.View;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.mikepenz.iconics.view.IconicsImageView;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
@ -13,7 +15,7 @@ public class CatalogueGridHolder extends CatalogueHolder {
|
||||
|
||||
@Bind(R.id.title) TextView title;
|
||||
@Bind(R.id.thumbnail) ImageView thumbnail;
|
||||
@Bind(R.id.favorite_sticker) ImageView favoriteSticker;
|
||||
@Bind(R.id.favorite_sticker) IconicsImageView favoriteSticker;
|
||||
|
||||
public CatalogueGridHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) {
|
||||
super(view, adapter, listener);
|
||||
@ -23,7 +25,10 @@ public class CatalogueGridHolder extends CatalogueHolder {
|
||||
@Override
|
||||
public void onSetValues(Manga manga, CataloguePresenter presenter) {
|
||||
title.setText(manga.title);
|
||||
// Set visibility of in library icon.
|
||||
favoriteSticker.setVisibility(manga.favorite ? View.VISIBLE : View.GONE);
|
||||
// Set alpha of thumbnail.
|
||||
thumbnail.setAlpha(manga.favorite ? 0.3f : 1.0f);
|
||||
setImage(manga, presenter);
|
||||
}
|
||||
|
||||
|
@ -32,6 +32,7 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
|
||||
@Inject CoverCache coverCache;
|
||||
@Inject PreferencesHelper prefs;
|
||||
|
||||
private List<Source> sources;
|
||||
private Source source;
|
||||
@State int sourceId;
|
||||
|
||||
@ -53,23 +54,25 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
source = sourceManager.get(sourceId);
|
||||
}
|
||||
|
||||
sources = sourceManager.getSources();
|
||||
|
||||
mangaDetailSubject = PublishSubject.create();
|
||||
|
||||
pager = new RxPager<>();
|
||||
|
||||
restartableReplay(GET_MANGA_LIST,
|
||||
startableReplay(GET_MANGA_LIST,
|
||||
pager::results,
|
||||
(view, pair) -> view.onAddPage(pair.first, pair.second));
|
||||
|
||||
restartableFirst(GET_MANGA_PAGE,
|
||||
startableFirst(GET_MANGA_PAGE,
|
||||
() -> pager.request(page -> getMangasPageObservable(page + 1)),
|
||||
(view, next) -> {},
|
||||
(view, error) -> view.onAddPageError());
|
||||
|
||||
restartableLatestCache(GET_MANGA_DETAIL,
|
||||
startableLatestCache(GET_MANGA_DETAIL,
|
||||
() -> mangaDetailSubject
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap(Observable::from)
|
||||
@ -84,13 +87,6 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
|
||||
.subscribe(this::setDisplayMode));
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
source = sourceManager.get(sourceId);
|
||||
stop(GET_MANGA_LIST);
|
||||
stop(GET_MANGA_DETAIL);
|
||||
stop(GET_MANGA_PAGE);
|
||||
}
|
||||
|
||||
private void setDisplayMode(boolean asList) {
|
||||
this.isListMode = asList;
|
||||
if (asList) {
|
||||
@ -175,6 +171,14 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
|
||||
return lastMangasPage != null && lastMangasPage.nextPageUrl != null;
|
||||
}
|
||||
|
||||
public int getLastUsedSourceIndex() {
|
||||
int index = prefs.lastUsedCatalogueSource().get();
|
||||
if (index < 0 || index >= sources.size() || !isValidSource(sources.get(index))) {
|
||||
return findFirstValidSource();
|
||||
}
|
||||
return index;
|
||||
}
|
||||
|
||||
public boolean isValidSource(Source source) {
|
||||
if (!source.isLoginRequired() || source.isLogged())
|
||||
return true;
|
||||
@ -183,6 +187,19 @@ public class CataloguePresenter extends BasePresenter<CatalogueFragment> {
|
||||
|| prefs.getSourcePassword(source).equals(""));
|
||||
}
|
||||
|
||||
public int findFirstValidSource() {
|
||||
for (int i = 0; i < sources.size(); i++) {
|
||||
if (isValidSource(sources.get(i))) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void setEnabledSource(int index) {
|
||||
prefs.lastUsedCatalogueSource().set(index);
|
||||
}
|
||||
|
||||
public List<Source> getEnabledSources() {
|
||||
// TODO filter by enabled source
|
||||
return sourceManager.getSources();
|
||||
|
@ -1,120 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.catalogue;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import org.jsoup.nodes.Document;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.MangasPage;
|
||||
|
||||
public class CatalogueSpinnerAdapter extends ArrayAdapter<Source> {
|
||||
|
||||
public CatalogueSpinnerAdapter(Context context, int resource, List<Source> sources) {
|
||||
super(context, resource, sources);
|
||||
sources.add(new SimpleSource());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View convertView, ViewGroup parent) {
|
||||
|
||||
View v = super.getView(position, convertView, parent);
|
||||
if (position == getCount()) {
|
||||
((TextView)v.findViewById(android.R.id.text1)).setText("");
|
||||
((TextView)v.findViewById(android.R.id.text1)).setHint(getItem(getCount()).getName());
|
||||
}
|
||||
|
||||
return v;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return super.getCount()-1; // you dont display last item. It is used as hint.
|
||||
}
|
||||
|
||||
public int getEmptyIndex() {
|
||||
return getCount();
|
||||
}
|
||||
|
||||
private class SimpleSource extends Source {
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return getContext().getString(R.string.select_source);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getId() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getBaseUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isLoginRequired() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialPopularMangasUrl() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String getInitialSearchUrl(String query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected String parseHtmlToImageUrl(String unparsedHtml) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -29,7 +29,8 @@ public class DownloadFragment extends BaseRxFragment<DownloadPresenter> {
|
||||
private DownloadAdapter adapter;
|
||||
|
||||
private MenuItem startButton;
|
||||
private MenuItem stopButton;
|
||||
private MenuItem pauseButton;
|
||||
private MenuItem clearButton;
|
||||
|
||||
private Subscription queueStatusSubscription;
|
||||
private boolean isRunning;
|
||||
@ -64,11 +65,16 @@ public class DownloadFragment extends BaseRxFragment<DownloadPresenter> {
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.download_queue, menu);
|
||||
startButton = menu.findItem(R.id.start_queue);
|
||||
stopButton = menu.findItem(R.id.stop_queue);
|
||||
pauseButton = menu.findItem(R.id.pause_queue);
|
||||
clearButton = menu.findItem(R.id.clear_queue);
|
||||
|
||||
if(adapter.getItemCount() > 0) {
|
||||
clearButton.setVisible(true);
|
||||
}
|
||||
|
||||
// Menu seems to be inflated after onResume in fragments, so we initialize them here
|
||||
startButton.setVisible(!isRunning && !getPresenter().downloadManager.getQueue().isEmpty());
|
||||
stopButton.setVisible(isRunning);
|
||||
pauseButton.setVisible(isRunning);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -77,9 +83,14 @@ public class DownloadFragment extends BaseRxFragment<DownloadPresenter> {
|
||||
case R.id.start_queue:
|
||||
DownloadService.start(getActivity());
|
||||
break;
|
||||
case R.id.stop_queue:
|
||||
case R.id.pause_queue:
|
||||
DownloadService.stop(getActivity());
|
||||
break;
|
||||
case R.id.clear_queue:
|
||||
DownloadService.stop(getActivity());
|
||||
getPresenter().clearQueue();
|
||||
clearButton.setVisible(false);
|
||||
break;
|
||||
}
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
@ -101,8 +112,8 @@ public class DownloadFragment extends BaseRxFragment<DownloadPresenter> {
|
||||
isRunning = running;
|
||||
if (startButton != null)
|
||||
startButton.setVisible(!running && !getPresenter().downloadManager.getQueue().isEmpty());
|
||||
if (stopButton != null)
|
||||
stopButton.setVisible(running);
|
||||
if (pauseButton != null)
|
||||
pauseButton.setVisible(running);
|
||||
}
|
||||
|
||||
private void createAdapter() {
|
||||
|
@ -20,15 +20,13 @@ import timber.log.Timber;
|
||||
|
||||
public class DownloadPresenter extends BasePresenter<DownloadFragment> {
|
||||
|
||||
public final static int GET_DOWNLOAD_QUEUE = 1;
|
||||
@Inject DownloadManager downloadManager;
|
||||
|
||||
private DownloadQueue downloadQueue;
|
||||
private Subscription statusSubscription;
|
||||
private Subscription pageProgressSubscription;
|
||||
private HashMap<Download, Subscription> progressSubscriptions;
|
||||
|
||||
public final static int GET_DOWNLOAD_QUEUE = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
@ -57,6 +55,7 @@ public class DownloadPresenter extends BasePresenter<DownloadFragment> {
|
||||
}));
|
||||
|
||||
add(pageProgressSubscription = downloadQueue.getProgressObservable()
|
||||
.onBackpressureBuffer()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(view::updateDownloadedPages));
|
||||
}
|
||||
@ -122,4 +121,8 @@ public class DownloadPresenter extends BasePresenter<DownloadFragment> {
|
||||
remove(statusSubscription);
|
||||
}
|
||||
|
||||
public void clearQueue() {
|
||||
downloadQueue.clear();
|
||||
start(GET_DOWNLOAD_QUEUE);
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,6 @@ package eu.kanade.tachiyomi.ui.library;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Filter;
|
||||
import android.widget.Filterable;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
@ -12,28 +10,24 @@ import java.util.List;
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import rx.Observable;
|
||||
|
||||
public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga>
|
||||
implements Filterable {
|
||||
public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga> {
|
||||
|
||||
List<Manga> mangas;
|
||||
Filter filter;
|
||||
private List<Manga> mangas;
|
||||
private LibraryCategoryFragment fragment;
|
||||
|
||||
public LibraryCategoryAdapter(LibraryCategoryFragment fragment) {
|
||||
this.fragment = fragment;
|
||||
mItems = new ArrayList<>();
|
||||
filter = new LibraryFilter();
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
public void setItems(List<Manga> list) {
|
||||
mItems = list;
|
||||
notifyDataSetChanged();
|
||||
|
||||
// TODO needed for filtering?
|
||||
mangas = list;
|
||||
// A copy of manga that it's always unfiltered
|
||||
mangas = new ArrayList<>(list);
|
||||
updateDataSet(null);
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
@ -47,7 +41,16 @@ public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga
|
||||
|
||||
@Override
|
||||
public void updateDataSet(String param) {
|
||||
if (mangas != null) {
|
||||
filterItems(mangas);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean filterObject(Manga manga, String query) {
|
||||
return (manga.title != null && manga.title.toLowerCase().contains(query)) ||
|
||||
(manga.author != null && manga.author.toLowerCase().contains(query));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -61,7 +64,6 @@ public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga
|
||||
final LibraryPresenter presenter = ((LibraryFragment) fragment.getParentFragment()).getPresenter();
|
||||
final Manga manga = getItem(position);
|
||||
holder.onSetValues(manga, presenter);
|
||||
|
||||
//When user scrolls this bind the correct selection status
|
||||
holder.itemView.setActivated(isSelected(position));
|
||||
}
|
||||
@ -70,40 +72,4 @@ public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga
|
||||
return fragment.recycler.getItemWidth() / 3 * 4;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Filter getFilter() {
|
||||
return filter;
|
||||
}
|
||||
|
||||
private class LibraryFilter extends Filter {
|
||||
@Override
|
||||
protected FilterResults performFiltering(CharSequence charSequence) {
|
||||
FilterResults results = new FilterResults();
|
||||
String query = charSequence.toString().toLowerCase();
|
||||
|
||||
if (query.length() == 0) {
|
||||
results.values = mangas;
|
||||
results.count = mangas.size();
|
||||
} else {
|
||||
List<Manga> filteredMangas = Observable.from(mangas)
|
||||
.filter(x ->
|
||||
(x.title != null && x.title.toLowerCase().contains(query)) ||
|
||||
(x.author != null && x.author.toLowerCase().contains(query)) ||
|
||||
(x.artist != null && x.artist.toLowerCase().contains(query)))
|
||||
.toList()
|
||||
.toBlocking()
|
||||
.single();
|
||||
results.values = filteredMangas;
|
||||
results.count = filteredMangas.size();
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void publishResults(CharSequence constraint, FilterResults results) {
|
||||
setItems((List<Manga>) results.values);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -9,6 +9,9 @@ import android.view.ViewGroup;
|
||||
|
||||
import com.f2prateek.rx.preferences.Preference;
|
||||
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
@ -22,7 +25,6 @@ import eu.kanade.tachiyomi.event.LibraryMangasEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import eu.kanade.tachiyomi.widget.AutofitRecyclerView;
|
||||
import icepick.State;
|
||||
import rx.Subscription;
|
||||
@ -37,6 +39,7 @@ public class LibraryCategoryFragment extends BaseFragment
|
||||
private List<Manga> mangas;
|
||||
|
||||
private Subscription numColumnsSubscription;
|
||||
private Subscription searchSubscription;
|
||||
|
||||
public static LibraryCategoryFragment newInstance(int position) {
|
||||
LibraryCategoryFragment fragment = new LibraryCategoryFragment();
|
||||
@ -44,6 +47,8 @@ public class LibraryCategoryFragment extends BaseFragment
|
||||
return fragment;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
|
||||
// Inflate the layout for this fragment
|
||||
@ -77,19 +82,29 @@ public class LibraryCategoryFragment extends BaseFragment
|
||||
}
|
||||
}
|
||||
|
||||
searchSubscription = getLibraryPresenter().searchSubject
|
||||
.subscribe(text -> {
|
||||
adapter.setSearchText(text);
|
||||
adapter.updateDataSet();
|
||||
});
|
||||
|
||||
|
||||
|
||||
return view;
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
numColumnsSubscription.unsubscribe();
|
||||
searchSubscription.unsubscribe();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
registerForStickyEvents();
|
||||
registerForEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -104,8 +119,8 @@ public class LibraryCategoryFragment extends BaseFragment
|
||||
super.onSaveInstanceState(outState);
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(LibraryMangasEvent event) {
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(LibraryMangasEvent event) {
|
||||
List<Category> categories = getLibraryFragment().getAdapter().categories;
|
||||
// When a category is deleted, the index can be greater than the number of categories
|
||||
if (position >= categories.size())
|
||||
@ -155,15 +170,16 @@ public class LibraryCategoryFragment extends BaseFragment
|
||||
|
||||
private void toggleSelection(int position) {
|
||||
LibraryFragment f = getLibraryFragment();
|
||||
|
||||
adapter.toggleSelection(position, false);
|
||||
f.getPresenter().setSelection(adapter.getItem(position), adapter.isSelected(position));
|
||||
|
||||
int count = f.getPresenter().selectedMangas.size();
|
||||
if (count == 0) {
|
||||
f.destroyActionModeIfNeeded();
|
||||
} else {
|
||||
}
|
||||
else {
|
||||
f.setContextTitle(count);
|
||||
f.setVisibilityOfCoverEdit(count);
|
||||
f.invalidateActionMode();
|
||||
}
|
||||
}
|
||||
|
@ -1,12 +1,16 @@
|
||||
package eu.kanade.tachiyomi.ui.library;
|
||||
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.design.widget.AppBarLayout;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.view.ActionMode;
|
||||
import android.support.v7.widget.SearchView;
|
||||
import android.text.TextUtils;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
@ -16,22 +20,27 @@ import android.view.ViewGroup;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.io.IOHandler;
|
||||
import eu.kanade.tachiyomi.data.sync.LibraryUpdateService;
|
||||
import eu.kanade.tachiyomi.event.LibraryMangasEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
|
||||
import eu.kanade.tachiyomi.ui.library.category.CategoryActivity;
|
||||
import eu.kanade.tachiyomi.ui.main.MainActivity;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import icepick.State;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
|
||||
@ -39,16 +48,25 @@ import nucleus.factory.RequiresPresenter;
|
||||
public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
|
||||
implements ActionMode.Callback {
|
||||
|
||||
@Bind(R.id.view_pager) ViewPager viewPager;
|
||||
private TabLayout tabs;
|
||||
private AppBarLayout appBar;
|
||||
|
||||
private static final int REQUEST_IMAGE_OPEN = 101;
|
||||
|
||||
protected LibraryAdapter adapter;
|
||||
|
||||
private ActionMode actionMode;
|
||||
@Bind(R.id.view_pager) ViewPager viewPager;
|
||||
|
||||
@State int activeCategory;
|
||||
|
||||
@State String query = "";
|
||||
|
||||
private TabLayout tabs;
|
||||
|
||||
private AppBarLayout appBar;
|
||||
|
||||
private ActionMode actionMode;
|
||||
|
||||
private Manga selectedCoverManga;
|
||||
|
||||
public static LibraryFragment newInstance() {
|
||||
return new LibraryFragment();
|
||||
}
|
||||
@ -60,8 +78,7 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
|
||||
// Inflate the layout for this fragment
|
||||
View view = inflater.inflate(R.layout.fragment_library, container, false);
|
||||
setToolbarTitle(getString(R.string.label_library));
|
||||
@ -75,6 +92,10 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
|
||||
viewPager.setAdapter(adapter);
|
||||
tabs.setupWithViewPager(viewPager);
|
||||
|
||||
if (savedState != null) {
|
||||
getPresenter().searchSubject.onNext(query);
|
||||
}
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@ -84,12 +105,6 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
EventBus.getDefault().removeStickyEvent(LibraryMangasEvent.class);
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle bundle) {
|
||||
activeCategory = viewPager.getCurrentItem();
|
||||
@ -99,6 +114,29 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.library, menu);
|
||||
|
||||
// Initialize search menu
|
||||
MenuItem searchItem = menu.findItem(R.id.action_search);
|
||||
final SearchView searchView = (SearchView) searchItem.getActionView();
|
||||
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
searchItem.expandActionView();
|
||||
searchView.setQuery(query, true);
|
||||
searchView.clearFocus();
|
||||
}
|
||||
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
|
||||
@Override
|
||||
public boolean onQueryTextSubmit(String query) {
|
||||
onSearchTextChange(query);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onQueryTextChange(String newText) {
|
||||
onSearchTextChange(newText);
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -115,6 +153,11 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
|
||||
return super.onOptionsItemSelected(item);
|
||||
}
|
||||
|
||||
private void onSearchTextChange(String query) {
|
||||
this.query = query;
|
||||
getPresenter().searchSubject.onNext(query);
|
||||
}
|
||||
|
||||
private void onEditCategories() {
|
||||
Intent intent = CategoryActivity.newIntent(getActivity());
|
||||
startActivity(intent);
|
||||
@ -158,6 +201,11 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
|
||||
actionMode.setTitle(getString(R.string.label_selected, count));
|
||||
}
|
||||
|
||||
public void setVisibilityOfCoverEdit(int count) {
|
||||
// If count = 1 display edit button
|
||||
actionMode.getMenu().findItem(R.id.action_edit_cover).setVisible((count == 1));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.library_selection, menu);
|
||||
@ -173,6 +221,11 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_edit_cover:
|
||||
changeSelectedCover(getPresenter().selectedMangas);
|
||||
rebuildAdapter();
|
||||
destroyActionModeIfNeeded();
|
||||
return true;
|
||||
case R.id.action_move_to_category:
|
||||
moveMangasToCategories(getPresenter().selectedMangas);
|
||||
return true;
|
||||
@ -184,6 +237,15 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO workaround. Covers won't refresh any other way.
|
||||
*/
|
||||
public void rebuildAdapter() {
|
||||
adapter = new LibraryAdapter(getChildFragmentManager());
|
||||
viewPager.setAdapter(adapter);
|
||||
tabs.setupWithViewPager(viewPager);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
adapter.setSelectionMode(FlexibleAdapter.MODE_SINGLE);
|
||||
@ -197,6 +259,53 @@ public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
|
||||
}
|
||||
}
|
||||
|
||||
private void changeSelectedCover(List<Manga> mangas) {
|
||||
if (mangas.size() == 1) {
|
||||
selectedCoverManga = mangas.get(0);
|
||||
if (selectedCoverManga.favorite) {
|
||||
|
||||
Intent intent = new Intent();
|
||||
intent.setType("image/*");
|
||||
intent.setAction(Intent.ACTION_GET_CONTENT);
|
||||
startActivityForResult(Intent.createChooser(intent,
|
||||
getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN);
|
||||
} else {
|
||||
ToastUtil.showShort(getContext(), R.string.notification_first_add_to_library);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onActivityResult(int requestCode, int resultCode, Intent data) {
|
||||
if (resultCode == Activity.RESULT_OK) {
|
||||
switch (requestCode) {
|
||||
case (REQUEST_IMAGE_OPEN):
|
||||
if (selectedCoverManga != null) {
|
||||
// Get the file's content URI from the incoming Intent
|
||||
Uri selectedImageUri = data.getData();
|
||||
|
||||
// Convert to absolute path to prevent FileNotFoundException
|
||||
String result = IOHandler.getFilePath(selectedImageUri,
|
||||
getContext().getContentResolver(), getContext());
|
||||
|
||||
// Get file from filepath
|
||||
File picture = new File(result != null ? result : "");
|
||||
|
||||
try {
|
||||
// Update cover to selected file, show error if something went wrong
|
||||
if (!getPresenter().editCoverWithLocalFile(picture, selectedCoverManga))
|
||||
ToastUtil.showShort(getContext(), R.string.notification_manga_update_failed);
|
||||
|
||||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void moveMangasToCategories(List<Manga> mangas) {
|
||||
new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.action_move_category)
|
||||
|
@ -50,4 +50,6 @@ public class LibraryHolder extends FlexibleViewHolder {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
@ -3,13 +3,16 @@ package eu.kanade.tachiyomi.ui.library;
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Category;
|
||||
@ -21,25 +24,27 @@ import eu.kanade.tachiyomi.event.LibraryMangasEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.subjects.BehaviorSubject;
|
||||
|
||||
public class LibraryPresenter extends BasePresenter<LibraryFragment> {
|
||||
|
||||
private static final int GET_LIBRARY = 1;
|
||||
protected List<Category> categories;
|
||||
protected List<Manga> selectedMangas;
|
||||
protected BehaviorSubject<String> searchSubject;
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject PreferencesHelper preferences;
|
||||
@Inject CoverCache coverCache;
|
||||
@Inject SourceManager sourceManager;
|
||||
|
||||
protected List<Category> categories;
|
||||
protected List<Manga> selectedMangas;
|
||||
|
||||
private static final int GET_LIBRARY = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
selectedMangas = new ArrayList<>();
|
||||
|
||||
searchSubject = BehaviorSubject.create();
|
||||
|
||||
restartableLatestCache(GET_LIBRARY,
|
||||
this::getLibraryObservable,
|
||||
(view, pair) -> view.onNextLibraryUpdate(pair.first, pair.second));
|
||||
@ -50,9 +55,9 @@ public class LibraryPresenter extends BasePresenter<LibraryFragment> {
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
protected void onDropView() {
|
||||
EventBus.getDefault().removeStickyEvent(LibraryMangasEvent.class);
|
||||
super.onDestroy();
|
||||
super.onDropView();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -135,4 +140,18 @@ public class LibraryPresenter extends BasePresenter<LibraryFragment> {
|
||||
|
||||
db.setMangaCategories(mc, mangas);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update cover with local file
|
||||
*/
|
||||
public boolean editCoverWithLocalFile(File file, Manga manga) throws IOException {
|
||||
if (!manga.initialized)
|
||||
return false;
|
||||
|
||||
if (manga.favorite) {
|
||||
coverCache.copyToLocalCache(manga.thumbnail_url, file);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -15,11 +15,12 @@ import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
|
@ -2,14 +2,16 @@ package eu.kanade.tachiyomi.ui.manga;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.event.MangaEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import icepick.State;
|
||||
import rx.Observable;
|
||||
|
||||
@ -28,7 +30,7 @@ public class MangaPresenter extends BasePresenter<MangaActivity> {
|
||||
restartableLatestCache(GET_MANGA, this::getMangaObservable, MangaActivity::setManga);
|
||||
|
||||
if (savedState == null)
|
||||
registerForStickyEvents();
|
||||
registerForEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -43,8 +45,8 @@ public class MangaPresenter extends BasePresenter<MangaActivity> {
|
||||
.doOnNext(manga -> EventBus.getDefault().postSticky(new MangaEvent(manga)));
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(Manga manga) {
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(Manga manga) {
|
||||
EventBus.getDefault().removeStickyEvent(manga);
|
||||
unregisterForEvents();
|
||||
this.manga = manga;
|
||||
|
@ -19,6 +19,7 @@ import android.widget.ImageView;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.Bind;
|
||||
@ -99,6 +100,15 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
// Stop recycler's scrolling when onPause is called. If the activity is finishing
|
||||
// the presenter will be destroyed, and it could cause NPE
|
||||
// https://github.com/inorichi/tachiyomi/issues/159
|
||||
recyclerView.stopScroll();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
|
||||
inflater.inflate(R.menu.chapters, menu);
|
||||
@ -110,6 +120,9 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
|
||||
case R.id.action_display_mode:
|
||||
showDisplayModeDialog();
|
||||
return true;
|
||||
case R.id.manga_download:
|
||||
showDownloadDialog();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
@ -164,9 +177,9 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchChaptersError() {
|
||||
public void onFetchChaptersError(Throwable error) {
|
||||
swipeRefresh.setRefreshing(false);
|
||||
ToastUtil.showShort(getContext(), R.string.fetch_chapters_error);
|
||||
ToastUtil.showShort(getContext(), error.getMessage());
|
||||
}
|
||||
|
||||
public boolean isCatalogueManga() {
|
||||
@ -190,6 +203,7 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
|
||||
int selectedIndex = manga.getDisplayMode() == Manga.DISPLAY_NAME ? 0 : 1;
|
||||
|
||||
new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.action_display_mode)
|
||||
.items(modes)
|
||||
.itemsIds(ids)
|
||||
.itemsCallbackSingleChoice(selectedIndex, (dialog, itemView, which, text) -> {
|
||||
@ -202,6 +216,32 @@ public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implemen
|
||||
.show();
|
||||
}
|
||||
|
||||
private void showDownloadDialog() {
|
||||
|
||||
// Get available modes
|
||||
String[] modes = {getString(R.string.download_all), getString(R.string.download_unread)};
|
||||
|
||||
new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.manga_download)
|
||||
.items(modes)
|
||||
.itemsCallback((dialog, view, i, charSequence) -> {
|
||||
List<Chapter> chapters = new ArrayList<>();
|
||||
|
||||
for(Chapter chapter : getPresenter().getChapters()) {
|
||||
if(!chapter.isDownloaded()) {
|
||||
if(i == 0 || (i == 1 && !chapter.read)) {
|
||||
chapters.add(chapter);
|
||||
}
|
||||
}
|
||||
}
|
||||
if(chapters.size() > 0) {
|
||||
onDownload(Observable.from(chapters));
|
||||
}
|
||||
})
|
||||
.negativeText(R.string.button_cancel)
|
||||
.show();
|
||||
}
|
||||
|
||||
private void observeChapterDownloadProgress() {
|
||||
downloadProgressSubscription = getPresenter().getDownloadProgressObs()
|
||||
.subscribe(this::onDownloadProgressChange,
|
||||
|
@ -2,14 +2,15 @@ package eu.kanade.tachiyomi.ui.manga.chapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.DecimalFormat;
|
||||
import java.text.DecimalFormatSymbols;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import butterknife.Bind;
|
||||
@ -23,23 +24,19 @@ import rx.Observable;
|
||||
|
||||
public class ChaptersHolder extends FlexibleViewHolder {
|
||||
|
||||
private final ChaptersAdapter adapter;
|
||||
private final int readColor;
|
||||
private final int unreadColor;
|
||||
private final DecimalFormat decimalFormat;
|
||||
private final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT);
|
||||
@Bind(R.id.chapter_title) TextView title;
|
||||
@Bind(R.id.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;
|
||||
|
||||
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;
|
||||
@ -71,6 +68,7 @@ public class ChaptersHolder extends FlexibleViewHolder {
|
||||
}
|
||||
title.setText(name);
|
||||
title.setTextColor(chapter.read ? readColor : unreadColor);
|
||||
date.setTextColor(chapter.read ? readColor : unreadColor);
|
||||
|
||||
if (!chapter.read && chapter.last_page_read > 0) {
|
||||
pages.setText(context.getString(R.string.chapter_progress, chapter.last_page_read + 1));
|
||||
@ -79,7 +77,7 @@ public class ChaptersHolder extends FlexibleViewHolder {
|
||||
}
|
||||
|
||||
onStatusChange(chapter.status);
|
||||
date.setText(sdf.format(new Date(chapter.date_upload)));
|
||||
date.setText(df.format(new Date(chapter.date_upload)));
|
||||
}
|
||||
|
||||
public void onStatusChange(int status) {
|
||||
@ -109,19 +107,36 @@ public class ChaptersHolder extends FlexibleViewHolder {
|
||||
// Inflate our menu resource into the PopupMenu's Menu
|
||||
popup.getMenuInflater().inflate(R.menu.chapter_single, popup.getMenu());
|
||||
|
||||
// Hide download and show delete if the chapter is downloaded and
|
||||
if(item.isDownloaded()) {
|
||||
Menu menu = popup.getMenu();
|
||||
menu.findItem(R.id.action_download).setVisible(false);
|
||||
menu.findItem(R.id.action_delete).setVisible(true);
|
||||
}
|
||||
|
||||
// Hide mark as unread when the chapter is unread
|
||||
if(!item.read && item.last_page_read == 0) {
|
||||
popup.getMenu().findItem(R.id.action_mark_as_unread).setVisible(false);
|
||||
}
|
||||
|
||||
// Hide mark as read when the chapter is read
|
||||
if(item.read) {
|
||||
popup.getMenu().findItem(R.id.action_mark_as_read).setVisible(false);
|
||||
}
|
||||
|
||||
// Set a listener so we are notified if a menu item is clicked
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
Observable<Chapter> chapter = Observable.just(item);
|
||||
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.action_mark_as_read:
|
||||
return adapter.getFragment().onMarkAsRead(chapter);
|
||||
case R.id.action_mark_as_unread:
|
||||
return adapter.getFragment().onMarkAsUnread(chapter);
|
||||
case R.id.action_download:
|
||||
return adapter.getFragment().onDownload(chapter);
|
||||
case R.id.action_delete:
|
||||
return adapter.getFragment().onDelete(chapter);
|
||||
case R.id.action_mark_as_read:
|
||||
return adapter.getFragment().onMarkAsRead(chapter);
|
||||
case R.id.action_mark_as_unread:
|
||||
return adapter.getFragment().onMarkAsUnread(chapter);
|
||||
case R.id.action_mark_previous_as_read:
|
||||
return adapter.getFragment().onMarkPreviousAsRead(item);
|
||||
}
|
||||
|
@ -3,11 +3,14 @@ package eu.kanade.tachiyomi.ui.manga.chapter;
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
@ -21,7 +24,6 @@ import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
|
||||
import eu.kanade.tachiyomi.event.MangaEvent;
|
||||
import eu.kanade.tachiyomi.event.ReaderEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import icepick.State;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
@ -52,38 +54,27 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
}
|
||||
|
||||
chaptersSubject = PublishSubject.create();
|
||||
|
||||
restartableLatestCache(GET_MANGA,
|
||||
startableLatestCache(GET_MANGA,
|
||||
() -> Observable.just(manga),
|
||||
ChaptersFragment::onNextManga);
|
||||
|
||||
restartableLatestCache(DB_CHAPTERS,
|
||||
startableLatestCache(DB_CHAPTERS,
|
||||
this::getDbChaptersObs,
|
||||
ChaptersFragment::onNextChapters);
|
||||
|
||||
restartableFirst(FETCH_CHAPTERS,
|
||||
startableFirst(FETCH_CHAPTERS,
|
||||
this::getOnlineChaptersObs,
|
||||
(view, result) -> view.onFetchChaptersDone(),
|
||||
(view, error) -> view.onFetchChaptersError());
|
||||
(view, error) -> view.onFetchChaptersError(error));
|
||||
|
||||
restartableLatestCache(CHAPTER_STATUS_CHANGES,
|
||||
startableLatestCache(CHAPTER_STATUS_CHANGES,
|
||||
this::getChapterStatusObs,
|
||||
(view, download) -> view.onChapterStatusChange(download),
|
||||
(view, error) -> Timber.e(error.getCause(), error.getMessage()));
|
||||
|
||||
registerForStickyEvents();
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
stop(GET_MANGA);
|
||||
stop(DB_CHAPTERS);
|
||||
stop(FETCH_CHAPTERS);
|
||||
stop(CHAPTER_STATUS_CHANGES);
|
||||
registerForEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -93,8 +84,8 @@ public class ChaptersPresenter extends BasePresenter<ChaptersFragment> {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(MangaEvent event) {
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(MangaEvent event) {
|
||||
this.manga = event.manga;
|
||||
start(GET_MANGA);
|
||||
|
||||
|
@ -1,8 +1,5 @@
|
||||
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;
|
||||
@ -10,178 +7,237 @@ import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
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;
|
||||
|
||||
/**
|
||||
* Fragment that shows manga information.
|
||||
* Uses R.layout.fragment_manga_info.
|
||||
* UI related actions should be called from here.
|
||||
*/
|
||||
@RequiresPresenter(MangaInfoPresenter.class)
|
||||
public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
|
||||
|
||||
private static final int REQUEST_IMAGE_OPEN = 101;
|
||||
/**
|
||||
* SwipeRefreshLayout showing refresh status
|
||||
*/
|
||||
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
|
||||
@Bind(R.id.manga_artist) TextView artist;
|
||||
@Bind(R.id.manga_author) TextView author;
|
||||
@Bind(R.id.manga_chapters) TextView chapterCount;
|
||||
@Bind(R.id.manga_genres) TextView genres;
|
||||
@Bind(R.id.manga_status) TextView status;
|
||||
@Bind(R.id.manga_source) TextView source;
|
||||
@Bind(R.id.manga_summary) TextView description;
|
||||
@Bind(R.id.manga_cover) ImageView cover;
|
||||
@Bind(R.id.action_favorite) Button favoriteBtn;
|
||||
@Bind(R.id.fab_edit) FloatingActionButton fabEdit;
|
||||
|
||||
/**
|
||||
* TextView containing artist information.
|
||||
*/
|
||||
@Bind(R.id.manga_artist) TextView artist;
|
||||
|
||||
/**
|
||||
* TextView containing author information.
|
||||
*/
|
||||
@Bind(R.id.manga_author) TextView author;
|
||||
|
||||
/**
|
||||
* TextView containing chapter count.
|
||||
*/
|
||||
@Bind(R.id.manga_chapters) TextView chapterCount;
|
||||
|
||||
/**
|
||||
* TextView containing genres.
|
||||
*/
|
||||
@Bind(R.id.manga_genres) TextView genres;
|
||||
|
||||
/**
|
||||
* TextView containing status (ongoing, finished).
|
||||
*/
|
||||
@Bind(R.id.manga_status) TextView status;
|
||||
|
||||
/**
|
||||
* TextView containing source.
|
||||
*/
|
||||
@Bind(R.id.manga_source) TextView source;
|
||||
|
||||
/**
|
||||
* TextView containing manga summary.
|
||||
*/
|
||||
@Bind(R.id.manga_summary) TextView description;
|
||||
|
||||
/**
|
||||
* ImageView of cover.
|
||||
*/
|
||||
@Bind(R.id.manga_cover) ImageView cover;
|
||||
|
||||
/**
|
||||
* ImageView containing manga cover shown as blurred backdrop.
|
||||
*/
|
||||
@Bind(R.id.backdrop) ImageView backdrop;
|
||||
|
||||
/**
|
||||
* FAB anchored to bottom of top view used to (add / remove) manga (to / from) library.
|
||||
*/
|
||||
@Bind(R.id.fab_favorite) FloatingActionButton fabFavorite;
|
||||
|
||||
/**
|
||||
* Create new instance of MangaInfoFragment.
|
||||
*
|
||||
* @return MangaInfoFragment.
|
||||
*/
|
||||
public static MangaInfoFragment newInstance() {
|
||||
return new MangaInfoFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
// Inflate the layout for this fragment
|
||||
// Inflate the layout for this fragment.
|
||||
View view = inflater.inflate(R.layout.fragment_manga_info, container, false);
|
||||
|
||||
// Bind layout objects.
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
//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());
|
||||
// Set onclickListener to toggle favorite when FAB clicked.
|
||||
fabFavorite.setOnClickListener(v -> getPresenter().toggleFavorite());
|
||||
|
||||
// Set SwipeRefresh to refresh manga data.
|
||||
swipeRefresh.setOnRefreshListener(this::fetchMangaFromSource);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if manga is initialized.
|
||||
* If true update view with manga information,
|
||||
* if false fetch manga information
|
||||
*
|
||||
* @param manga manga object containing information about manga.
|
||||
* @param source the source of the manga.
|
||||
*/
|
||||
public void onNextManga(Manga manga, Source source) {
|
||||
if (manga.initialized) {
|
||||
// Update view.
|
||||
setMangaInfo(manga, source);
|
||||
} else {
|
||||
// Initialize manga
|
||||
// Initialize manga.
|
||||
fetchMangaFromSource();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the info of the manga
|
||||
* Update the view with manga information.
|
||||
*
|
||||
* @param manga manga object containing information about manga
|
||||
* @param mangaSource the source 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) {
|
||||
// Update artist TextView.
|
||||
artist.setText(manga.artist);
|
||||
|
||||
// Update author TextView.
|
||||
author.setText(manga.author);
|
||||
|
||||
// If manga source is known update source TextView.
|
||||
if (mangaSource != null) {
|
||||
source.setText(mangaSource.getName());
|
||||
}
|
||||
|
||||
// Update genres TextView.
|
||||
genres.setText(manga.genre);
|
||||
|
||||
// Update status TextView.
|
||||
status.setText(manga.getStatus(getActivity()));
|
||||
|
||||
// Update description TextView.
|
||||
description.setText(manga.description);
|
||||
|
||||
setFavoriteText(manga.favorite);
|
||||
// Set the favorite drawable to the correct one.
|
||||
setFavoriteDrawable(manga.favorite);
|
||||
|
||||
// Initialize CoverCache and Glide headers to retrieve cover information.
|
||||
CoverCache coverCache = getPresenter().coverCache;
|
||||
LazyHeaders headers = getPresenter().source.getGlideHeaders();
|
||||
if (manga.thumbnail_url != null && cover.getDrawable() == null) {
|
||||
if (manga.favorite) {
|
||||
coverCache.saveOrLoadFromCache(cover, manga.thumbnail_url, headers);
|
||||
} else {
|
||||
coverCache.loadFromNetwork(cover, manga.thumbnail_url, headers);
|
||||
|
||||
// Check if thumbnail_url is given.
|
||||
if (manga.thumbnail_url != null) {
|
||||
// Check if cover is already drawn.
|
||||
if (cover.getDrawable() == null) {
|
||||
// If manga is in library then (download / save) (from / to) local cache if available,
|
||||
// else download from network.
|
||||
if (manga.favorite) {
|
||||
coverCache.saveOrLoadFromCache(cover, manga.thumbnail_url, headers);
|
||||
} else {
|
||||
coverCache.loadFromNetwork(cover, manga.thumbnail_url, headers);
|
||||
}
|
||||
}
|
||||
// Check if backdrop is already drawn.
|
||||
if (backdrop.getDrawable() == null) {
|
||||
// If manga is in library then (download / save) (from / to) local cache if available,
|
||||
// else download from network.
|
||||
if (manga.favorite) {
|
||||
coverCache.saveOrLoadFromCache(backdrop, manga.thumbnail_url, headers);
|
||||
} else {
|
||||
coverCache.loadFromNetwork(backdrop, manga.thumbnail_url, headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update chapter count TextView.
|
||||
*
|
||||
* @param count number of chapters.
|
||||
*/
|
||||
public void setChapterCount(int count) {
|
||||
chapterCount.setText(String.valueOf(count));
|
||||
}
|
||||
|
||||
private void setFavoriteText(boolean isFavorite) {
|
||||
favoriteBtn.setText(!isFavorite ? R.string.add_to_library : R.string.remove_from_library);
|
||||
/**
|
||||
* Update FAB with correct drawable.
|
||||
*
|
||||
* @param isFavorite determines if manga is favorite or not.
|
||||
*/
|
||||
private void setFavoriteDrawable(boolean isFavorite) {
|
||||
// Set the Favorite drawable to the correct one.
|
||||
// Border drawable if false, filled drawable if true.
|
||||
fabFavorite.setImageDrawable(ContextCompat.getDrawable(getContext(), isFavorite ?
|
||||
R.drawable.ic_bookmark_white_24dp :
|
||||
R.drawable.ic_bookmark_border_white_24dp));
|
||||
}
|
||||
|
||||
/**
|
||||
* Start fetching manga information from source.
|
||||
*/
|
||||
private void fetchMangaFromSource() {
|
||||
setRefreshing(true);
|
||||
// Call presenter and start fetching manga information
|
||||
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();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update swipeRefresh to stop showing refresh in progress spinner.
|
||||
*/
|
||||
public void onFetchMangaDone() {
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update swipeRefresh to start showing refresh in progress spinner.
|
||||
*/
|
||||
public void onFetchMangaError() {
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set swipeRefresh status.
|
||||
*
|
||||
* @param value status of manga fetch.
|
||||
*/
|
||||
private void setRefreshing(boolean value) {
|
||||
swipeRefresh.setRefreshing(value);
|
||||
}
|
||||
|
@ -1,10 +1,9 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.info;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.IOException;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
@ -16,47 +15,59 @@ import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.event.ChapterCountEvent;
|
||||
import eu.kanade.tachiyomi.event.MangaEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Presenter of MangaInfoFragment.
|
||||
* Contains information and data for fragment.
|
||||
* Observable updates should be called from here.
|
||||
*/
|
||||
public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
||||
|
||||
/**
|
||||
* 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
|
||||
* Source information.
|
||||
*/
|
||||
protected Source source;
|
||||
|
||||
/**
|
||||
* Used to connect to database
|
||||
* Used to connect to database.
|
||||
*/
|
||||
@Inject DatabaseHelper db;
|
||||
|
||||
/**
|
||||
* Used to connect to different manga sources
|
||||
* Used to connect to different manga sources.
|
||||
*/
|
||||
@Inject SourceManager sourceManager;
|
||||
|
||||
/**
|
||||
* Used to connect to cache
|
||||
* Used to connect to cache.
|
||||
*/
|
||||
@Inject CoverCache coverCache;
|
||||
|
||||
/**
|
||||
* Selected manga information
|
||||
* Selected manga information.
|
||||
*/
|
||||
private Manga manga;
|
||||
|
||||
/**
|
||||
* Count of chapters
|
||||
* Count of chapters.
|
||||
*/
|
||||
private int count = -1;
|
||||
|
||||
@ -64,37 +75,24 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
}
|
||||
|
||||
// Update manga cache
|
||||
restartableLatestCache(GET_MANGA,
|
||||
// Notify the view a manga is available or has changed.
|
||||
startableLatestCache(GET_MANGA,
|
||||
() -> Observable.just(manga),
|
||||
(view, manga) -> view.onNextManga(manga, source));
|
||||
|
||||
// Update chapter count
|
||||
restartableLatestCache(GET_CHAPTER_COUNT,
|
||||
// Update chapter count.
|
||||
startableLatestCache(GET_CHAPTER_COUNT,
|
||||
() -> Observable.just(count),
|
||||
MangaInfoFragment::setChapterCount);
|
||||
|
||||
// Fetch manga info from source
|
||||
restartableFirst(FETCH_MANGA_INFO,
|
||||
// Fetch manga info from source.
|
||||
startableFirst(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);
|
||||
stop(FETCH_MANGA_INFO);
|
||||
// Listen for events.
|
||||
registerForEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -103,23 +101,24 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(MangaEvent event) {
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(MangaEvent event) {
|
||||
this.manga = event.manga;
|
||||
source = sourceManager.get(manga.source);
|
||||
refreshManga();
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(ChapterCountEvent event) {
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(ChapterCountEvent event) {
|
||||
if (count != event.getCount()) {
|
||||
count = event.getCount();
|
||||
// Update chapter count
|
||||
start(GET_CHAPTER_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch manga info from source
|
||||
* Fetch manga information from source.
|
||||
*/
|
||||
public void fetchMangaFromSource() {
|
||||
if (isUnsubscribed(FETCH_MANGA_INFO)) {
|
||||
@ -127,6 +126,11 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch manga information from source.
|
||||
*
|
||||
* @return manga information.
|
||||
*/
|
||||
private Observable<Manga> fetchMangaObs() {
|
||||
return source.pullMangaFromNetwork(manga.url)
|
||||
.flatMap(networkManga -> {
|
||||
@ -139,6 +143,9 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
||||
.doOnNext(manga -> refreshManga());
|
||||
}
|
||||
|
||||
/**
|
||||
* Update favorite status of manga, (removes / adds) manga (to / from) library.
|
||||
*/
|
||||
public void toggleFavorite() {
|
||||
manga.favorite = !manga.favorite;
|
||||
onMangaFavoriteChange(manga.favorite);
|
||||
@ -146,21 +153,12 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
||||
refreshManga();
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Update cover with local file
|
||||
* (Removes / Saves) cover depending on favorite status.
|
||||
*
|
||||
* @param isFavorite determines if manga is favorite or not.
|
||||
*/
|
||||
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());
|
||||
@ -169,12 +167,10 @@ public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
||||
}
|
||||
}
|
||||
|
||||
public Manga getManga() {
|
||||
return manga;
|
||||
}
|
||||
|
||||
// Used to refresh the view
|
||||
protected void refreshManga() {
|
||||
/**
|
||||
* Refresh MangaInfo view.
|
||||
*/
|
||||
private void refreshManga() {
|
||||
start(GET_MANGA);
|
||||
}
|
||||
|
||||
|
@ -11,7 +11,6 @@ import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
@ -25,11 +24,6 @@ import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.subjects.PublishSubject;
|
||||
import uk.co.ribot.easyadapter.EasyAdapter;
|
||||
import uk.co.ribot.easyadapter.ItemViewHolder;
|
||||
import uk.co.ribot.easyadapter.PositionInfo;
|
||||
import uk.co.ribot.easyadapter.annotations.LayoutId;
|
||||
import uk.co.ribot.easyadapter.annotations.ViewId;
|
||||
|
||||
public class MyAnimeListDialogFragment extends DialogFragment {
|
||||
|
||||
@ -37,7 +31,7 @@ public class MyAnimeListDialogFragment extends DialogFragment {
|
||||
@Bind(R.id.myanimelist_search_results) ListView searchResults;
|
||||
@Bind(R.id.progress) ProgressBar progressBar;
|
||||
|
||||
private EasyAdapter<MangaSync> adapter;
|
||||
private MyAnimeListSearchAdapter adapter;
|
||||
private MangaSync selectedItem;
|
||||
|
||||
private Subscription searchSubscription;
|
||||
@ -59,7 +53,7 @@ public class MyAnimeListDialogFragment extends DialogFragment {
|
||||
ButterKnife.bind(this, dialog.getView());
|
||||
|
||||
// Create adapter
|
||||
adapter = new EasyAdapter<>(getActivity(), ResultViewHolder.class);
|
||||
adapter = new MyAnimeListSearchAdapter(getActivity());
|
||||
searchResults.setAdapter(adapter);
|
||||
|
||||
// Set listeners
|
||||
@ -125,7 +119,7 @@ public class MyAnimeListDialogFragment extends DialogFragment {
|
||||
public void onSearchResultsError() {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
searchResults.setVisibility(View.VISIBLE);
|
||||
adapter.getItems().clear();
|
||||
adapter.clear();
|
||||
}
|
||||
|
||||
public MyAnimeListFragment getMALFragment() {
|
||||
@ -136,21 +130,6 @@ public class MyAnimeListDialogFragment extends DialogFragment {
|
||||
return getMALFragment().getPresenter();
|
||||
}
|
||||
|
||||
@LayoutId(R.layout.dialog_myanimelist_search_item)
|
||||
public static class ResultViewHolder extends ItemViewHolder<MangaSync> {
|
||||
|
||||
@ViewId(R.id.myanimelist_result_title) TextView title;
|
||||
|
||||
public ResultViewHolder(View view) {
|
||||
super(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetValues(MangaSync chapter, PositionInfo positionInfo) {
|
||||
title.setText(chapter.title);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SimpleTextChangeListener implements TextWatcher {
|
||||
|
||||
@Override
|
||||
|
@ -4,6 +4,9 @@ import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
@ -16,7 +19,6 @@ import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
||||
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
|
||||
import eu.kanade.tachiyomi.event.MangaEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
@ -44,20 +46,16 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
}
|
||||
|
||||
myAnimeList = syncManager.getMyAnimeList();
|
||||
|
||||
restartableLatestCache(GET_MANGA_SYNC,
|
||||
startableLatestCache(GET_MANGA_SYNC,
|
||||
() -> db.getMangaSync(manga, myAnimeList).asRxObservable()
|
||||
.doOnNext(mangaSync -> this.mangaSync = mangaSync)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread()),
|
||||
MyAnimeListFragment::setMangaSync);
|
||||
|
||||
restartableLatestCache(GET_SEARCH_RESULTS,
|
||||
startableLatestCache(GET_SEARCH_RESULTS,
|
||||
this::getSearchResultsObservable,
|
||||
(view, results) -> {
|
||||
view.setSearchResults(results);
|
||||
@ -66,7 +64,7 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
|
||||
view.setSearchResultsError();
|
||||
});
|
||||
|
||||
restartableFirst(REFRESH,
|
||||
startableFirst(REFRESH,
|
||||
() -> myAnimeList.getList()
|
||||
.flatMap(myList -> {
|
||||
for (MangaSync myManga : myList) {
|
||||
@ -86,16 +84,10 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
|
||||
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
stop(GET_MANGA_SYNC);
|
||||
stop(GET_SEARCH_RESULTS);
|
||||
stop(REFRESH);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTakeView(MyAnimeListFragment view) {
|
||||
super.onTakeView(view);
|
||||
registerForStickyEvents();
|
||||
registerForEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -104,8 +96,8 @@ public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
|
||||
super.onDropView();
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(MangaEvent event) {
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(MangaEvent event) {
|
||||
this.manga = event.manga;
|
||||
start(GET_MANGA_SYNC);
|
||||
}
|
||||
|
@ -0,0 +1,61 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.myanimelist;
|
||||
|
||||
import android.content.Context;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.ArrayAdapter;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
|
||||
public class MyAnimeListSearchAdapter extends ArrayAdapter<MangaSync> {
|
||||
|
||||
public MyAnimeListSearchAdapter(Context context) {
|
||||
super(context, R.layout.dialog_myanimelist_search_item, new ArrayList<>());
|
||||
}
|
||||
|
||||
@Override
|
||||
public View getView(int position, View view, ViewGroup parent) {
|
||||
// Get the data item for this position
|
||||
MangaSync sync = getItem(position);
|
||||
// Check if an existing view is being reused, otherwise inflate the view
|
||||
SearchViewHolder holder; // view lookup cache stored in tag
|
||||
if (view == null) {
|
||||
LayoutInflater inflater = LayoutInflater.from(getContext());
|
||||
view = inflater.inflate(R.layout.dialog_myanimelist_search_item, parent, false);
|
||||
holder = new SearchViewHolder(view);
|
||||
view.setTag(holder);
|
||||
} else {
|
||||
holder = (SearchViewHolder) view.getTag();
|
||||
}
|
||||
holder.onSetValues(sync);
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setItems(List<MangaSync> syncs) {
|
||||
setNotifyOnChange(false);
|
||||
clear();
|
||||
addAll(syncs);
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public static class SearchViewHolder {
|
||||
|
||||
@Bind(R.id.myanimelist_result_title) TextView title;
|
||||
|
||||
public SearchViewHolder(View view) {
|
||||
ButterKnife.bind(this, view);
|
||||
}
|
||||
|
||||
public void onSetValues(MangaSync sync) {
|
||||
title.setText(sync.title);
|
||||
}
|
||||
}
|
||||
}
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.reader;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.pm.ActivityInfo;
|
||||
import android.content.res.Configuration;
|
||||
import android.graphics.Color;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
@ -10,9 +11,9 @@ 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.KeyEvent;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.Surface;
|
||||
import android.view.View;
|
||||
import android.view.WindowManager;
|
||||
import android.widget.TextView;
|
||||
@ -153,21 +154,57 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean dispatchKeyEvent(KeyEvent event) {
|
||||
int action = event.getAction();
|
||||
int keyCode = event.getKeyCode();
|
||||
switch (keyCode) {
|
||||
case KeyEvent.KEYCODE_VOLUME_DOWN:
|
||||
if (action == KeyEvent.ACTION_UP && viewer != null)
|
||||
viewer.moveToNext();
|
||||
return true;
|
||||
case KeyEvent.KEYCODE_VOLUME_UP:
|
||||
if (action == KeyEvent.ACTION_UP && viewer != null)
|
||||
viewer.moveToPrevious();
|
||||
return true;
|
||||
default:
|
||||
return super.dispatchKeyEvent(event);
|
||||
}
|
||||
}
|
||||
|
||||
public void onChapterError() {
|
||||
finish();
|
||||
ToastUtil.showShort(this, R.string.page_list_error);
|
||||
}
|
||||
|
||||
public void onChapterReady(List<Page> pages, Manga manga, Chapter chapter, int currentPage) {
|
||||
if (currentPage == -1) {
|
||||
currentPage = pages.size() - 1;
|
||||
public void onChapterAppendError() {
|
||||
// Ignore
|
||||
}
|
||||
|
||||
public void onChapterReady(Manga manga, Chapter chapter, Page currentPage) {
|
||||
List<Page> pages = chapter.getPages();
|
||||
if (currentPage == null) {
|
||||
currentPage = pages.get(pages.size() - 1);
|
||||
}
|
||||
|
||||
if (viewer == null) {
|
||||
viewer = getOrCreateViewer(manga);
|
||||
}
|
||||
viewer.onPageListReady(pages, currentPage);
|
||||
readerMenu.onChapterReady(pages.size(), manga, chapter, currentPage);
|
||||
viewer.onPageListReady(chapter, currentPage);
|
||||
readerMenu.setActiveManga(manga);
|
||||
readerMenu.setActiveChapter(chapter, currentPage.getPageNumber());
|
||||
}
|
||||
|
||||
public void onEnterChapter(Chapter chapter, int currentPage) {
|
||||
if (currentPage == -1) {
|
||||
currentPage = chapter.getPages().size() - 1;
|
||||
}
|
||||
getPresenter().setActiveChapter(chapter);
|
||||
readerMenu.setActiveChapter(chapter, currentPage);
|
||||
}
|
||||
|
||||
public void onAppendChapter(Chapter chapter) {
|
||||
viewer.onPageListAppendReady(chapter);
|
||||
}
|
||||
|
||||
public void onAdjacentChapters(Chapter previous, Chapter next) {
|
||||
@ -209,8 +246,9 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
|
||||
readerMenu.onPageChanged(currentPageIndex);
|
||||
}
|
||||
|
||||
public void setSelectedPage(int pageIndex) {
|
||||
viewer.setSelectedPage(pageIndex);
|
||||
public void gotoPageInCurrentChapter(int pageIndex) {
|
||||
Page requestedPage = viewer.getCurrentPage().getChapter().getPages().get(pageIndex);
|
||||
viewer.setSelectedPage(requestedPage);
|
||||
}
|
||||
|
||||
public void onCenterSingleTap() {
|
||||
@ -218,7 +256,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
|
||||
}
|
||||
|
||||
public void requestNextChapter() {
|
||||
getPresenter().setCurrentPage(viewer != null ? viewer.getCurrentPage() : 0);
|
||||
getPresenter().setCurrentPage(viewer.getCurrentPage());
|
||||
if (!getPresenter().loadNextChapter()) {
|
||||
ToastUtil.showShort(this, R.string.no_next_chapter);
|
||||
}
|
||||
@ -226,7 +264,7 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
|
||||
}
|
||||
|
||||
public void requestPreviousChapter() {
|
||||
getPresenter().setCurrentPage(viewer != null ? viewer.getCurrentPage() : 0);
|
||||
getPresenter().setCurrentPage(viewer.getCurrentPage());
|
||||
if (!getPresenter().loadPreviousChapter()) {
|
||||
ToastUtil.showShort(this, R.string.no_previous_chapter);
|
||||
}
|
||||
@ -239,9 +277,9 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
|
||||
.asObservable()
|
||||
.subscribe(this::setPageNumberVisibility));
|
||||
|
||||
subscriptions.add(preferences.lockOrientation()
|
||||
subscriptions.add(preferences.rotation()
|
||||
.asObservable()
|
||||
.subscribe(this::setOrientation));
|
||||
.subscribe(this::setRotation));
|
||||
|
||||
subscriptions.add(preferences.hideStatusBar()
|
||||
.asObservable()
|
||||
@ -261,28 +299,25 @@ public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
|
||||
.subscribe(this::applyTheme));
|
||||
}
|
||||
|
||||
private void setOrientation(boolean locked) {
|
||||
if (locked) {
|
||||
int orientation;
|
||||
int rotation = ((WindowManager) getSystemService(
|
||||
Context.WINDOW_SERVICE)).getDefaultDisplay().getRotation();
|
||||
switch (rotation) {
|
||||
case Surface.ROTATION_0:
|
||||
orientation = ActivityInfo.SCREEN_ORIENTATION_PORTRAIT;
|
||||
break;
|
||||
case Surface.ROTATION_90:
|
||||
orientation = ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE;
|
||||
break;
|
||||
case Surface.ROTATION_180:
|
||||
orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_PORTRAIT;
|
||||
break;
|
||||
default:
|
||||
orientation = ActivityInfo.SCREEN_ORIENTATION_REVERSE_LANDSCAPE;
|
||||
break;
|
||||
}
|
||||
setRequestedOrientation(orientation);
|
||||
} else {
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
private void setRotation(int rotation) {
|
||||
switch (rotation) {
|
||||
// Rotation free
|
||||
case 1:
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED);
|
||||
break;
|
||||
// Lock in current rotation
|
||||
case 2:
|
||||
int currentOrientation = getResources().getConfiguration().orientation;
|
||||
setRotation(currentOrientation == Configuration.ORIENTATION_PORTRAIT ? 3 : 4);
|
||||
break;
|
||||
// Lock in portrait
|
||||
case 3:
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_PORTRAIT);
|
||||
break;
|
||||
// Lock in landscape
|
||||
case 4:
|
||||
setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_SENSOR_LANDSCAPE);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.reader;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.content.Context;
|
||||
import android.content.res.Configuration;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.view.Gravity;
|
||||
import android.view.Menu;
|
||||
@ -43,9 +44,10 @@ public class ReaderMenu {
|
||||
@Bind(R.id.page_seeker) SeekBar seekBar;
|
||||
@Bind(R.id.total_pages) TextView totalPages;
|
||||
@Bind(R.id.lock_orientation) ImageButton lockOrientation;
|
||||
@Bind(R.id.reader_zoom_selector) ImageButton zoomSelector;
|
||||
@Bind(R.id.reader_scale_type_selector) ImageButton scaleTypeSelector;
|
||||
@Bind(R.id.reader_selector) ImageButton readerSelector;
|
||||
@Bind(R.id.reader_extra_settings) ImageButton extraSettings;
|
||||
@Bind(R.id.reader_scale_type_selector) ImageButton scaleTypeSelector;
|
||||
|
||||
private MenuItem nextChapterBtn;
|
||||
private MenuItem prevChapterBtn;
|
||||
@ -133,7 +135,7 @@ public class ReaderMenu {
|
||||
return true;
|
||||
}
|
||||
|
||||
public void onChapterReady(int numPages, Manga manga, Chapter chapter, int currentPageIndex) {
|
||||
public void setActiveManga(Manga manga) {
|
||||
if (manga.viewer == ReaderActivity.RIGHT_TO_LEFT && !inverted) {
|
||||
// Invert the seekbar and textview fields for the right to left reader
|
||||
seekBar.setRotation(180);
|
||||
@ -143,14 +145,17 @@ public class ReaderMenu {
|
||||
// Don't invert again on chapter change
|
||||
inverted = true;
|
||||
}
|
||||
activity.setToolbarTitle(manga.title);
|
||||
}
|
||||
|
||||
public void setActiveChapter(Chapter chapter, int currentPageIndex) {
|
||||
// Set initial values
|
||||
int numPages = chapter.getPages().size();
|
||||
totalPages.setText("" + numPages);
|
||||
currentPage.setText("" + (currentPageIndex + 1));
|
||||
seekBar.setMax(numPages - 1);
|
||||
seekBar.setProgress(currentPageIndex);
|
||||
|
||||
activity.setToolbarTitle(manga.title);
|
||||
activity.setToolbarSubtitle(chapter.chapter_number != -1 ?
|
||||
activity.getString(R.string.chapter_subtitle,
|
||||
decimalFormat.format(chapter.chapter_number)) :
|
||||
@ -174,24 +179,49 @@ public class ReaderMenu {
|
||||
if (nextChapterBtn != null) nextChapterBtn.setVisible(nextChapter != null);
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void initializeMenu() {
|
||||
// Orientation changes
|
||||
add(preferences.lockOrientation().asObservable()
|
||||
.subscribe(locked -> {
|
||||
int resourceId = !locked ? R.drawable.ic_screen_rotation :
|
||||
activity.getResources().getConfiguration().orientation == 1 ?
|
||||
// Orientation selector
|
||||
add(preferences.rotation().asObservable()
|
||||
.subscribe(value -> {
|
||||
boolean isPortrait = activity.getResources().getConfiguration()
|
||||
.orientation == Configuration.ORIENTATION_PORTRAIT;
|
||||
int resourceId = value == 1 ? R.drawable.ic_screen_rotation : isPortrait ?
|
||||
R.drawable.ic_screen_lock_portrait :
|
||||
R.drawable.ic_screen_lock_landscape;
|
||||
|
||||
lockOrientation.setImageResource(resourceId);
|
||||
}));
|
||||
|
||||
lockOrientation.setOnClickListener(v ->
|
||||
preferences.lockOrientation().set(!preferences.lockOrientation().get()));
|
||||
lockOrientation.setOnClickListener(v -> {
|
||||
showImmersiveDialog(new MaterialDialog.Builder(activity)
|
||||
.title(R.string.pref_rotation_type)
|
||||
.items(R.array.rotation_type)
|
||||
.itemsCallbackSingleChoice(preferences.rotation().get() - 1,
|
||||
(d, itemView, which, text) -> {
|
||||
preferences.rotation().set(which + 1);
|
||||
return true;
|
||||
})
|
||||
.build());
|
||||
});
|
||||
|
||||
// Zoom selector
|
||||
zoomSelector.setOnClickListener(v -> {
|
||||
showImmersiveDialog(new MaterialDialog.Builder(activity)
|
||||
.title(R.string.pref_zoom_start)
|
||||
.items(R.array.zoom_start)
|
||||
.itemsCallbackSingleChoice(preferences.zoomStart().get() - 1,
|
||||
(d, itemView, which, text) -> {
|
||||
preferences.zoomStart().set(which + 1);
|
||||
return true;
|
||||
})
|
||||
.build());
|
||||
});
|
||||
|
||||
// Scale type selector
|
||||
scaleTypeSelector.setOnClickListener(v -> {
|
||||
showImmersiveDialog(new MaterialDialog.Builder(activity)
|
||||
.title(R.string.pref_image_scale_type)
|
||||
.items(R.array.image_scale_type)
|
||||
.itemsCallbackSingleChoice(preferences.imageScaleType().get() - 1,
|
||||
(d, itemView, which, text) -> {
|
||||
@ -205,6 +235,7 @@ public class ReaderMenu {
|
||||
readerSelector.setOnClickListener(v -> {
|
||||
final Manga manga = activity.getPresenter().getManga();
|
||||
showImmersiveDialog(new MaterialDialog.Builder(activity)
|
||||
.title(R.string.pref_viewer_type)
|
||||
.items(R.array.viewers_selector)
|
||||
.itemsCallbackSingleChoice(manga.viewer,
|
||||
(d, itemView, which, text) -> {
|
||||
@ -260,6 +291,7 @@ public class ReaderMenu {
|
||||
initializePopupMenu();
|
||||
}
|
||||
|
||||
@SuppressWarnings("ConstantConditions")
|
||||
private void initializePopupMenu() {
|
||||
// Load values from preferences
|
||||
enableTransitions.setChecked(preferences.enableTransitions().get());
|
||||
@ -337,7 +369,7 @@ public class ReaderMenu {
|
||||
@Override
|
||||
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
|
||||
if (fromUser) {
|
||||
activity.setSelectedPage(progress);
|
||||
activity.gotoPageInCurrentChapter(progress);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,17 +4,21 @@ import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.util.Pair;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
import org.greenrobot.eventbus.Subscribe;
|
||||
import org.greenrobot.eventbus.ThreadMode;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
@ -24,9 +28,9 @@ import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.data.sync.UpdateMangaSyncService;
|
||||
import eu.kanade.tachiyomi.event.ReaderEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import icepick.State;
|
||||
import rx.Observable;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import rx.subjects.PublishSubject;
|
||||
@ -41,72 +45,55 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
||||
@Inject SourceManager sourceManager;
|
||||
|
||||
@State Manga manga;
|
||||
@State Chapter chapter;
|
||||
@State Chapter activeChapter;
|
||||
@State int sourceId;
|
||||
@State boolean isDownloaded;
|
||||
@State int currentPage;
|
||||
@State int requestedPage;
|
||||
private Page currentPage;
|
||||
private Source source;
|
||||
private Chapter nextChapter;
|
||||
private Chapter previousChapter;
|
||||
private List<Page> pageList;
|
||||
private List<Page> nextChapterPageList;
|
||||
private List<MangaSync> mangaSyncList;
|
||||
|
||||
private PublishSubject<Page> retryPageSubject;
|
||||
private PublishSubject<Chapter> pageInitializerSubject;
|
||||
|
||||
private boolean seamlessMode;
|
||||
private Subscription appenderSubscription;
|
||||
|
||||
private static final int GET_PAGE_LIST = 1;
|
||||
private static final int GET_PAGE_IMAGES = 2;
|
||||
private static final int GET_ADJACENT_CHAPTERS = 3;
|
||||
private static final int RETRY_IMAGES = 4;
|
||||
private static final int PRELOAD_NEXT_CHAPTER = 5;
|
||||
private static final int GET_MANGA_SYNC = 6;
|
||||
private static final int GET_ADJACENT_CHAPTERS = 2;
|
||||
private static final int GET_MANGA_SYNC = 3;
|
||||
private static final int PRELOAD_NEXT_CHAPTER = 4;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
source = sourceManager.get(sourceId);
|
||||
initializeSubjects();
|
||||
}
|
||||
|
||||
retryPageSubject = PublishSubject.create();
|
||||
seamlessMode = prefs.seamlessMode();
|
||||
|
||||
restartableLatestCache(PRELOAD_NEXT_CHAPTER,
|
||||
this::getPreloadNextChapterObservable,
|
||||
(view, pages) -> {},
|
||||
(view, error) -> Timber.e("An error occurred while preloading a chapter"));
|
||||
startableLatestCache(GET_ADJACENT_CHAPTERS, this::getAdjacentChaptersObservable,
|
||||
(view, pair) -> view.onAdjacentChapters(pair.first, pair.second));
|
||||
|
||||
restartableLatestCache(GET_PAGE_IMAGES,
|
||||
this::getPageImagesObservable,
|
||||
(view, page) -> {},
|
||||
(view, error) -> Timber.e("An error occurred while downloading an image"));
|
||||
startable(PRELOAD_NEXT_CHAPTER, this::getPreloadNextChapterObservable,
|
||||
next -> {},
|
||||
error -> Timber.e("Error preloading chapter"));
|
||||
|
||||
restartableLatestCache(GET_ADJACENT_CHAPTERS,
|
||||
this::getAdjacentChaptersObservable,
|
||||
(view, pair) -> view.onAdjacentChapters(pair.first, pair.second),
|
||||
(view, error) -> Timber.e("An error occurred while getting adjacent chapters"));
|
||||
|
||||
restartableLatestCache(RETRY_IMAGES,
|
||||
this::getRetryPageObservable,
|
||||
(view, page) -> {},
|
||||
(view, error) -> Timber.e("An error occurred while downloading an image"));
|
||||
restartable(GET_MANGA_SYNC, () -> getMangaSyncObservable().subscribe());
|
||||
|
||||
restartableLatestCache(GET_PAGE_LIST,
|
||||
() -> getPageListObservable()
|
||||
.doOnNext(pages -> pageList = pages)
|
||||
.doOnCompleted(() -> {
|
||||
start(GET_ADJACENT_CHAPTERS);
|
||||
start(GET_PAGE_IMAGES);
|
||||
start(RETRY_IMAGES);
|
||||
}),
|
||||
(view, pages) -> view.onChapterReady(pages, manga, chapter, currentPage),
|
||||
() -> getPageListObservable(activeChapter),
|
||||
(view, chapter) -> view.onChapterReady(manga, activeChapter, currentPage),
|
||||
(view, error) -> view.onChapterError());
|
||||
|
||||
restartableFirst(GET_MANGA_SYNC, this::getMangaSyncObservable,
|
||||
(view, mangaSync) -> {},
|
||||
(view, error) -> {});
|
||||
|
||||
registerForStickyEvents();
|
||||
if (savedState == null) {
|
||||
registerForEvents();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -121,59 +108,85 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
||||
super.onSave(state);
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
source = sourceManager.get(sourceId);
|
||||
|
||||
// These are started by GET_PAGE_LIST, so we don't let them restart itselves
|
||||
stop(GET_PAGE_IMAGES);
|
||||
stop(GET_ADJACENT_CHAPTERS);
|
||||
stop(RETRY_IMAGES);
|
||||
stop(PRELOAD_NEXT_CHAPTER);
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(ReaderEvent event) {
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(ReaderEvent event) {
|
||||
EventBus.getDefault().removeStickyEvent(event);
|
||||
manga = event.getManga();
|
||||
source = event.getSource();
|
||||
sourceId = source.getId();
|
||||
initializeSubjects();
|
||||
loadChapter(event.getChapter());
|
||||
if (prefs.autoUpdateMangaSync()) {
|
||||
start(GET_MANGA_SYNC);
|
||||
}
|
||||
}
|
||||
|
||||
private void initializeSubjects() {
|
||||
// Listen for pages initialization events
|
||||
pageInitializerSubject = PublishSubject.create();
|
||||
add(pageInitializerSubject
|
||||
.observeOn(Schedulers.io())
|
||||
.concatMap(chapter -> {
|
||||
Observable observable;
|
||||
if (chapter.isDownloaded()) {
|
||||
File chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter);
|
||||
observable = Observable.from(chapter.getPages())
|
||||
.flatMap(page -> downloadManager.getDownloadedImage(page, chapterDir));
|
||||
} else {
|
||||
observable = source.getAllImageUrlsFromPageList(chapter.getPages())
|
||||
.flatMap(source::getCachedImage, 2)
|
||||
.doOnCompleted(() -> source.savePageList(chapter.url, chapter.getPages()));
|
||||
}
|
||||
return observable.doOnCompleted(() -> {
|
||||
if (!seamlessMode && activeChapter == chapter) {
|
||||
preloadNextChapter();
|
||||
}
|
||||
});
|
||||
})
|
||||
.subscribe());
|
||||
|
||||
// Listen por retry events
|
||||
retryPageSubject = PublishSubject.create();
|
||||
add(retryPageSubject
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap(page -> page.getImageUrl() == null ?
|
||||
source.getImageUrlFromPage(page) :
|
||||
Observable.just(page))
|
||||
.flatMap(source::getCachedImage)
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
// Returns the page list of a chapter
|
||||
private Observable<List<Page>> getPageListObservable() {
|
||||
return isDownloaded ?
|
||||
private Observable<Chapter> getPageListObservable(Chapter chapter) {
|
||||
return (chapter.isDownloaded() ?
|
||||
// Fetch the page list from disk
|
||||
Observable.just(downloadManager.getSavedPageList(source, manga, chapter)) :
|
||||
// Fetch the page list from cache or fallback to network
|
||||
source.getCachedPageListOrPullFromNetwork(chapter.url)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
// Get the chapter images from network or disk
|
||||
private Observable<Page> getPageImagesObservable() {
|
||||
Observable<Page> pageObservable;
|
||||
|
||||
if (!isDownloaded) {
|
||||
pageObservable = source.getAllImageUrlsFromPageList(pageList)
|
||||
.flatMap(source::getCachedImage, 2);
|
||||
} else {
|
||||
File chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter);
|
||||
pageObservable = Observable.from(pageList)
|
||||
.flatMap(page -> downloadManager.getDownloadedImage(page, chapterDir));
|
||||
}
|
||||
return pageObservable.subscribeOn(Schedulers.io())
|
||||
.doOnCompleted(this::preloadNextChapter);
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
).map(pages -> {
|
||||
for (Page page : pages) {
|
||||
page.setChapter(chapter);
|
||||
}
|
||||
chapter.setPages(pages);
|
||||
if (requestedPage >= -1 || currentPage == null) {
|
||||
if (requestedPage == -1) {
|
||||
currentPage = pages.get(pages.size() - 1);
|
||||
} else {
|
||||
currentPage = pages.get(requestedPage);
|
||||
}
|
||||
}
|
||||
requestedPage = -2;
|
||||
pageInitializerSubject.onNext(chapter);
|
||||
return chapter;
|
||||
});
|
||||
}
|
||||
|
||||
private Observable<Pair<Chapter, Chapter>> getAdjacentChaptersObservable() {
|
||||
return Observable.zip(
|
||||
db.getPreviousChapter(chapter).asRxObservable().take(1),
|
||||
db.getNextChapter(chapter).asRxObservable().take(1),
|
||||
db.getPreviousChapter(activeChapter).asRxObservable().take(1),
|
||||
db.getNextChapter(activeChapter).asRxObservable().take(1),
|
||||
Pair::create)
|
||||
.doOnNext(pair -> {
|
||||
previousChapter = pair.first;
|
||||
@ -182,29 +195,22 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
// Listen for retry page events
|
||||
private Observable<Page> getRetryPageObservable() {
|
||||
return retryPageSubject
|
||||
.observeOn(Schedulers.io())
|
||||
.flatMap(page -> page.getImageUrl() == null ?
|
||||
source.getImageUrlFromPage(page) :
|
||||
Observable.just(page))
|
||||
.flatMap(source::getCachedImage)
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
// Preload the first pages of the next chapter
|
||||
// Preload the first pages of the next chapter. Only for non seamless mode
|
||||
private Observable<Page> getPreloadNextChapterObservable() {
|
||||
return source.getCachedPageListOrPullFromNetwork(nextChapter.url)
|
||||
.flatMap(pages -> {
|
||||
nextChapterPageList = pages;
|
||||
// Preload at most 5 pages
|
||||
nextChapter.setPages(pages);
|
||||
int pagesToPreload = Math.min(pages.size(), 5);
|
||||
return Observable.from(pages).take(pagesToPreload);
|
||||
})
|
||||
// Preload up to 5 images
|
||||
.concatMap(page -> page.getImageUrl() == null ?
|
||||
source.getImageUrlFromPage(page) :
|
||||
Observable.just(page))
|
||||
// Download the first image
|
||||
.concatMap(page -> page.getPageNumber() == 0 ?
|
||||
source.getCachedImage(page) :
|
||||
Observable.just(page))
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnCompleted(this::stopPreloadingNextChapter);
|
||||
@ -212,6 +218,7 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
||||
|
||||
private Observable<List<MangaSync>> getMangaSyncObservable() {
|
||||
return db.getMangasSync(manga).asRxObservable()
|
||||
.take(1)
|
||||
.doOnNext(mangaSync -> this.mangaSyncList = mangaSync);
|
||||
}
|
||||
|
||||
@ -221,24 +228,58 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
||||
|
||||
// Loads the given chapter
|
||||
private void loadChapter(Chapter chapter, int requestedPage) {
|
||||
// Before loading the chapter, stop preloading (if it's working) and save current progress
|
||||
stopPreloadingNextChapter();
|
||||
if (seamlessMode) {
|
||||
if (appenderSubscription != null)
|
||||
remove(appenderSubscription);
|
||||
} else {
|
||||
stopPreloadingNextChapter();
|
||||
}
|
||||
|
||||
this.chapter = chapter;
|
||||
isDownloaded = isChapterDownloaded(chapter);
|
||||
this.activeChapter = chapter;
|
||||
chapter.status = isChapterDownloaded(chapter) ? Download.DOWNLOADED : Download.NOT_DOWNLOADED;
|
||||
|
||||
// If the chapter is partially read, set the starting page to the last the user read
|
||||
if (!chapter.read && chapter.last_page_read != 0)
|
||||
currentPage = chapter.last_page_read;
|
||||
this.requestedPage = chapter.last_page_read;
|
||||
else
|
||||
currentPage = requestedPage;
|
||||
this.requestedPage = requestedPage;
|
||||
|
||||
// Reset next and previous chapter. They have to be fetched again
|
||||
nextChapter = null;
|
||||
previousChapter = null;
|
||||
nextChapterPageList = null;
|
||||
|
||||
start(GET_PAGE_LIST);
|
||||
start(GET_ADJACENT_CHAPTERS);
|
||||
}
|
||||
|
||||
public void setActiveChapter(Chapter chapter) {
|
||||
onChapterLeft();
|
||||
this.activeChapter = chapter;
|
||||
nextChapter = null;
|
||||
previousChapter = null;
|
||||
start(GET_ADJACENT_CHAPTERS);
|
||||
}
|
||||
|
||||
public void appendNextChapter() {
|
||||
if (nextChapter == null)
|
||||
return;
|
||||
|
||||
if (appenderSubscription != null)
|
||||
remove(appenderSubscription);
|
||||
|
||||
nextChapter.status = isChapterDownloaded(nextChapter) ? Download.DOWNLOADED : Download.NOT_DOWNLOADED;
|
||||
|
||||
appenderSubscription = getPageListObservable(nextChapter)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.compose(deliverLatestCache())
|
||||
.subscribe(split((view, chapter) -> {
|
||||
view.onAppendChapter(chapter);
|
||||
}, (view, error) -> {
|
||||
view.onChapterAppendError();
|
||||
}));
|
||||
|
||||
add(appenderSubscription);
|
||||
}
|
||||
|
||||
// Check whether the given chapter is downloaded
|
||||
@ -254,34 +295,39 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
||||
// Called before loading another chapter or leaving the reader. It allows to do operations
|
||||
// over the chapter read like saving progress
|
||||
public void onChapterLeft() {
|
||||
if (pageList == null)
|
||||
List<Page> pages = activeChapter.getPages();
|
||||
if (pages == null)
|
||||
return;
|
||||
|
||||
// Get the last page read
|
||||
int activePageNumber = activeChapter.last_page_read;
|
||||
|
||||
// Just in case, avoid out of index exceptions
|
||||
if (activePageNumber >= pages.size()) {
|
||||
activePageNumber = pages.size() - 1;
|
||||
}
|
||||
Page activePage = pages.get(activePageNumber);
|
||||
|
||||
// Cache current page list progress for online chapters to allow a faster reopen
|
||||
if (!isDownloaded)
|
||||
source.savePageList(chapter.url, pageList);
|
||||
if (!activeChapter.isDownloaded()) {
|
||||
source.savePageList(activeChapter.url, pages);
|
||||
}
|
||||
|
||||
// Save current progress of the chapter. Mark as read if the chapter is finished
|
||||
chapter.last_page_read = currentPage;
|
||||
if (isChapterFinished()) {
|
||||
chapter.read = true;
|
||||
if (activePage.isLastPage()) {
|
||||
activeChapter.read = true;
|
||||
}
|
||||
db.insertChapter(chapter).asRxObservable().subscribe();
|
||||
}
|
||||
|
||||
// Check whether the chapter has been read
|
||||
private boolean isChapterFinished() {
|
||||
return !chapter.read && currentPage == pageList.size() - 1;
|
||||
db.insertChapter(activeChapter).asRxObservable().subscribe();
|
||||
}
|
||||
|
||||
public int getMangaSyncChapterToUpdate() {
|
||||
if (pageList == null || mangaSyncList == null || mangaSyncList.isEmpty())
|
||||
if (activeChapter.getPages() == null || mangaSyncList == null || mangaSyncList.isEmpty())
|
||||
return 0;
|
||||
|
||||
int lastChapterReadLocal = 0;
|
||||
// If the current chapter has been read, we check with this one
|
||||
if (chapter.read)
|
||||
lastChapterReadLocal = (int) Math.floor(chapter.chapter_number);
|
||||
if (activeChapter.read)
|
||||
lastChapterReadLocal = (int) Math.floor(activeChapter.chapter_number);
|
||||
// If not, we check if the previous chapter has been read
|
||||
else if (previousChapter != null && previousChapter.read)
|
||||
lastChapterReadLocal = (int) Math.floor(previousChapter.chapter_number);
|
||||
@ -309,7 +355,7 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
||||
}
|
||||
}
|
||||
|
||||
public void setCurrentPage(int currentPage) {
|
||||
public void setCurrentPage(Page currentPage) {
|
||||
this.currentPage = currentPage;
|
||||
}
|
||||
|
||||
@ -348,8 +394,8 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
||||
private void stopPreloadingNextChapter() {
|
||||
if (!isUnsubscribed(PRELOAD_NEXT_CHAPTER)) {
|
||||
stop(PRELOAD_NEXT_CHAPTER);
|
||||
if (nextChapterPageList != null)
|
||||
source.savePageList(nextChapter.url, nextChapterPageList);
|
||||
if (nextChapter.getPages() != null)
|
||||
source.savePageList(nextChapter.url, nextChapter.getPages());
|
||||
}
|
||||
}
|
||||
|
||||
@ -362,4 +408,11 @@ public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
||||
return manga;
|
||||
}
|
||||
|
||||
public Page getCurrentPage() {
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
public boolean isSeamlessMode() {
|
||||
return seamlessMode;
|
||||
}
|
||||
}
|
||||
|
@ -1,15 +1,15 @@
|
||||
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.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
|
||||
@ -18,40 +18,86 @@ public abstract class BaseReader extends BaseFragment {
|
||||
|
||||
protected int currentPage;
|
||||
protected List<Page> pages;
|
||||
protected List<Chapter> chapters;
|
||||
protected Class<? extends ImageRegionDecoder> regionDecoderClass;
|
||||
protected Class<? extends ImageDecoder> bitmapDecoderClass;
|
||||
|
||||
private boolean hasRequestedNextChapter;
|
||||
|
||||
public static final int RAPID_DECODER = 0;
|
||||
public static final int SKIA_DECODER = 1;
|
||||
|
||||
public void updatePageNumber() {
|
||||
getReaderActivity().onPageChanged(getCurrentPage(), getTotalPages());
|
||||
getReaderActivity().onPageChanged(getCurrentPage().getPageNumber(), getCurrentPage().getChapter().getPages().size());
|
||||
}
|
||||
|
||||
public int getCurrentPage() {
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
public int getPageForPosition(int position) {
|
||||
return position;
|
||||
}
|
||||
|
||||
public int getPositionForPage(int page) {
|
||||
return page;
|
||||
public Page getCurrentPage() {
|
||||
return pages.get(currentPage);
|
||||
}
|
||||
|
||||
public void onPageChanged(int position) {
|
||||
currentPage = getPageForPosition(position);
|
||||
Page oldPage = pages.get(currentPage);
|
||||
Page newPage = pages.get(position);
|
||||
newPage.getChapter().last_page_read = newPage.getPageNumber();
|
||||
|
||||
if (getReaderActivity().getPresenter().isSeamlessMode()) {
|
||||
Chapter oldChapter = oldPage.getChapter();
|
||||
Chapter newChapter = newPage.getChapter();
|
||||
if (!hasRequestedNextChapter && position > pages.size() - 5) {
|
||||
hasRequestedNextChapter = true;
|
||||
getReaderActivity().getPresenter().appendNextChapter();
|
||||
}
|
||||
if (!oldChapter.id.equals(newChapter.id)) {
|
||||
onChapterChanged(newPage.getChapter(), newPage);
|
||||
}
|
||||
}
|
||||
currentPage = position;
|
||||
updatePageNumber();
|
||||
}
|
||||
|
||||
public int getTotalPages() {
|
||||
return pages == null ? 0 : pages.size();
|
||||
private void onChapterChanged(Chapter chapter, Page currentPage) {
|
||||
getReaderActivity().onEnterChapter(chapter, currentPage.getPageNumber());
|
||||
}
|
||||
|
||||
public void setSelectedPage(Page page) {
|
||||
setSelectedPage(getPageIndex(page));
|
||||
}
|
||||
|
||||
public int getPageIndex(Page search) {
|
||||
// search for the index of a page in the current list without requiring them to be the same object
|
||||
for (Page page : pages) {
|
||||
if (page.getPageNumber() == search.getPageNumber() &&
|
||||
page.getChapter().id.equals(search.getChapter().id)) {
|
||||
return pages.indexOf(page);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
public void onPageListReady(Chapter chapter, Page currentPage) {
|
||||
if (chapters == null || !chapters.contains(chapter)) {
|
||||
// if we reset the loaded page we also need to reset the loaded chapters
|
||||
chapters = new ArrayList<>();
|
||||
chapters.add(chapter);
|
||||
onSetChapter(chapter, currentPage);
|
||||
} else {
|
||||
setSelectedPage(currentPage);
|
||||
}
|
||||
}
|
||||
|
||||
public void onPageListAppendReady(Chapter chapter) {
|
||||
if (!chapters.contains(chapter)) {
|
||||
hasRequestedNextChapter = false;
|
||||
chapters.add(chapter);
|
||||
onAppendChapter(chapter);
|
||||
}
|
||||
}
|
||||
|
||||
public abstract void setSelectedPage(int pageNumber);
|
||||
public abstract void onPageListReady(List<Page> pages, int currentPage);
|
||||
public abstract boolean onImageTouch(MotionEvent motionEvent);
|
||||
public abstract void onSetChapter(Chapter chapter, Page currentPage);
|
||||
public abstract void onAppendChapter(Chapter chapter);
|
||||
public abstract void moveToNext();
|
||||
public abstract void moveToPrevious();
|
||||
|
||||
public void setDecoderClass(int value) {
|
||||
switch (value) {
|
||||
|
@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.base;
|
||||
|
||||
public interface OnChapterSingleTapListener {
|
||||
void onCenterTap();
|
||||
void onLeftSideTap();
|
||||
void onRightSideTap();
|
||||
}
|
@ -1,4 +1,4 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.base;
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager;
|
||||
|
||||
public interface OnChapterBoundariesOutListener {
|
||||
void onFirstPageOutEvent();
|
@ -1,11 +1,8 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager;
|
||||
|
||||
import android.support.v4.view.PagerAdapter;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener;
|
||||
import rx.functions.Action1;
|
||||
|
||||
public interface Pager {
|
||||
@ -24,13 +21,7 @@ public interface Pager {
|
||||
PagerAdapter getAdapter();
|
||||
void setAdapter(PagerAdapter adapter);
|
||||
|
||||
boolean onImageTouch(MotionEvent motionEvent);
|
||||
|
||||
void setOnChapterBoundariesOutListener(OnChapterBoundariesOutListener listener);
|
||||
void setOnChapterSingleTapListener(OnChapterSingleTapListener listener);
|
||||
|
||||
OnChapterBoundariesOutListener getChapterBoundariesListener();
|
||||
OnChapterSingleTapListener getChapterSingleTapListener();
|
||||
|
||||
void setOnPageChangeListener(Action1<Integer> onPageChanged);
|
||||
void clearOnPageChangeListeners();
|
||||
|
@ -1,71 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager;
|
||||
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
public class PagerGestureListener extends GestureDetector.SimpleOnGestureListener {
|
||||
|
||||
private Pager pager;
|
||||
|
||||
private static final float LEFT_REGION = 0.33f;
|
||||
private static final float RIGHT_REGION = 0.66f;
|
||||
|
||||
public PagerGestureListener(Pager pager) {
|
||||
this.pager = pager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
final int position = pager.getCurrentItem();
|
||||
final float positionX = e.getX();
|
||||
|
||||
if (positionX < pager.getWidth() * LEFT_REGION) {
|
||||
if (position != 0) {
|
||||
onLeftSideTap();
|
||||
} else {
|
||||
onFirstPageOut();
|
||||
}
|
||||
} else if (positionX > pager.getWidth() * RIGHT_REGION) {
|
||||
if (position != pager.getAdapter().getCount() - 1) {
|
||||
onRightSideTap();
|
||||
} else {
|
||||
onLastPageOut();
|
||||
}
|
||||
} else {
|
||||
onCenterTap();
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private void onLeftSideTap() {
|
||||
if (pager.getChapterSingleTapListener() != null) {
|
||||
pager.getChapterSingleTapListener().onLeftSideTap();
|
||||
}
|
||||
}
|
||||
|
||||
private void onRightSideTap() {
|
||||
if (pager.getChapterSingleTapListener() != null) {
|
||||
pager.getChapterSingleTapListener().onRightSideTap();
|
||||
}
|
||||
}
|
||||
|
||||
private void onCenterTap() {
|
||||
if (pager.getChapterSingleTapListener() != null) {
|
||||
pager.getChapterSingleTapListener().onCenterTap();
|
||||
}
|
||||
}
|
||||
|
||||
private void onFirstPageOut() {
|
||||
if (pager.getChapterBoundariesListener() != null) {
|
||||
pager.getChapterBoundariesListener().onFirstPageOutEvent();
|
||||
}
|
||||
}
|
||||
|
||||
private void onLastPageOut() {
|
||||
if (pager.getChapterBoundariesListener() != null) {
|
||||
pager.getChapterBoundariesListener().onLastPageOutEvent();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -1,15 +1,18 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager;
|
||||
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader;
|
||||
import rx.subscriptions.CompositeSubscription;
|
||||
|
||||
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
|
||||
@ -18,12 +21,21 @@ public abstract class PagerReader extends BaseReader {
|
||||
|
||||
protected PagerReaderAdapter adapter;
|
||||
protected Pager pager;
|
||||
protected GestureDetector gestureDetector;
|
||||
|
||||
private boolean isReady;
|
||||
protected boolean transitions;
|
||||
protected CompositeSubscription subscriptions;
|
||||
|
||||
protected int scaleType = 1;
|
||||
protected int zoomStart = 1;
|
||||
|
||||
public static final int ALIGN_AUTO = 1;
|
||||
public static final int ALIGN_LEFT = 2;
|
||||
public static final int ALIGN_RIGHT = 3;
|
||||
public static final int ALIGN_CENTER = 4;
|
||||
|
||||
private static final float LEFT_REGION = 0.33f;
|
||||
private static final float RIGHT_REGION = 0.66f;
|
||||
|
||||
protected void initializePager(Pager pager) {
|
||||
this.pager = pager;
|
||||
@ -33,55 +45,47 @@ public abstract class PagerReader extends BaseReader {
|
||||
pager.setOnChapterBoundariesOutListener(new OnChapterBoundariesOutListener() {
|
||||
@Override
|
||||
public void onFirstPageOutEvent() {
|
||||
onFirstPageOut();
|
||||
getReaderActivity().requestPreviousChapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLastPageOutEvent() {
|
||||
onLastPageOut();
|
||||
}
|
||||
});
|
||||
pager.setOnChapterSingleTapListener(new OnChapterSingleTapListener() {
|
||||
@Override
|
||||
public void onCenterTap() {
|
||||
getReaderActivity().onCenterSingleTap();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLeftSideTap() {
|
||||
pager.setCurrentItem(pager.getCurrentItem() - 1, transitions);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onRightSideTap() {
|
||||
pager.setCurrentItem(pager.getCurrentItem() + 1, transitions);
|
||||
getReaderActivity().requestNextChapter();
|
||||
}
|
||||
});
|
||||
gestureDetector = createGestureDetector();
|
||||
|
||||
adapter = new PagerReaderAdapter(getChildFragmentManager());
|
||||
pager.setAdapter(adapter);
|
||||
|
||||
PreferencesHelper preferences = getReaderActivity().getPreferences();
|
||||
subscriptions = new CompositeSubscription();
|
||||
subscriptions.add(getReaderActivity().getPreferences().imageDecoder()
|
||||
subscriptions.add(preferences.imageDecoder()
|
||||
.asObservable()
|
||||
.doOnNext(this::setDecoderClass)
|
||||
.skip(1)
|
||||
.distinctUntilChanged()
|
||||
.subscribe(v -> adapter.notifyDataSetChanged()));
|
||||
.subscribe(v -> pager.setAdapter(adapter)));
|
||||
|
||||
subscriptions.add(getReaderActivity().getPreferences().imageScaleType()
|
||||
subscriptions.add(preferences.imageScaleType()
|
||||
.asObservable()
|
||||
.doOnNext(this::setImageScaleType)
|
||||
.skip(1)
|
||||
.distinctUntilChanged()
|
||||
.subscribe(v -> adapter.notifyDataSetChanged()));
|
||||
.subscribe(v -> pager.setAdapter(adapter)));
|
||||
|
||||
subscriptions.add(getReaderActivity().getPreferences().enableTransitions()
|
||||
subscriptions.add(preferences.zoomStart()
|
||||
.asObservable()
|
||||
.doOnNext(this::setZoomStart)
|
||||
.skip(1)
|
||||
.distinctUntilChanged()
|
||||
.subscribe(v -> pager.setAdapter(adapter)));
|
||||
|
||||
subscriptions.add(preferences.enableTransitions()
|
||||
.asObservable()
|
||||
.subscribe(value -> transitions = value));
|
||||
|
||||
setPages();
|
||||
isReady = true;
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -90,14 +94,41 @@ public abstract class PagerReader extends BaseReader {
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageListReady(List<Page> pages, int currentPage) {
|
||||
if (this.pages != pages) {
|
||||
this.pages = pages;
|
||||
this.currentPage = currentPage;
|
||||
if (isReady) {
|
||||
setPages();
|
||||
protected GestureDetector createGestureDetector() {
|
||||
return new GestureDetector(getContext(), new GestureDetector.SimpleOnGestureListener() {
|
||||
@Override
|
||||
public boolean onSingleTapConfirmed(MotionEvent e) {
|
||||
final float positionX = e.getX();
|
||||
|
||||
if (positionX < pager.getWidth() * LEFT_REGION) {
|
||||
onLeftSideTap();
|
||||
} else if (positionX > pager.getWidth() * RIGHT_REGION) {
|
||||
onRightSideTap();
|
||||
} else {
|
||||
getReaderActivity().onCenterSingleTap();
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetChapter(Chapter chapter, Page currentPage) {
|
||||
pages = new ArrayList<>(chapter.getPages());
|
||||
this.currentPage = getPageIndex(currentPage); // we might have a new page object
|
||||
|
||||
// This method can be called before the view is created
|
||||
if (pager != null) {
|
||||
setPages();
|
||||
}
|
||||
}
|
||||
|
||||
public void onAppendChapter(Chapter chapter) {
|
||||
pages.addAll(chapter.getPages());
|
||||
|
||||
// This method can be called before the view is created
|
||||
if (pager != null) {
|
||||
adapter.setPages(pages);
|
||||
}
|
||||
}
|
||||
|
||||
@ -113,19 +144,48 @@ public abstract class PagerReader extends BaseReader {
|
||||
|
||||
@Override
|
||||
public void setSelectedPage(int pageNumber) {
|
||||
pager.setCurrentItem(getPositionForPage(pageNumber), false);
|
||||
pager.setCurrentItem(pageNumber, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onImageTouch(MotionEvent motionEvent) {
|
||||
return pager.onImageTouch(motionEvent);
|
||||
protected void onLeftSideTap() {
|
||||
moveToPrevious();
|
||||
}
|
||||
|
||||
protected void onRightSideTap() {
|
||||
moveToNext();
|
||||
}
|
||||
|
||||
public void moveToNext() {
|
||||
if (pager.getCurrentItem() != pager.getAdapter().getCount() - 1) {
|
||||
pager.setCurrentItem(pager.getCurrentItem() + 1, transitions);
|
||||
} else {
|
||||
getReaderActivity().requestNextChapter();
|
||||
}
|
||||
}
|
||||
|
||||
public void moveToPrevious() {
|
||||
if (pager.getCurrentItem() != 0) {
|
||||
pager.setCurrentItem(pager.getCurrentItem() - 1, transitions);
|
||||
} else {
|
||||
getReaderActivity().requestPreviousChapter();
|
||||
}
|
||||
}
|
||||
|
||||
private void setImageScaleType(int scaleType) {
|
||||
this.scaleType = scaleType;
|
||||
}
|
||||
|
||||
public abstract void onFirstPageOut();
|
||||
public abstract void onLastPageOut();
|
||||
private void setZoomStart(int zoomStart) {
|
||||
if (zoomStart == ALIGN_AUTO) {
|
||||
if (this instanceof LeftToRightReader)
|
||||
setZoomStart(ALIGN_LEFT);
|
||||
else if (this instanceof RightToLeftReader)
|
||||
setZoomStart(ALIGN_RIGHT);
|
||||
else
|
||||
setZoomStart(ALIGN_CENTER);
|
||||
} else {
|
||||
this.zoomStart = zoomStart;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -31,14 +31,10 @@ public class PagerReaderAdapter extends FragmentStatePagerAdapter {
|
||||
public Object instantiateItem(ViewGroup container, int position) {
|
||||
PagerReaderFragment f = (PagerReaderFragment) super.instantiateItem(container, position);
|
||||
f.setPage(pages.get(position));
|
||||
f.setPosition(position);
|
||||
return f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(Object object) {
|
||||
return POSITION_NONE;
|
||||
}
|
||||
|
||||
public List<Page> getPages() {
|
||||
return pages;
|
||||
}
|
||||
@ -48,4 +44,17 @@ public class PagerReaderAdapter extends FragmentStatePagerAdapter {
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemPosition(Object object) {
|
||||
PagerReaderFragment f = (PagerReaderFragment) object;
|
||||
int position = f.getPosition();
|
||||
if (position >= 0 && position < getCount()) {
|
||||
if (pages.get(position) == f.getPage()) {
|
||||
return POSITION_UNCHANGED;
|
||||
} else {
|
||||
return POSITION_NONE;
|
||||
}
|
||||
}
|
||||
return super.getItemPosition(object);
|
||||
}
|
||||
}
|
||||
|
@ -1,5 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager;
|
||||
|
||||
import android.graphics.PointF;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
@ -16,6 +17,7 @@ import android.widget.TextView;
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource;
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
|
||||
|
||||
import java.io.File;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.concurrent.atomic.AtomicInteger;
|
||||
|
||||
@ -25,6 +27,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.pager.horizontal.RightToLeftReader;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader;
|
||||
import rx.Observable;
|
||||
import rx.Subscription;
|
||||
@ -41,9 +44,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;
|
||||
private int position = -1;
|
||||
|
||||
private int lightGreyColor;
|
||||
private int blackColor;
|
||||
|
||||
public static PagerReaderFragment newInstance() {
|
||||
return new PagerReaderFragment();
|
||||
@ -56,8 +62,15 @@ public class PagerReaderFragment extends BaseFragment {
|
||||
ReaderActivity activity = getReaderActivity();
|
||||
PagerReader parentFragment = (PagerReader) getParentFragment();
|
||||
|
||||
lightGreyColor = ContextCompat.getColor(getContext(), R.color.light_grey);
|
||||
blackColor = ContextCompat.getColor(getContext(), R.color.primary_text);
|
||||
|
||||
if (activity.getReaderTheme() == ReaderActivity.BLACK_THEME) {
|
||||
progressText.setTextColor(ContextCompat.getColor(getContext(), R.color.light_grey));
|
||||
progressText.setTextColor(lightGreyColor);
|
||||
}
|
||||
|
||||
if (parentFragment instanceof RightToLeftReader) {
|
||||
view.setRotation(-180);
|
||||
}
|
||||
|
||||
imageView.setParallelLoadingEnabled(true);
|
||||
@ -65,11 +78,29 @@ public class PagerReaderFragment extends BaseFragment {
|
||||
imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED);
|
||||
imageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE);
|
||||
imageView.setMinimumScaleType(parentFragment.scaleType);
|
||||
imageView.setMinimumDpi(50);
|
||||
imageView.setRegionDecoderClass(parentFragment.getRegionDecoderClass());
|
||||
imageView.setBitmapDecoderClass(parentFragment.getBitmapDecoderClass());
|
||||
imageView.setVerticalScrollingParent(parentFragment instanceof VerticalReader);
|
||||
imageView.setOnTouchListener((v, motionEvent) -> parentFragment.onImageTouch(motionEvent));
|
||||
imageView.setOnTouchListener((v, motionEvent) -> parentFragment.gestureDetector.onTouchEvent(motionEvent));
|
||||
imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() {
|
||||
@Override
|
||||
public void onReady() {
|
||||
switch (parentFragment.zoomStart) {
|
||||
case PagerReader.ALIGN_LEFT:
|
||||
imageView.setScaleAndCenter(imageView.getScale(), new PointF(0, 0));
|
||||
break;
|
||||
case PagerReader.ALIGN_RIGHT:
|
||||
imageView.setScaleAndCenter(imageView.getScale(), new PointF(imageView.getSWidth(), 0));
|
||||
break;
|
||||
case PagerReader.ALIGN_CENTER:
|
||||
PointF center = imageView.getCenter();
|
||||
center.y = 0;
|
||||
imageView.setScaleAndCenter(imageView.getScale(), center);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onImageLoadError(Exception e) {
|
||||
showImageLoadError();
|
||||
@ -85,7 +116,6 @@ public class PagerReaderFragment extends BaseFragment {
|
||||
});
|
||||
|
||||
observeStatus();
|
||||
isReady = true;
|
||||
return view;
|
||||
}
|
||||
|
||||
@ -93,23 +123,36 @@ public class PagerReaderFragment extends BaseFragment {
|
||||
public void onDestroyView() {
|
||||
unsubscribeProgress();
|
||||
unsubscribeStatus();
|
||||
imageView.setOnTouchListener(null);
|
||||
imageView.setOnImageEventListener(null);
|
||||
ButterKnife.unbind(this);
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
public void setPage(Page page) {
|
||||
this.page = page;
|
||||
if (isReady) {
|
||||
|
||||
// This method can be called before the view is created
|
||||
if (imageView != null) {
|
||||
observeStatus();
|
||||
}
|
||||
}
|
||||
|
||||
public void setPosition(int position) {
|
||||
this.position = position;
|
||||
}
|
||||
|
||||
private void showImage() {
|
||||
if (page == null || page.getImagePath() == null)
|
||||
return;
|
||||
|
||||
imageView.setImage(ImageSource.uri(page.getImagePath()));
|
||||
progressContainer.setVisibility(View.GONE);
|
||||
File imagePath = new File(page.getImagePath());
|
||||
if (imagePath.exists()) {
|
||||
imageView.setImage(ImageSource.uri(page.getImagePath()));
|
||||
progressContainer.setVisibility(View.GONE);
|
||||
} else {
|
||||
page.setStatus(Page.ERROR);
|
||||
}
|
||||
}
|
||||
|
||||
private void showDownloading() {
|
||||
@ -141,8 +184,7 @@ public class PagerReaderFragment extends BaseFragment {
|
||||
errorText.setGravity(Gravity.CENTER);
|
||||
errorText.setText(R.string.decode_image_error);
|
||||
errorText.setTextColor(getReaderActivity().getReaderTheme() == ReaderActivity.BLACK_THEME ?
|
||||
ContextCompat.getColor(getContext(), R.color.light_grey) :
|
||||
ContextCompat.getColor(getContext(), R.color.primary_text));
|
||||
lightGreyColor : blackColor);
|
||||
|
||||
view.addView(errorText);
|
||||
}
|
||||
@ -162,7 +204,6 @@ public class PagerReaderFragment extends BaseFragment {
|
||||
case Page.READY:
|
||||
showImage();
|
||||
unsubscribeProgress();
|
||||
unsubscribeStatus();
|
||||
break;
|
||||
case Page.ERROR:
|
||||
showError();
|
||||
@ -217,6 +258,14 @@ public class PagerReaderFragment extends BaseFragment {
|
||||
}
|
||||
}
|
||||
|
||||
public Page getPage() {
|
||||
return page;
|
||||
}
|
||||
|
||||
public int getPosition() {
|
||||
return position;
|
||||
}
|
||||
|
||||
private ReaderActivity getReaderActivity() {
|
||||
return (ReaderActivity) getActivity();
|
||||
}
|
||||
|
@ -2,38 +2,21 @@ package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.OnChapterBoundariesOutListener;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerGestureListener;
|
||||
import rx.functions.Action1;
|
||||
|
||||
public class HorizontalPager extends ViewPager implements Pager {
|
||||
|
||||
private GestureDetector gestureDetector;
|
||||
|
||||
private OnChapterBoundariesOutListener onChapterBoundariesOutListener;
|
||||
private OnChapterSingleTapListener onChapterSingleTapListener;
|
||||
|
||||
private static final float SWIPE_TOLERANCE = 0.25f;
|
||||
private float startDragX;
|
||||
|
||||
public HorizontalPager(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public HorizontalPager(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
gestureDetector = new GestureDetector(context, new PagerGestureListener(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -86,31 +69,11 @@ public class HorizontalPager extends ViewPager implements Pager {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onImageTouch(MotionEvent event) {
|
||||
return gestureDetector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnChapterBoundariesOutListener(OnChapterBoundariesOutListener listener) {
|
||||
onChapterBoundariesOutListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnChapterSingleTapListener(OnChapterSingleTapListener listener) {
|
||||
onChapterSingleTapListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OnChapterBoundariesOutListener getChapterBoundariesListener() {
|
||||
return onChapterBoundariesOutListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OnChapterSingleTapListener getChapterSingleTapListener() {
|
||||
return onChapterSingleTapListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnPageChangeListener(Action1<Integer> function) {
|
||||
addOnPageChangeListener(new SimpleOnPageChangeListener() {
|
||||
|
@ -1,19 +0,0 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader;
|
||||
|
||||
public abstract class HorizontalReader extends PagerReader {
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
|
||||
HorizontalPager pager = new HorizontalPager(getActivity());
|
||||
initializePager(pager);
|
||||
return pager;
|
||||
}
|
||||
|
||||
}
|
@ -1,15 +1,19 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
|
||||
|
||||
public class LeftToRightReader extends HorizontalReader {
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader;
|
||||
|
||||
public class LeftToRightReader extends PagerReader {
|
||||
|
||||
@Override
|
||||
public void onFirstPageOut() {
|
||||
getReaderActivity().requestPreviousChapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLastPageOut() {
|
||||
getReaderActivity().requestNextChapter();
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
|
||||
HorizontalPager pager = new HorizontalPager(getActivity());
|
||||
initializePager(pager);
|
||||
return pager;
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,38 +1,30 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerReader;
|
||||
|
||||
public class RightToLeftReader extends HorizontalReader {
|
||||
public class RightToLeftReader extends PagerReader {
|
||||
|
||||
@Override
|
||||
public void onPageListReady(List<Page> pages, int currentPage) {
|
||||
ArrayList<Page> inversedPages = new ArrayList<>(pages);
|
||||
Collections.reverse(inversedPages);
|
||||
super.onPageListReady(inversedPages, currentPage);
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
|
||||
HorizontalPager pager = new HorizontalPager(getActivity());
|
||||
pager.setRotation(180);
|
||||
initializePager(pager);
|
||||
return pager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPageForPosition(int position) {
|
||||
return (getTotalPages() - 1) - position;
|
||||
protected void onLeftSideTap() {
|
||||
moveToNext();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPositionForPage(int page) {
|
||||
return (getTotalPages() - 1) - page;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFirstPageOut() {
|
||||
getReaderActivity().requestNextChapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLastPageOut() {
|
||||
getReaderActivity().requestPreviousChapter();
|
||||
protected void onRightSideTap() {
|
||||
moveToPrevious();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -1,38 +1,21 @@
|
||||
package eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical;
|
||||
|
||||
import android.content.Context;
|
||||
import android.util.AttributeSet;
|
||||
import android.view.GestureDetector;
|
||||
import android.view.MotionEvent;
|
||||
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterBoundariesOutListener;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.OnChapterSingleTapListener;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.OnChapterBoundariesOutListener;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.pager.PagerGestureListener;
|
||||
import rx.functions.Action1;
|
||||
|
||||
public class VerticalPager extends VerticalViewPagerImpl implements Pager {
|
||||
|
||||
private GestureDetector gestureDetector;
|
||||
|
||||
private OnChapterBoundariesOutListener onChapterBoundariesOutListener;
|
||||
private OnChapterSingleTapListener onChapterSingleTapListener;
|
||||
|
||||
private static final float SWIPE_TOLERANCE = 0.25f;
|
||||
private float startDragY;
|
||||
|
||||
public VerticalPager(Context context) {
|
||||
super(context);
|
||||
init(context);
|
||||
}
|
||||
|
||||
public VerticalPager(Context context, AttributeSet attrs) {
|
||||
super(context, attrs);
|
||||
init(context);
|
||||
}
|
||||
|
||||
private void init(Context context) {
|
||||
gestureDetector = new GestureDetector(context, new PagerGestureListener(this));
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -85,31 +68,11 @@ public class VerticalPager extends VerticalViewPagerImpl implements Pager {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onImageTouch(MotionEvent event) {
|
||||
return gestureDetector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnChapterBoundariesOutListener(OnChapterBoundariesOutListener listener) {
|
||||
onChapterBoundariesOutListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnChapterSingleTapListener(OnChapterSingleTapListener listener) {
|
||||
onChapterSingleTapListener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OnChapterBoundariesOutListener getChapterBoundariesListener() {
|
||||
return onChapterBoundariesOutListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OnChapterSingleTapListener getChapterSingleTapListener() {
|
||||
return onChapterSingleTapListener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setOnPageChangeListener(Action1<Integer> function) {
|
||||
addOnPageChangeListener(new SimpleOnPageChangeListener() {
|
||||
|
@ -16,14 +16,4 @@ public class VerticalReader extends PagerReader {
|
||||
return pager;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onFirstPageOut() {
|
||||
getReaderActivity().requestPreviousChapter();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLastPageOut() {
|
||||
getReaderActivity().requestNextChapter();
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -21,7 +21,7 @@ public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> {
|
||||
public WebtoonAdapter(WebtoonReader fragment) {
|
||||
this.fragment = fragment;
|
||||
pages = new ArrayList<>();
|
||||
touchListener = (v, event) -> fragment.onImageTouch(event);
|
||||
touchListener = (v, event) -> fragment.gestureDetector.onTouchEvent(event);
|
||||
}
|
||||
|
||||
public Page getItem(int position) {
|
||||
|
@ -10,6 +10,8 @@ import android.widget.ProgressBar;
|
||||
import com.davemorrissey.labs.subscaleview.ImageSource;
|
||||
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
|
||||
|
||||
import java.io.File;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
@ -62,7 +64,6 @@ public class WebtoonHolder extends RecyclerView.ViewHolder {
|
||||
if (event.getAction() == MotionEvent.ACTION_UP) {
|
||||
if (page != null)
|
||||
adapter.retryPage(page);
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
@ -99,7 +100,14 @@ public class WebtoonHolder extends RecyclerView.ViewHolder {
|
||||
setErrorButtonVisible(false);
|
||||
setProgressVisible(false);
|
||||
setImageVisible(true);
|
||||
imageView.setImage(ImageSource.uri(page.getImagePath()));
|
||||
|
||||
File imagePath = new File(page.getImagePath());
|
||||
if (imagePath.exists()) {
|
||||
imageView.setImage(ImageSource.uri(page.getImagePath()));
|
||||
} else {
|
||||
page.setStatus(Page.ERROR);
|
||||
onError();
|
||||
}
|
||||
}
|
||||
|
||||
private void onError() {
|
||||
|
@ -8,8 +8,9 @@ import android.view.MotionEvent;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.ArrayList;
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
|
||||
import eu.kanade.tachiyomi.widget.PreCachingLayoutManager;
|
||||
@ -27,12 +28,11 @@ public class WebtoonReader extends BaseReader {
|
||||
private PreCachingLayoutManager layoutManager;
|
||||
private Subscription subscription;
|
||||
private Subscription decoderSubscription;
|
||||
private GestureDetector gestureDetector;
|
||||
protected GestureDetector gestureDetector;
|
||||
|
||||
private boolean isReady;
|
||||
private int scrollDistance;
|
||||
|
||||
private static final String SCROLL_STATE = "scroll_state";
|
||||
private static final String SAVED_POSITION = "saved_position";
|
||||
|
||||
private static final float LEFT_REGION = 0.33f;
|
||||
private static final float RIGHT_REGION = 0.66f;
|
||||
@ -47,7 +47,7 @@ public class WebtoonReader extends BaseReader {
|
||||
layoutManager = new PreCachingLayoutManager(getActivity());
|
||||
layoutManager.setExtraLayoutSpace(screenHeight / 2);
|
||||
if (savedState != null) {
|
||||
layoutManager.onRestoreInstanceState(savedState.getParcelable(SCROLL_STATE));
|
||||
layoutManager.scrollToPositionWithOffset(savedState.getInt(SAVED_POSITION), 0);
|
||||
}
|
||||
|
||||
recycler = new RecyclerView(getActivity());
|
||||
@ -69,9 +69,9 @@ public class WebtoonReader extends BaseReader {
|
||||
final float positionX = e.getX();
|
||||
|
||||
if (positionX < recycler.getWidth() * LEFT_REGION) {
|
||||
recycler.smoothScrollBy(0, -scrollDistance);
|
||||
moveToPrevious();
|
||||
} else if (positionX > recycler.getWidth() * RIGHT_REGION) {
|
||||
recycler.smoothScrollBy(0, scrollDistance);
|
||||
moveToNext();
|
||||
} else {
|
||||
getReaderActivity().onCenterSingleTap();
|
||||
}
|
||||
@ -80,8 +80,6 @@ public class WebtoonReader extends BaseReader {
|
||||
});
|
||||
|
||||
setPages();
|
||||
isReady = true;
|
||||
|
||||
return recycler;
|
||||
}
|
||||
|
||||
@ -100,7 +98,9 @@ public class WebtoonReader extends BaseReader {
|
||||
@Override
|
||||
public void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putParcelable(SCROLL_STATE, layoutManager.onSaveInstanceState());
|
||||
int savedPosition = pages != null ?
|
||||
pages.get(layoutManager.findFirstVisibleItemPosition()).getPageNumber() : 0;
|
||||
outState.putInt(SAVED_POSITION, savedPosition);
|
||||
}
|
||||
|
||||
private void unsubscribeStatus() {
|
||||
@ -110,17 +110,42 @@ public class WebtoonReader extends BaseReader {
|
||||
|
||||
@Override
|
||||
public void setSelectedPage(int pageNumber) {
|
||||
recycler.scrollToPosition(getPositionForPage(pageNumber));
|
||||
recycler.scrollToPosition(pageNumber);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPageListReady(List<Page> pages, int currentPage) {
|
||||
if (this.pages != pages) {
|
||||
this.pages = pages;
|
||||
// Restoring current page is not supported. It's getting weird scrolling jumps
|
||||
// this.currentPage = currentPage;
|
||||
if (isReady) {
|
||||
setPages();
|
||||
public void moveToNext() {
|
||||
recycler.smoothScrollBy(0, scrollDistance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void moveToPrevious() {
|
||||
recycler.smoothScrollBy(0, -scrollDistance);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetChapter(Chapter chapter, Page currentPage) {
|
||||
pages = new ArrayList<>(chapter.getPages());
|
||||
// Restoring current page is not supported. It's getting weird scrolling jumps
|
||||
// this.currentPage = currentPage;
|
||||
|
||||
// This method can be called before the view is created
|
||||
if (recycler != null) {
|
||||
setPages();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onAppendChapter(Chapter chapter) {
|
||||
int insertStart = pages.size();
|
||||
pages.addAll(chapter.getPages());
|
||||
|
||||
// This method can be called before the view is created
|
||||
if (recycler != null) {
|
||||
adapter.setPages(pages);
|
||||
adapter.notifyItemRangeInserted(insertStart, chapter.getPages().size());
|
||||
if (subscription != null && subscription.isUnsubscribed()) {
|
||||
observeStatus(insertStart);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -141,22 +166,19 @@ public class WebtoonReader extends BaseReader {
|
||||
recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
|
||||
@Override
|
||||
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
|
||||
super.onScrolled(recyclerView, dx, dy);
|
||||
|
||||
currentPage = layoutManager.findLastVisibleItemPosition();
|
||||
updatePageNumber();
|
||||
int page = layoutManager.findLastVisibleItemPosition();
|
||||
if (page != currentPage) {
|
||||
onPageChanged(page);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onImageTouch(MotionEvent motionEvent) {
|
||||
return gestureDetector.onTouchEvent(motionEvent);
|
||||
}
|
||||
|
||||
private void observeStatus(int position) {
|
||||
if (position == pages.size())
|
||||
if (position == pages.size()) {
|
||||
unsubscribeStatus();
|
||||
return;
|
||||
}
|
||||
|
||||
final Page page = pages.get(position);
|
||||
|
||||
|
@ -16,13 +16,33 @@ import eu.davidea.flexibleadapter.FlexibleAdapter;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
|
||||
|
||||
/**
|
||||
* Adapter of RecentChaptersHolder.
|
||||
* Connection between Fragment and Holder
|
||||
* Holder updates should be called from here.
|
||||
*/
|
||||
public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHolder, Object> {
|
||||
|
||||
private RecentChaptersFragment fragment;
|
||||
/**
|
||||
* Fragment of RecentChaptersFragment
|
||||
*/
|
||||
private final RecentChaptersFragment fragment;
|
||||
|
||||
/**
|
||||
* The id of the view type
|
||||
*/
|
||||
private static final int VIEW_TYPE_CHAPTER = 0;
|
||||
|
||||
/**
|
||||
* The id of the view type
|
||||
*/
|
||||
private static final int VIEW_TYPE_SECTION = 1;
|
||||
|
||||
/**
|
||||
* Constructor
|
||||
*
|
||||
* @param fragment fragment
|
||||
*/
|
||||
public RecentChaptersAdapter(RecentChaptersFragment fragment) {
|
||||
this.fragment = fragment;
|
||||
setHasStableIds(true);
|
||||
@ -37,6 +57,11 @@ public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHold
|
||||
return item.hashCode();
|
||||
}
|
||||
|
||||
/**
|
||||
* Update items
|
||||
*
|
||||
* @param items items
|
||||
*/
|
||||
public void setItems(List<Object> items) {
|
||||
mItems = items;
|
||||
notifyDataSetChanged();
|
||||
@ -56,6 +81,8 @@ public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHold
|
||||
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
LayoutInflater inflater = LayoutInflater.from(parent.getContext());
|
||||
View v;
|
||||
|
||||
// Check which view type and set correct values.
|
||||
switch (viewType) {
|
||||
case VIEW_TYPE_CHAPTER:
|
||||
v = inflater.inflate(R.layout.item_recent_chapter, parent, false);
|
||||
@ -69,6 +96,7 @@ public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHold
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
|
||||
// Check which view type and set correct values.
|
||||
switch (holder.getItemViewType()) {
|
||||
case VIEW_TYPE_CHAPTER:
|
||||
final MangaChapter chapter = (MangaChapter) getItem(position);
|
||||
@ -84,6 +112,10 @@ public class RecentChaptersAdapter extends FlexibleAdapter<RecyclerView.ViewHold
|
||||
holder.itemView.setActivated(isSelected(position));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns fragment
|
||||
* @return RecentChaptersFragment
|
||||
*/
|
||||
public RecentChaptersFragment getFragment() {
|
||||
return fragment;
|
||||
}
|
||||
|
@ -2,6 +2,7 @@ package eu.kanade.tachiyomi.ui.recent;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
@ -9,18 +10,32 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
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.database.models.MangaChapter;
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
|
||||
import eu.kanade.tachiyomi.ui.decoration.DividerItemDecoration;
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
/**
|
||||
* Fragment that shows recent chapters.
|
||||
* Uses R.layout.fragment_recent_chapters.
|
||||
* UI related actions should be called from here.
|
||||
*/
|
||||
@RequiresPresenter(RecentChaptersPresenter.class)
|
||||
public class RecentChaptersFragment extends BaseRxFragment<RecentChaptersPresenter> implements FlexibleViewHolder.OnListItemClickListener {
|
||||
|
||||
@ -50,14 +65,21 @@ public class RecentChaptersFragment extends BaseRxFragment<RecentChaptersPresent
|
||||
return view;
|
||||
}
|
||||
|
||||
/**
|
||||
* Populate adapter with chapters
|
||||
*
|
||||
* @param chapters list of chapters
|
||||
*/
|
||||
public void onNextMangaChapters(List<Object> chapters) {
|
||||
adapter.setItems(chapters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onListItemClick(int position) {
|
||||
// Get item from position
|
||||
Object item = adapter.getItem(position);
|
||||
if (item instanceof MangaChapter) {
|
||||
// Open chapter in reader
|
||||
openChapter((MangaChapter) item);
|
||||
}
|
||||
return false;
|
||||
@ -65,12 +87,114 @@ public class RecentChaptersFragment extends BaseRxFragment<RecentChaptersPresent
|
||||
|
||||
@Override
|
||||
public void onListItemLongClick(int position) {
|
||||
|
||||
// Empty function
|
||||
}
|
||||
|
||||
protected void openChapter(MangaChapter chapter) {
|
||||
/**
|
||||
* Open chapter in reader
|
||||
*
|
||||
* @param chapter selected chapter
|
||||
*/
|
||||
private void openChapter(MangaChapter chapter) {
|
||||
getPresenter().onOpenChapter(chapter);
|
||||
Intent intent = ReaderActivity.newIntent(getActivity());
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
/**
|
||||
* Update download status of chapter
|
||||
*
|
||||
* @param download download object containing download progress.
|
||||
*/
|
||||
public void onChapterStatusChange(Download download) {
|
||||
RecentChaptersHolder holder = getHolder(download.chapter);
|
||||
if (holder != null)
|
||||
holder.onStatusChange(download.getStatus());
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private RecentChaptersHolder getHolder(Chapter chapter) {
|
||||
return (RecentChaptersHolder) recyclerView.findViewHolderForItemId(chapter.id);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start downloading chapter
|
||||
*
|
||||
* @param chapters selected chapters
|
||||
* @param manga manga that belongs to chapter
|
||||
* @return true
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
protected boolean onDownload(Observable<Chapter> chapters, Manga manga) {
|
||||
// Start the download service.
|
||||
DownloadService.start(getActivity());
|
||||
|
||||
// Refresh data on download competition.
|
||||
Observable<Chapter> observable = chapters
|
||||
.doOnCompleted(adapter::notifyDataSetChanged);
|
||||
|
||||
// Download chapter.
|
||||
getPresenter().downloadChapter(observable, manga);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Start deleting chapter
|
||||
* @param chapters selected chapters
|
||||
* @param manga manga that belongs to chapter
|
||||
* @return success of deletion.
|
||||
*/
|
||||
protected boolean onDelete(Observable<Chapter> chapters, Manga manga) {
|
||||
int size = adapter.getSelectedItemCount();
|
||||
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.deleting)
|
||||
.progress(false, size, true)
|
||||
.cancelable(false)
|
||||
.show();
|
||||
|
||||
Observable<Chapter> observable = chapters
|
||||
.concatMap(chapter -> {
|
||||
getPresenter().deleteChapter(chapter, manga);
|
||||
return Observable.just(chapter);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(chapter -> {
|
||||
dialog.incrementProgress(1);
|
||||
chapter.status = Download.NOT_DOWNLOADED;
|
||||
})
|
||||
.doOnCompleted(adapter::notifyDataSetChanged)
|
||||
.finallyDo(dialog::dismiss);
|
||||
|
||||
getPresenter().deleteChapters(observable);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as read
|
||||
*
|
||||
* @param chapters selected chapter
|
||||
* @return true
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
protected boolean onMarkAsRead(Observable<Chapter> chapters) {
|
||||
getPresenter().markChaptersRead(chapters, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark chapter as unread
|
||||
*
|
||||
* @param chapters selected chapter
|
||||
* @return true
|
||||
*/
|
||||
@SuppressWarnings("SameReturnValue")
|
||||
protected boolean onMarkAsUnread(Observable<Chapter> chapters) {
|
||||
getPresenter().markChaptersRead(chapters, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -1,35 +1,102 @@
|
||||
package eu.kanade.tachiyomi.ui.recent;
|
||||
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.Menu;
|
||||
import android.view.View;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
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.MangaChapter;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
import rx.Observable;
|
||||
|
||||
/**
|
||||
* Holder that contains chapter item
|
||||
* Uses R.layout.item_recent_chapter.
|
||||
* UI related actions should be called from here.
|
||||
*/
|
||||
public class RecentChaptersHolder extends FlexibleViewHolder {
|
||||
|
||||
/**
|
||||
* Adapter for recent chapters
|
||||
*/
|
||||
private final RecentChaptersAdapter adapter;
|
||||
|
||||
/**
|
||||
* TextView containing chapter title
|
||||
*/
|
||||
@Bind(R.id.chapter_title) TextView chapterTitle;
|
||||
|
||||
/**
|
||||
* TextView containing manga name
|
||||
*/
|
||||
@Bind(R.id.manga_title) TextView mangaTitle;
|
||||
|
||||
/**
|
||||
* TextView containing download status
|
||||
*/
|
||||
@Bind(R.id.download_text) TextView downloadText;
|
||||
|
||||
/**
|
||||
* RelativeLayout containing popup menu with download options
|
||||
*/
|
||||
@Bind(R.id.chapter_menu) RelativeLayout chapterMenu;
|
||||
|
||||
/**
|
||||
* Color of read chapter
|
||||
*/
|
||||
private final int readColor;
|
||||
|
||||
/**
|
||||
* Color of unread chapter
|
||||
*/
|
||||
private final int unreadColor;
|
||||
|
||||
/**
|
||||
* Object containing chapter information
|
||||
*/
|
||||
private MangaChapter mangaChapter;
|
||||
|
||||
/**
|
||||
* Constructor of RecentChaptersHolder
|
||||
* @param view view of ChapterHolder
|
||||
* @param adapter adapter of ChapterHolder
|
||||
* @param onListItemClickListener ClickListener
|
||||
*/
|
||||
public RecentChaptersHolder(View view, RecentChaptersAdapter adapter, OnListItemClickListener onListItemClickListener) {
|
||||
super(view, adapter, onListItemClickListener);
|
||||
this.adapter = adapter;
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
// Set colors.
|
||||
readColor = ContextCompat.getColor(view.getContext(), R.color.hint_text);
|
||||
unreadColor = ContextCompat.getColor(view.getContext(), R.color.primary_text);
|
||||
|
||||
//Set OnClickListener for download menu
|
||||
chapterMenu.setOnClickListener(v -> v.post(() -> showPopupMenu(v)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Set values of view
|
||||
*
|
||||
* @param item item containing chapter information
|
||||
*/
|
||||
public void onSetValues(MangaChapter item) {
|
||||
this.mangaChapter = item;
|
||||
|
||||
// Set chapter title
|
||||
chapterTitle.setText(item.chapter.name);
|
||||
|
||||
// Set manga title
|
||||
mangaTitle.setText(item.manga.title);
|
||||
|
||||
// Check if chapter is read and set correct color
|
||||
if (item.chapter.read) {
|
||||
chapterTitle.setTextColor(readColor);
|
||||
mangaTitle.setTextColor(readColor);
|
||||
@ -37,6 +104,84 @@ public class RecentChaptersHolder extends FlexibleViewHolder {
|
||||
chapterTitle.setTextColor(unreadColor);
|
||||
mangaTitle.setTextColor(unreadColor);
|
||||
}
|
||||
|
||||
// Set chapter status
|
||||
onStatusChange(item.chapter.status);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates chapter status in view.
|
||||
*
|
||||
* @param status download status
|
||||
*/
|
||||
public void onStatusChange(int status) {
|
||||
switch (status) {
|
||||
case Download.QUEUE:
|
||||
downloadText.setText(R.string.chapter_queued);
|
||||
break;
|
||||
case Download.DOWNLOADING:
|
||||
downloadText.setText(R.string.chapter_downloading);
|
||||
break;
|
||||
case Download.DOWNLOADED:
|
||||
downloadText.setText(R.string.chapter_downloaded);
|
||||
break;
|
||||
case Download.ERROR:
|
||||
downloadText.setText(R.string.chapter_error);
|
||||
break;
|
||||
default:
|
||||
downloadText.setText("");
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Show pop up menu
|
||||
* @param view view containing popup menu.
|
||||
*/
|
||||
private void showPopupMenu(View view) {
|
||||
// Create a PopupMenu, giving it the clicked view for an anchor
|
||||
PopupMenu popup = new PopupMenu(adapter.getFragment().getActivity(), view);
|
||||
|
||||
// Inflate our menu resource into the PopupMenu's Menu
|
||||
popup.getMenuInflater().inflate(R.menu.chapter_recent, popup.getMenu());
|
||||
|
||||
// Hide download and show delete if the chapter is downloaded and
|
||||
if (mangaChapter.chapter.isDownloaded()) {
|
||||
Menu menu = popup.getMenu();
|
||||
menu.findItem(R.id.action_download).setVisible(false);
|
||||
menu.findItem(R.id.action_delete).setVisible(true);
|
||||
}
|
||||
|
||||
// Hide mark as unread when the chapter is unread
|
||||
if (!mangaChapter.chapter.read /*&& mangaChapter.chapter.last_page_read == 0*/) {
|
||||
popup.getMenu().findItem(R.id.action_mark_as_unread).setVisible(false);
|
||||
}
|
||||
|
||||
// Hide mark as read when the chapter is read
|
||||
if (mangaChapter.chapter.read) {
|
||||
popup.getMenu().findItem(R.id.action_mark_as_read).setVisible(false);
|
||||
}
|
||||
|
||||
// Set a listener so we are notified if a menu item is clicked
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
Observable<Chapter> chapterObservable = Observable.just(mangaChapter.chapter);
|
||||
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.action_download:
|
||||
return adapter.getFragment().onDownload(chapterObservable, mangaChapter.manga);
|
||||
case R.id.action_delete:
|
||||
return adapter.getFragment().onDelete(chapterObservable, mangaChapter.manga);
|
||||
case R.id.action_mark_as_read:
|
||||
return adapter.getFragment().onMarkAsRead(chapterObservable);
|
||||
case R.id.action_mark_as_unread:
|
||||
return adapter.getFragment().onMarkAsUnread(chapterObservable);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Finally show the PopupMenu
|
||||
popup.show();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -2,6 +2,8 @@ package eu.kanade.tachiyomi.ui.recent;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Calendar;
|
||||
import java.util.Collection;
|
||||
@ -12,48 +14,185 @@ import java.util.TreeMap;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaChapter;
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
|
||||
import eu.kanade.tachiyomi.event.ReaderEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
/**
|
||||
* Presenter of RecentChaptersFragment.
|
||||
* Contains information and data for fragment.
|
||||
* Observable updates should be called from here.
|
||||
*/
|
||||
public class RecentChaptersPresenter extends BasePresenter<RecentChaptersFragment> {
|
||||
|
||||
/**
|
||||
* The id of the restartable.
|
||||
*/
|
||||
private static final int GET_RECENT_CHAPTERS = 1;
|
||||
|
||||
/**
|
||||
* The id of the restartable.
|
||||
*/
|
||||
private static final int CHAPTER_STATUS_CHANGES = 2;
|
||||
|
||||
/**
|
||||
* Used to connect to database
|
||||
*/
|
||||
@Inject DatabaseHelper db;
|
||||
|
||||
/**
|
||||
* Used to get information from download manager
|
||||
*/
|
||||
@Inject DownloadManager downloadManager;
|
||||
|
||||
/**
|
||||
* Used to get source from source id
|
||||
*/
|
||||
@Inject SourceManager sourceManager;
|
||||
|
||||
private static final int GET_RECENT_CHAPTERS = 1;
|
||||
/**
|
||||
* List containing chapter and manga information
|
||||
*/
|
||||
private List<MangaChapter> mangaChapters;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
// Used to get recent chapters
|
||||
restartableLatestCache(GET_RECENT_CHAPTERS,
|
||||
this::getRecentChaptersObservable,
|
||||
RecentChaptersFragment::onNextMangaChapters);
|
||||
(recentChaptersFragment, chapters) -> {
|
||||
// Update adapter to show recent manga's
|
||||
recentChaptersFragment.onNextMangaChapters(chapters);
|
||||
// Update download status
|
||||
updateChapterStatus(convertToMangaChaptersList(chapters));
|
||||
});
|
||||
|
||||
if (savedState == null)
|
||||
// Used to update download status
|
||||
startableLatestCache(CHAPTER_STATUS_CHANGES,
|
||||
this::getChapterStatusObs,
|
||||
RecentChaptersFragment::onChapterStatusChange,
|
||||
(view, error) -> Timber.e(error.getCause(), error.getMessage()));
|
||||
|
||||
if (savedState == null) {
|
||||
// Start fetching recent chapters
|
||||
start(GET_RECENT_CHAPTERS);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list only containing MangaChapter objects.
|
||||
*
|
||||
* @param input the list that will be converted.
|
||||
* @return list containing MangaChapters objects.
|
||||
*/
|
||||
private List<MangaChapter> convertToMangaChaptersList(List<Object> input) {
|
||||
// Create temp list
|
||||
List<MangaChapter> tempMangaChapterList = new ArrayList<>();
|
||||
|
||||
// Only add MangaChapter objects
|
||||
//noinspection Convert2streamapi
|
||||
for (Object object : input) {
|
||||
if (object instanceof MangaChapter) {
|
||||
tempMangaChapterList.add((MangaChapter) object);
|
||||
}
|
||||
}
|
||||
|
||||
// Return temp list
|
||||
return tempMangaChapterList;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status of chapters
|
||||
*
|
||||
* @param mangaChapters list containing recent chapters
|
||||
*/
|
||||
private void updateChapterStatus(List<MangaChapter> mangaChapters) {
|
||||
// Set global list of chapters.
|
||||
this.mangaChapters = mangaChapters;
|
||||
|
||||
// Update status.
|
||||
//noinspection Convert2streamapi
|
||||
for (MangaChapter mangaChapter : mangaChapters)
|
||||
setChapterStatus(mangaChapter);
|
||||
|
||||
// Start onChapterStatusChange restartable.
|
||||
start(CHAPTER_STATUS_CHANGES);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns observable containing chapter status.
|
||||
*
|
||||
* @return download object containing download progress.
|
||||
*/
|
||||
private Observable<Download> getChapterStatusObs() {
|
||||
return downloadManager.getQueue().getStatusObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter(download -> chapterIdEquals(download.chapter.id))
|
||||
.doOnNext(this::updateChapterStatus);
|
||||
}
|
||||
|
||||
/**
|
||||
* Function to check if chapter is in recent list
|
||||
* @param chaptersId id of chapter
|
||||
* @return exist in recent list
|
||||
*/
|
||||
private boolean chapterIdEquals(Long chaptersId) {
|
||||
for (MangaChapter mangaChapter : mangaChapters) {
|
||||
if (chaptersId.equals(mangaChapter.chapter.id)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Update status of chapters.
|
||||
*
|
||||
* @param download download object containing progress.
|
||||
*/
|
||||
private void updateChapterStatus(Download download) {
|
||||
// Loop through list
|
||||
for (MangaChapter item : mangaChapters) {
|
||||
if (download.chapter.id.equals(item.chapter.id)) {
|
||||
item.chapter.status = download.getStatus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get observable containing recent chapters and date
|
||||
* @return observable containing recent chapters and date
|
||||
*/
|
||||
private Observable<List<Object>> getRecentChaptersObservable() {
|
||||
// Set date for recent chapters
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(new Date());
|
||||
cal.add(Calendar.MONTH, -1);
|
||||
|
||||
// Get recent chapters from database.
|
||||
return db.getRecentChapters(cal.getTime()).asRxObservable()
|
||||
// group chapters by the date they were fetched on a ordered map
|
||||
// Group chapters by the date they were fetched on a ordered map.
|
||||
.flatMap(recents -> Observable.from(recents)
|
||||
.toMultimap(
|
||||
recent -> getMapKey(recent.chapter.date_fetch),
|
||||
recent -> recent,
|
||||
() -> new TreeMap<>((d1, d2) -> d2.compareTo(d1))))
|
||||
// add every day and all its chapters to a single list
|
||||
// Add every day and all its chapters to a single list.
|
||||
.map(recents -> {
|
||||
List<Object> items = new ArrayList<>();
|
||||
for (Map.Entry<Date, Collection<MangaChapter>> recent : recents.entrySet()) {
|
||||
@ -65,6 +204,35 @@ public class RecentChaptersPresenter extends BasePresenter<RecentChaptersFragmen
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the chapter status
|
||||
* @param mangaChapter MangaChapter which status gets updated
|
||||
*/
|
||||
private void setChapterStatus(MangaChapter mangaChapter) {
|
||||
// Check if chapter in queue
|
||||
for (Download download : downloadManager.getQueue()) {
|
||||
if (mangaChapter.chapter.id.equals(download.chapter.id)) {
|
||||
mangaChapter.chapter.status = download.getStatus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// Get source of chapter
|
||||
Source source = sourceManager.get(mangaChapter.manga.source);
|
||||
|
||||
// Check if chapter is downloaded
|
||||
if (downloadManager.isChapterDownloaded(source, mangaChapter.manga, mangaChapter.chapter)) {
|
||||
mangaChapter.chapter.status = Download.DOWNLOADED;
|
||||
} else {
|
||||
mangaChapter.chapter.status = Download.NOT_DOWNLOADED;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get date as time key
|
||||
* @param date desired date
|
||||
* @return date as time key
|
||||
*/
|
||||
private Date getMapKey(long date) {
|
||||
Calendar cal = Calendar.getInstance();
|
||||
cal.setTime(new Date(date));
|
||||
@ -75,8 +243,67 @@ public class RecentChaptersPresenter extends BasePresenter<RecentChaptersFragmen
|
||||
return cal.getTime();
|
||||
}
|
||||
|
||||
/**
|
||||
* Open chapter in reader
|
||||
* @param item chapter that is opened
|
||||
*/
|
||||
public void onOpenChapter(MangaChapter item) {
|
||||
Source source = sourceManager.get(item.manga.source);
|
||||
EventBus.getDefault().postSticky(new ReaderEvent(source, item.manga, item.chapter));
|
||||
}
|
||||
|
||||
/**
|
||||
* Download selected chapter
|
||||
* @param selectedChapter chapter that is selected
|
||||
* @param manga manga that belongs to chapter
|
||||
*/
|
||||
public void downloadChapter(Observable<Chapter> selectedChapter, Manga manga) {
|
||||
add(selectedChapter
|
||||
.toList()
|
||||
.subscribe(chapters -> {
|
||||
EventBus.getDefault().postSticky(new DownloadChaptersEvent(manga, chapters));
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected chapter
|
||||
* @param chapter chapter that is selected
|
||||
* @param manga manga that belongs to chapter
|
||||
*/
|
||||
public void deleteChapter(Chapter chapter, Manga manga) {
|
||||
Source source = sourceManager.get(manga.source);
|
||||
downloadManager.deleteChapter(source, manga, chapter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete selected chapter observable
|
||||
* @param selectedChapters chapter that are selected
|
||||
*/
|
||||
public void deleteChapters(Observable<Chapter> selectedChapters) {
|
||||
add(selectedChapters
|
||||
.subscribe(chapter -> {
|
||||
downloadManager.getQueue().remove(chapter);
|
||||
}, error -> {
|
||||
Timber.e(error.getMessage());
|
||||
}));
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark selected chapter as read
|
||||
* @param selectedChapters chapter that is selected
|
||||
* @param read read status
|
||||
*/
|
||||
public void markChaptersRead(Observable<Chapter> selectedChapters, boolean read) {
|
||||
add(selectedChapters
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map(chapter -> {
|
||||
chapter.read = read;
|
||||
if (!read) chapter.last_page_read = 0;
|
||||
return chapter;
|
||||
})
|
||||
.toList()
|
||||
.flatMap(chapters -> db.insertChapters(chapters).asRxObservable())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe());
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,8 @@ import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.ParseException;
|
||||
import java.text.SimpleDateFormat;
|
||||
@ -15,8 +17,23 @@ import java.util.TimeZone;
|
||||
|
||||
import eu.kanade.tachiyomi.BuildConfig;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateChecker;
|
||||
import eu.kanade.tachiyomi.data.updater.UpdateDownloader;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
public class SettingsAboutFragment extends SettingsNestedFragment {
|
||||
/**
|
||||
* Checks for new releases
|
||||
*/
|
||||
private UpdateChecker updateChecker;
|
||||
|
||||
/**
|
||||
* The subscribtion service of the obtained release object
|
||||
*/
|
||||
private Subscription releaseSubscription;
|
||||
|
||||
public static SettingsNestedFragment newInstance(int resourcePreference, int resourceTitle) {
|
||||
SettingsNestedFragment fragment = new SettingsAboutFragment();
|
||||
@ -25,14 +42,36 @@ public class SettingsAboutFragment extends SettingsNestedFragment {
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
//Check for update
|
||||
updateChecker = new UpdateChecker(getActivity());
|
||||
|
||||
super.onCreate(savedInstanceState);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
if (releaseSubscription != null)
|
||||
releaseSubscription.unsubscribe();
|
||||
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
|
||||
Preference version = findPreference(getString(R.string.pref_version));
|
||||
Preference buildTime = findPreference(getString(R.string.pref_build_time));
|
||||
|
||||
version.setSummary(BuildConfig.DEBUG ? "r" + BuildConfig.COMMIT_COUNT :
|
||||
BuildConfig.VERSION_NAME);
|
||||
|
||||
//Set onClickListener to check for new version
|
||||
version.setOnPreferenceClickListener(preference -> {
|
||||
if (!BuildConfig.DEBUG && BuildConfig.INCLUDE_UPDATER)
|
||||
checkVersion();
|
||||
return true;
|
||||
});
|
||||
|
||||
buildTime.setSummary(getFormattedBuildTime());
|
||||
|
||||
return super.onCreateView(inflater, container, savedState);
|
||||
@ -54,4 +93,40 @@ public class SettingsAboutFragment extends SettingsNestedFragment {
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks version and shows a user prompt when update available.
|
||||
*/
|
||||
private void checkVersion() {
|
||||
releaseSubscription = updateChecker.checkForApplicationUpdate()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(release -> {
|
||||
//Get version of latest release
|
||||
String newVersion = release.getVersion();
|
||||
newVersion = newVersion.replaceAll("[^\\d.]", "");
|
||||
|
||||
//Check if latest version is different from current version
|
||||
if (!newVersion.equals(BuildConfig.VERSION_NAME)) {
|
||||
String downloadLink = release.getDownloadLink();
|
||||
String body = release.getChangeLog();
|
||||
|
||||
//Create confirmation window
|
||||
new MaterialDialog.Builder(getActivity())
|
||||
.title(getString(R.string.update_check_title))
|
||||
.content(body)
|
||||
.positiveText(getString(R.string.update_check_confirm))
|
||||
.negativeText(getString(R.string.update_check_ignore))
|
||||
.onPositive((dialog, which) -> {
|
||||
// User output that download has started
|
||||
ToastUtil.showShort(getActivity(), getString(R.string.update_check_download_started));
|
||||
// Start download
|
||||
new UpdateDownloader(getActivity().getApplicationContext()).execute(downloadLink);
|
||||
})
|
||||
.show();
|
||||
} else {
|
||||
ToastUtil.showShort(getActivity(), getString(R.string.update_check_no_new_updates));
|
||||
}
|
||||
}, Throwable::printStackTrace);
|
||||
}
|
||||
}
|
||||
|
@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.setting;
|
||||
import android.app.Activity;
|
||||
import android.content.Intent;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.preference.Preference;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
@ -61,6 +62,13 @@ public class SettingsDownloadsFragment extends SettingsNestedFragment {
|
||||
if (requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) {
|
||||
Uri uri = data.getData();
|
||||
preferences.setDownloadsDirectory(uri.getPath());
|
||||
|
||||
// Persist access permissions.
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
|
||||
getActivity().getContentResolver().takePersistableUriPermission(uri,
|
||||
Intent.FLAG_GRANT_READ_URI_PERMISSION |
|
||||
Intent.FLAG_GRANT_WRITE_URI_PERMISSION);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -10,12 +10,18 @@ import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
|
||||
public class ChapterRecognition {
|
||||
|
||||
private static final Pattern p1 = Pattern.compile("ch[^0-9]?\\s*(\\d+[\\.,]?\\d*)");
|
||||
private static final Pattern p2 = Pattern.compile("(\\d+[\\.,]?\\d*)");
|
||||
private static final Pattern p3 = Pattern.compile("(\\d+[\\.,]?\\d*\\s*:)");
|
||||
private static final Pattern cleanWithToken = Pattern.compile("ch[^0-9]?\\s*(\\d+[\\.,]?\\d+)($|\\b)");
|
||||
private static final Pattern uncleanWithToken = Pattern.compile("ch[^0-9]?\\s*(\\d+[\\.,]?\\d*)");
|
||||
private static final Pattern withAlphaPostfix = Pattern.compile("(\\d+[\\.,]?\\d*\\s*)([a-z])($|\\b)");
|
||||
private static final Pattern cleanNumber = Pattern.compile("(\\d+[\\.,]?\\d+)($|\\b)");
|
||||
private static final Pattern uncleanNumber = Pattern.compile("(\\d+[\\.,]?\\d*)");
|
||||
private static final Pattern withColon = Pattern.compile("(\\d+[\\.,]?\\d*\\s*:)([^\\d]|$)");
|
||||
private static final Pattern startingNumber = Pattern.compile("^(\\d+[\\.,]?\\d*)");
|
||||
|
||||
private static final Pattern pUnwanted =
|
||||
Pattern.compile("\\b(v|ver|vol|version|volume)\\.?\\s*\\d+\\b");
|
||||
Pattern.compile("(\\b|\\d)(v|ver|vol|version|volume)\\.?\\s*\\d+\\b");
|
||||
private static final Pattern pPart =
|
||||
Pattern.compile("(\\b|\\d)part\\s*\\d+.+");
|
||||
|
||||
public static void parseChapterNumber(Chapter chapter, Manga manga) {
|
||||
if (chapter.chapter_number != -1)
|
||||
@ -24,20 +30,34 @@ public class ChapterRecognition {
|
||||
String name = chapter.name.toLowerCase();
|
||||
Matcher matcher;
|
||||
|
||||
// Safest option, the chapter has a token prepended
|
||||
matcher = p1.matcher(name);
|
||||
// Safest option, the chapter has a token prepended and nothing at the end of the number
|
||||
matcher = cleanWithToken.matcher(name);
|
||||
if (matcher.find()) {
|
||||
chapter.chapter_number = Float.parseFloat(matcher.group(1));
|
||||
return;
|
||||
}
|
||||
|
||||
// a number with a single alpha prefix is parsed as sub-chapter
|
||||
matcher = withAlphaPostfix.matcher(name);
|
||||
if (matcher.find()) {
|
||||
chapter.chapter_number = Float.parseFloat(matcher.group(1)) + parseAlphaPostFix(matcher.group(2));
|
||||
return;
|
||||
}
|
||||
|
||||
// the chapter has a token prepended and something at the end of the number
|
||||
matcher = uncleanWithToken.matcher(name);
|
||||
if (matcher.find()) {
|
||||
chapter.chapter_number = Float.parseFloat(matcher.group(1));
|
||||
return;
|
||||
}
|
||||
|
||||
// Remove anything related to the volume or version
|
||||
name = pUnwanted.matcher(name).replaceAll("");
|
||||
name = pUnwanted.matcher(name).replaceAll("$1");
|
||||
|
||||
List<Float> occurrences;
|
||||
|
||||
// If there's only one number, use it
|
||||
matcher = p2.matcher(name);
|
||||
matcher = uncleanNumber.matcher(name);
|
||||
occurrences = getAllOccurrences(matcher);
|
||||
if (occurrences.size() == 1) {
|
||||
chapter.chapter_number = occurrences.get(0);
|
||||
@ -45,7 +65,15 @@ public class ChapterRecognition {
|
||||
}
|
||||
|
||||
// If it has a colon, the chapter number should be that one
|
||||
matcher = p3.matcher(name);
|
||||
matcher = withColon.matcher(name);
|
||||
occurrences = getAllOccurrences(matcher);
|
||||
if (occurrences.size() == 1) {
|
||||
chapter.chapter_number = occurrences.get(0);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer numbers without anything appended
|
||||
matcher = cleanNumber.matcher(name);
|
||||
occurrences = getAllOccurrences(matcher);
|
||||
if (occurrences.size() == 1) {
|
||||
chapter.chapter_number = occurrences.get(0);
|
||||
@ -57,9 +85,9 @@ public class ChapterRecognition {
|
||||
|
||||
// Try to remove the manga name from the chapter, and try again
|
||||
String mangaName = replaceIrrelevantCharacters(manga.title);
|
||||
String nameWithoutManga = difference(mangaName, name);
|
||||
String nameWithoutManga = difference(mangaName, name).trim();
|
||||
if (!nameWithoutManga.isEmpty()) {
|
||||
matcher = p2.matcher(nameWithoutManga);
|
||||
matcher = uncleanNumber.matcher(nameWithoutManga);
|
||||
occurrences = getAllOccurrences(matcher);
|
||||
if (occurrences.size() == 1) {
|
||||
chapter.chapter_number = occurrences.get(0);
|
||||
@ -69,6 +97,52 @@ public class ChapterRecognition {
|
||||
|
||||
// TODO more checks (maybe levenshtein?)
|
||||
|
||||
// try splitting the name in parts an pick the first valid one
|
||||
String[] nameParts = chapter.name.split("-");
|
||||
Chapter dummyChapter = Chapter.create();
|
||||
if (nameParts.length > 1) {
|
||||
for (String part : nameParts) {
|
||||
dummyChapter.name = part;
|
||||
parseChapterNumber(dummyChapter, manga);
|
||||
if (dummyChapter.chapter_number >= 0) {
|
||||
chapter.chapter_number = dummyChapter.chapter_number;
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Strip anything after "part xxx" and try that
|
||||
matcher = pPart.matcher(name);
|
||||
if (matcher.find()) {
|
||||
name = pPart.matcher(name).replaceAll("$1");
|
||||
dummyChapter.name = name;
|
||||
parseChapterNumber(dummyChapter, manga);
|
||||
if (dummyChapter.chapter_number >= 0) {
|
||||
chapter.chapter_number = dummyChapter.chapter_number;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// check for a number either at the start or right after the manga title
|
||||
matcher = startingNumber.matcher(name);
|
||||
if (matcher.find()) {
|
||||
chapter.chapter_number = Float.parseFloat(matcher.group(1));
|
||||
return;
|
||||
}
|
||||
matcher = startingNumber.matcher(nameWithoutManga);
|
||||
if (matcher.find()) {
|
||||
chapter.chapter_number = Float.parseFloat(matcher.group(1));
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* x.a -> x.1, x.b -> x.2, etc
|
||||
*/
|
||||
private static float parseAlphaPostFix(String postfix) {
|
||||
char alpha = postfix.charAt(0);
|
||||
return Float.parseFloat("0." + Integer.toString((int)alpha - 96));
|
||||
}
|
||||
|
||||
public static List<Float> getAllOccurrences(Matcher matcher) {
|
||||
@ -76,7 +150,7 @@ public class ChapterRecognition {
|
||||
while (matcher.find()) {
|
||||
// Match again to get only numbers from the captured text
|
||||
String text = matcher.group();
|
||||
Matcher m = p2.matcher(text);
|
||||
Matcher m = uncleanNumber.matcher(text);
|
||||
if (m.find()) {
|
||||
try {
|
||||
Float value = Float.parseFloat(m.group(1));
|
||||
|
@ -1,7 +0,0 @@
|
||||
package eu.kanade.tachiyomi.util;
|
||||
|
||||
import java.lang.annotation.ElementType;
|
||||
import java.lang.annotation.Target;
|
||||
|
||||
@Target({ElementType.METHOD})
|
||||
public @interface EventBusHook {}
|
@ -5,6 +5,9 @@ import android.content.res.TypedArray;
|
||||
import android.support.v7.widget.GridLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.util.AttributeSet;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
|
||||
public class AutofitRecyclerView extends RecyclerView {
|
||||
|
||||
|
6
app/src/main/res/anim/fab_hide_to_bottom.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:interpolator="@android:interpolator/accelerate_quint"
|
||||
android:fromYDelta="0"
|
||||
android:toYDelta="30%p"
|
||||
android:duration="200" />
|
6
app/src/main/res/anim/fab_show_from_bottom.xml
Normal file
@ -0,0 +1,6 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<translate xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
android:interpolator="@android:interpolator/accelerate_cubic"
|
||||
android:fromYDelta="30%p"
|
||||
android:toYDelta="0"
|
||||
android:duration="300" />
|
Before Width: | Height: | Size: 134 B |
Before Width: | Height: | Size: 387 B |
Before Width: | Height: | Size: 651 B |
Before Width: | Height: | Size: 584 B |
BIN
app/src/main/res/drawable-hdpi/ic_add_white_24dp.png
Normal file
After Width: | Height: | Size: 127 B |
BIN
app/src/main/res/drawable-hdpi/ic_bookmark_border_white_24dp.png
Normal file
After Width: | Height: | Size: 239 B |
BIN
app/src/main/res/drawable-hdpi/ic_bookmark_white_24dp.png
Normal file
After Width: | Height: | Size: 185 B |
BIN
app/src/main/res/drawable-hdpi/ic_crop_original_white_24dp.png
Normal file
After Width: | Height: | Size: 271 B |
BIN
app/src/main/res/drawable-hdpi/ic_favorite_border_white_24dp.png
Normal file
After Width: | Height: | Size: 532 B |
BIN
app/src/main/res/drawable-hdpi/ic_favorite_white_24dp.png
Normal file
After Width: | Height: | Size: 358 B |
BIN
app/src/main/res/drawable-hdpi/ic_mode_edit_white_24dp.png
Normal file
After Width: | Height: | Size: 219 B |
BIN
app/src/main/res/drawable-hdpi/ic_pause.png
Normal file
After Width: | Height: | Size: 105 B |
Before Width: | Height: | Size: 119 B |
BIN
app/src/main/res/drawable-ldpi/ic_crop_original_white_24dp.png
Normal file
After Width: | Height: | Size: 237 B |
BIN
app/src/main/res/drawable-ldpi/ic_pause.png
Normal file
After Width: | Height: | Size: 95 B |
Before Width: | Height: | Size: 116 B |
Before Width: | Height: | Size: 262 B |
Before Width: | Height: | Size: 449 B |