Compare commits

..

76 Commits

Author SHA1 Message Date
110df59197 Release 0.1.4 2016-02-21 15:59:07 +01:00
75ae4081d8 Merge pull request #167 from j2ghz/patch-1
Fix link broken by PR #164
2016-02-20 23:36:24 +01:00
2efca050b3 Fix link broken by PR #164 2016-02-20 23:33:55 +01:00
37e119c4f2 Merge pull request #164 from j2ghz/patch-1
Create ISSUE_TEMPLATE.md
2016-02-20 22:07:47 +01:00
9786074119 Move github files to .github/ 2016-02-20 12:06:53 +01:00
d4876b426f Create ISSUE_TEMPLATE.md 2016-02-20 12:02:57 +01:00
8581e4667a Merge pull request #160 from NoodleMage/issue_118
Implements Issue #118 download from recent tab
2016-02-19 20:36:31 +01:00
b94f86765d Code cleanup, implements #118 2016-02-18 19:01:40 +01:00
ba0f3778ce Can now mark as read / unread 2016-02-18 17:40:12 +01:00
aac6b242a0 Can now delete manga from recent + added missing res files #118 2016-02-18 17:29:31 +01:00
dec9442a65 Can now download from recent tab. #118 2016-02-18 17:29:29 +01:00
b33da641d9 Fix crash in chapters list #159 2016-02-18 14:25:35 +01:00
96d498e7e5 Merge pull request #152 from icewind1991/chapter-parsing
Chapter recognition improvements
2016-02-16 21:08:10 +01:00
eee137a084 prefer numbers at the start of the chapter title if otherwise unparsed 2016-02-16 21:03:52 +01:00
5e834ae3be improve colon handling 2016-02-16 21:03:21 +01:00
dcfda61aba Always create nomedia file 2016-02-16 20:47:23 +01:00
5ac7f7057a Merge pull request #150 from NoodleMage/comments
Improved comments
2016-02-16 20:41:45 +01:00
ff46c61f63 Merge pull request #151 from icewind1991/chapter-recognition-fallback
Fix infinite loop when no chapter number is parsed
2016-02-16 20:41:34 +01:00
57b64a412e Fix infinite loop when no chapter number is parsed 2016-02-16 20:37:57 +01:00
1e81f75377 Possible fix for #120 2016-02-16 18:19:54 +01:00
1dd49a2ab1 Improved comments 2016-02-16 15:30:15 +01:00
1cd77a97a7 Merge pull request #143 from NoodleMage/fab_improvement
Moved edit to cover select and update manga info view
2016-02-15 23:28:04 +01:00
f820522a69 Show keep screen on in reader settings. Closes #146 2016-02-15 21:25:01 +01:00
3da613dedb Moved edit cover to library | Updated manga info view | Updated catalogue
grid
2016-02-15 16:59:24 +01:00
5c329d2314 Incorrect mark as read with seamless mode. #142 2016-02-14 15:35:58 +01:00
4c073e713d Merge pull request #139 from j2ghz/patch-1
Make CONTRIBUTING.md more visible
2016-02-13 18:08:57 +01:00
2832f4ae5e Update README.md 2016-02-13 17:57:17 +01:00
7690e8a53f Merge pull request #137 from NoodleMage/fab_improvement
FAB animation update
2016-02-13 14:53:51 +01:00
3a19f8e40b FAB animation update 2016-02-13 12:08:15 +01:00
a33b525f9e Merge pull request #136 from icewind1991/search-sort
sort by views for mangafox and mangahere search results
2016-02-12 22:01:43 +01:00
7d6ce46829 sort by views for mangafox and mangahere search results 2016-02-12 21:49:38 +01:00
a90a4bf80c Remove old orientation lock. Add orientation types to preferences 2016-02-12 21:22:54 +01:00
140bf8caee Allow to force a rotation 2016-02-12 19:36:00 +01:00
56a45f263e Strip html tags from batoto notice and directly throw an exception 2016-02-12 15:38:16 +01:00
01d6ddfafb Merge pull request #132 from icewind1991/batato-staff-notice
Show batoto staff notice if updating chapters failed
2016-02-11 23:32:32 +01:00
393b4916f6 Show batoto staff notice if updating chapters failed 2016-02-11 22:59:24 +01:00
cb3c3af865 Include reactive network as library 2016-02-11 14:16:36 +01:00
5a83976fa5 Remove unneeded dependency. 2016-02-10 21:15:02 +01:00
a81f6c3ac4 Trying to give write permissions on SD card 2016-02-10 15:41:59 +01:00
6846ce5bfb Increase maximum allowed scale on pagers 2016-02-10 15:19:31 +01:00
0c0ebe06e5 Volume keys scroll pages. Closes #95 2016-02-10 15:06:18 +01:00
e50c683159 Fix tests failing after upgrading EventBus 2016-02-09 22:07:14 +01:00
872af276ea Merge pull request #130 from icewind1991/chapter-number-parsing
Improve chapter number parsing
2016-02-09 21:50:32 +01:00
e6faee9779 handle chapters with part numbers 2016-02-09 21:23:57 +01:00
bc1ddd4379 fallback to parsing parts to handle arc numbers 2016-02-09 21:20:17 +01:00
e348d6c1cf Upgrade to EventBus 3 2016-02-09 21:19:11 +01:00
7835921045 Merge pull request #126 from beschoenen/downloading
Download features
2016-02-09 21:15:36 +01:00
1611a274b9 differentiate subchapters denoted by an alpha prefix 2016-02-09 20:57:26 +01:00
fa4a8204a4 prefer numbers without anything appended when parsing chapter numbers 2016-02-09 20:43:10 +01:00
5977e9f47f handle chapter versions which are attached to the chapter number 2016-02-09 20:26:51 +01:00
63d0161da5 move clear queue to presenter 2016-02-09 17:42:39 +01:00
d8b46c1969 set display mode title 2016-02-09 16:57:11 +01:00
f84731c2df cleanup chapter action menu 2016-02-09 16:50:26 +01:00
50d71d1395 clear the download queue 2016-02-09 16:34:41 +01:00
4be0b2502e Change stop to pause in download queue view 2016-02-09 16:25:36 +01:00
6c069ad87b multiple chapter download from manga view 2016-02-09 16:01:11 +01:00
e69011ac5b Use a shorter description for seamless mode 2016-02-08 22:33:23 +01:00
ea130a0899 Merge pull request #112 from icewind1991/seamless-chapters
Seamless chapter transition
2016-02-08 22:25:56 +01:00
2566862e0f seamless chapter transitions 2016-02-08 22:24:47 +01:00
16081817c2 Upgrade dependencies 2016-02-08 22:14:48 +01:00
945625d3ad Cancel notification when no new chapters are found. Closes #121 2016-02-07 19:15:45 +01:00
050b9c9fce Remember last used source. Closes #30 2016-02-06 19:03:15 +01:00
c35184abdc Upgrade gradle. Other minor changes 2016-02-06 00:37:11 +01:00
34c5f0b7ba Try to mark readded chapters as read. #119 2016-02-05 22:08:54 +01:00
6435eeb251 Use network cache 2016-02-05 20:18:39 +01:00
eec2dcd981 Fix a crash 2016-02-05 17:30:58 +01:00
57ba368ae0 Add library search. Closes #64 2016-02-05 16:24:34 +01:00
ed06469885 Trying to fix a backpressure isue 2016-02-05 15:42:53 +01:00
79cd8c691e Minor changes 2016-02-05 14:53:07 +01:00
391550f49a Implement zoom start position. Closes #92. Rapid decoder properly throws an error when it fails to decode. 2016-02-04 17:16:47 +01:00
6aa07dd17e Download the first image of the next chapter 2016-02-03 22:21:15 +01:00
aada373a0c Replace onProcessRestart with the new startables. 2016-02-03 21:09:40 +01:00
3deac86bbe Merge pull request #98 from NoodleMage/download_updates
Download updates
2016-02-03 17:17:15 +01:00
d7aef2e97a Application can now check if update available 2016-02-03 17:12:26 +01:00
7953ba6e78 Display date in local format. Fix #108 2016-02-03 13:28:55 +01:00
8aa3c2a260 Update readme 2016-02-03 12:58:07 +01:00
176 changed files with 3577 additions and 1411 deletions

6
.github/ISSUE_TEMPLATE.md vendored Normal file
View 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

View File

@ -1,13 +1,15 @@
[![stable release](https://img.shields.io/badge/release-v0.1.2-blue.svg)](https://github.com/inorichi/tachiyomi/releases)
[![stable release](https://img.shields.io/badge/release-v0.1.4-blue.svg)](https://github.com/inorichi/tachiyomi/releases)
[![fdroid release](https://img.shields.io/badge/release-F--Droid-blue.svg)](https://f-droid.org/repository/browse/?fdid=eu.kanade.tachiyomi)
[![latest debug build](https://img.shields.io/badge/debug-latest%20build-blue.svg)](http://tachiyomi.kanade.eu/latest/app-debug.apk)
## [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

View File

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

View File

@ -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.** { *; }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

@ -50,4 +50,6 @@ public class LibraryHolder extends FlexibleViewHolder {
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,7 +0,0 @@
package eu.kanade.tachiyomi.ui.reader.viewer.base;
public interface OnChapterSingleTapListener {
void onCenterTap();
void onLeftSideTap();
void onRightSideTap();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

@ -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() {

View File

@ -16,14 +16,4 @@ public class VerticalReader extends PagerReader {
return pager;
}
@Override
public void onFirstPageOut() {
getReaderActivity().requestPreviousChapter();
}
@Override
public void onLastPageOut() {
getReaderActivity().requestNextChapter();
}
}

View File

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

View File

@ -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() {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

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

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

Binary file not shown.

Before

Width:  |  Height:  |  Size: 134 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 387 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 651 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 584 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 239 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 271 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 532 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 358 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 119 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 237 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 116 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 262 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 449 B

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