diff --git a/app/src/main/java/eu/kanade/mangafeed/data/source/base/BaseSource.java b/app/src/main/java/eu/kanade/mangafeed/data/source/base/BaseSource.java index fc789868c..a12a7cf90 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/source/base/BaseSource.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/source/base/BaseSource.java @@ -80,8 +80,8 @@ public abstract class BaseSource { return defaultPageUrl; } - // Get the URL of the remaining pages that contains source images - protected String overrideRemainingPagesUrl(String defaultPageUrl) { + // Get the URL of the pages that contains source images + protected String overridePageUrl(String defaultPageUrl) { return defaultPageUrl; } diff --git a/app/src/main/java/eu/kanade/mangafeed/data/source/base/Source.java b/app/src/main/java/eu/kanade/mangafeed/data/source/base/Source.java index 0faff6db6..fd139584f 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/source/base/Source.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/source/base/Source.java @@ -103,7 +103,7 @@ public abstract class Source extends BaseSource { public Observable getImageUrlFromPage(final Page page) { page.setStatus(Page.LOAD_PAGE); return mNetworkService - .getStringResponse(overrideRemainingPagesUrl(page.getUrl()), mRequestHeaders, null) + .getStringResponse(overridePageUrl(page.getUrl()), mRequestHeaders, null) .flatMap(unparsedHtml -> Observable.just(parseHtmlToImageUrl(unparsedHtml))) .onErrorResumeNext(e -> { page.setStatus(Page.ERROR); diff --git a/app/src/main/java/eu/kanade/mangafeed/data/source/online/english/Batoto.java b/app/src/main/java/eu/kanade/mangafeed/data/source/online/english/Batoto.java index b8c192d47..45e849c18 100644 --- a/app/src/main/java/eu/kanade/mangafeed/data/source/online/english/Batoto.java +++ b/app/src/main/java/eu/kanade/mangafeed/data/source/online/english/Batoto.java @@ -135,7 +135,7 @@ public class Batoto extends Source { } @Override - protected String overrideRemainingPagesUrl(String defaultPageUrl) { + protected String overridePageUrl(String defaultPageUrl) { int start = defaultPageUrl.indexOf("#") + 1; int end = defaultPageUrl.indexOf("_", start); String id = defaultPageUrl.substring(start, end); diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/base/presenter/BasePresenter.java b/app/src/main/java/eu/kanade/mangafeed/ui/base/presenter/BasePresenter.java index c03a7703c..cace48cdc 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/base/presenter/BasePresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/base/presenter/BasePresenter.java @@ -6,7 +6,6 @@ import android.support.annotation.NonNull; import de.greenrobot.event.EventBus; import icepick.Icepick; -import nucleus.presenter.RxPresenter; import nucleus.view.ViewWithPresenter; public class BasePresenter extends RxPresenter { diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/base/presenter/RxPresenter.java b/app/src/main/java/eu/kanade/mangafeed/ui/base/presenter/RxPresenter.java new file mode 100644 index 000000000..df7fbd96d --- /dev/null +++ b/app/src/main/java/eu/kanade/mangafeed/ui/base/presenter/RxPresenter.java @@ -0,0 +1,332 @@ +package eu.kanade.mangafeed.ui.base.presenter; + +import android.os.Bundle; +import android.support.annotation.CallSuper; +import android.support.annotation.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Map; + +import nucleus.presenter.Presenter; +import nucleus.presenter.delivery.DeliverFirst; +import nucleus.presenter.delivery.DeliverLatestCache; +import nucleus.presenter.delivery.DeliverReplay; +import nucleus.presenter.delivery.Delivery; +import rx.Observable; +import rx.Subscription; +import rx.functions.Action1; +import rx.functions.Action2; +import rx.functions.Func0; +import rx.internal.util.SubscriptionList; +import rx.subjects.BehaviorSubject; + +/** + * This is an extension of {@link Presenter} which provides RxJava functionality. + * + * @param a type of view. + */ +public class RxPresenter extends Presenter { + + private static final String REQUESTED_KEY = RxPresenter.class.getName() + "#requested"; + + private final BehaviorSubject views = BehaviorSubject.create(); + private final SubscriptionList subscriptions = new SubscriptionList(); + + private final HashMap> restartables = new HashMap<>(); + private final HashMap restartableSubscriptions = new HashMap<>(); + private final ArrayList requested = new ArrayList<>(); + + /** + * Returns an {@link rx.Observable} that emits the current attached view or null. + * See {@link BehaviorSubject} for more information. + * + * @return an observable that emits the current attached view or null. + */ + public Observable view() { + return views; + } + + /** + * Registers a subscription to automatically unsubscribe it during onDestroy. + * See {@link SubscriptionList#add(Subscription) for details.} + * + * @param subscription a subscription to add. + */ + public void add(Subscription subscription) { + subscriptions.add(subscription); + } + + /** + * Removes and unsubscribes a subscription that has been registered with {@link #add} previously. + * See {@link SubscriptionList#remove(Subscription)} for details. + * + * @param subscription a subscription to remove. + */ + public void remove(Subscription subscription) { + subscriptions.remove(subscription); + } + + /** + * A restartable is any RxJava observable that can be started (subscribed) and + * should be automatically restarted (re-subscribed) after a process restart if + * it was still subscribed at the moment of saving presenter's state. + * + * Registers a factory. Re-subscribes the restartable after the process restart. + * + * @param restartableId id of the restartable + * @param factory factory of the restartable + */ + public void restartable(int restartableId, Func0 factory) { + restartables.put(restartableId, factory); + if (requested.contains(restartableId)) + start(restartableId); + } + + /** + * Starts the given restartable. + * + * @param restartableId id of the restartable + */ + public void start(int restartableId) { + stop(restartableId); + requested.add(restartableId); + restartableSubscriptions.put(restartableId, restartables.get(restartableId).call()); + } + + /** + * Unsubscribes a restartable + * + * @param restartableId id of a restartable. + */ + public void stop(int restartableId) { + requested.remove((Integer)restartableId); + Subscription subscription = restartableSubscriptions.get(restartableId); + if (subscription != null) + subscription.unsubscribe(); + } + + /** + * Checks if a restartable is started. + * + * @param restartableId id of a restartable. + * @return True if the restartable is started, false otherwise. + */ + public boolean isStarted(int restartableId) { + return requested.contains(restartableId); + } + + /** + * This is a shortcut that can be used instead of combining together + * {@link #restartable(int, Func0)}, + * {@link #deliverFirst()}, + * {@link #split(Action2, Action2)}. + * + * @param restartableId an id of the restartable. + * @param observableFactory a factory that should return an Observable when the restartable 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 the type of the observable. + */ + public void restartableFirst(int restartableId, final Func0> observableFactory, + final Action2 onNext, @Nullable final Action2 onError) { + + restartable(restartableId, new Func0() { + @Override + public Subscription call() { + return observableFactory.call() + .compose(RxPresenter.this.deliverFirst()) + .subscribe(split(onNext, onError)); + } + }); + } + + /** + * This is a shortcut for calling {@link #restartableFirst(int, Func0, Action2, Action2)} with the last parameter = null. + */ + public void restartableFirst(int restartableId, final Func0> observableFactory, final Action2 onNext) { + restartableFirst(restartableId, observableFactory, onNext, null); + } + + /** + * This is a shortcut that can be used instead of combining together + * {@link #restartable(int, Func0)}, + * {@link #deliverLatestCache()}, + * {@link #split(Action2, Action2)}. + * + * @param restartableId an id of the restartable. + * @param observableFactory a factory that should return an Observable when the restartable 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 the type of the observable. + */ + public void restartableLatestCache(int restartableId, final Func0> observableFactory, + final Action2 onNext, @Nullable final Action2 onError) { + + restartable(restartableId, new Func0() { + @Override + public Subscription call() { + return observableFactory.call() + .compose(RxPresenter.this.deliverLatestCache()) + .subscribe(split(onNext, onError)); + } + }); + } + + /** + * This is a shortcut for calling {@link #restartableLatestCache(int, Func0, Action2, Action2)} with the last parameter = null. + */ + public void restartableLatestCache(int restartableId, final Func0> observableFactory, final Action2 onNext) { + restartableLatestCache(restartableId, observableFactory, onNext, null); + } + + /** + * This is a shortcut that can be used instead of combining together + * {@link #restartable(int, Func0)}, + * {@link #deliverReplay()}, + * {@link #split(Action2, Action2)}. + * + * @param restartableId an id of the restartable. + * @param observableFactory a factory that should return an Observable when the restartable 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 the type of the observable. + */ + public void restartableReplay(int restartableId, final Func0> observableFactory, + final Action2 onNext, @Nullable final Action2 onError) { + + restartable(restartableId, new Func0() { + @Override + public Subscription call() { + return observableFactory.call() + .compose(RxPresenter.this.deliverReplay()) + .subscribe(split(onNext, onError)); + } + }); + } + + /** + * This is a shortcut for calling {@link #restartableReplay(int, Func0, Action2, Action2)} with the last parameter = null. + */ + public void restartableReplay(int restartableId, final Func0> observableFactory, final Action2 onNext) { + restartableReplay(restartableId, observableFactory, onNext, null); + } + + /** + * Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by + * the source {@link rx.Observable}. + * + * {@link #deliverLatestCache} keeps the latest onNext value and emits it each time a new view gets attached. + * If a new onNext value appears while a view is attached, it will be delivered immediately. + * + * @param the type of source observable emissions + */ + public DeliverLatestCache deliverLatestCache() { + return new DeliverLatestCache<>(views); + } + + /** + * Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by + * the source {@link rx.Observable}. + * + * {@link #deliverFirst} delivers only the first onNext value that has been emitted by the source observable. + * + * @param the type of source observable emissions + */ + public DeliverFirst deliverFirst() { + return new DeliverFirst<>(views); + } + + /** + * Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by + * the source {@link rx.Observable}. + * + * {@link #deliverReplay} keeps all onNext values and emits them each time a new view gets attached. + * If a new onNext value appears while a view is attached, it will be delivered immediately. + * + * @param the type of source observable emissions + */ + public DeliverReplay deliverReplay() { + return new DeliverReplay<>(views); + } + + /** + * Returns a method that can be used for manual restartable chain build. It returns an Action1 that splits + * a received {@link Delivery} into two {@link Action2} onNext and onError calls. + * + * @param onNext a method that will be called if the delivery contains an emitted onNext value. + * @param onError a method that will be called if the delivery contains an onError throwable. + * @param a type on onNext value. + * @return an Action1 that splits a received {@link Delivery} into two {@link Action2} onNext and onError calls. + */ + public Action1> split(final Action2 onNext, @Nullable final Action2 onError) { + return new Action1>() { + @Override + public void call(Delivery delivery) { + delivery.split(onNext, onError); + } + }; + } + + /** + * This is a shortcut for calling {@link #split(Action2, Action2)} when the second parameter is null. + */ + public Action1> split(Action2 onNext) { + return split(onNext, null); + } + + /** + * {@inheritDoc} + */ + @CallSuper + @Override + protected void onCreate(Bundle savedState) { + if (savedState != null) + requested.addAll(savedState.getIntegerArrayList(REQUESTED_KEY)); + } + + /** + * {@inheritDoc} + */ + @CallSuper + @Override + protected void onDestroy() { + views.onCompleted(); + subscriptions.unsubscribe(); + for (Map.Entry entry : restartableSubscriptions.entrySet()) + entry.getValue().unsubscribe(); + } + + /** + * {@inheritDoc} + */ + @CallSuper + @Override + protected void onSave(Bundle state) { + for (int i = requested.size() - 1; i >= 0; i--) { + int restartableId = requested.get(i); + Subscription subscription = restartableSubscriptions.get(restartableId); + if (subscription != null && subscription.isUnsubscribed()) + requested.remove(i); + } + state.putIntegerArrayList(REQUESTED_KEY, requested); + } + + /** + * {@inheritDoc} + */ + @CallSuper + @Override + protected void onTakeView(View view) { + views.onNext(view); + } + + /** + * {@inheritDoc} + */ + @CallSuper + @Override + protected void onDropView() { + views.onNext(null); + } +} diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/manga/myanimelist/MyAnimeListPresenter.java b/app/src/main/java/eu/kanade/mangafeed/ui/manga/myanimelist/MyAnimeListPresenter.java index d19d4c487..45031a77a 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/manga/myanimelist/MyAnimeListPresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/manga/myanimelist/MyAnimeListPresenter.java @@ -84,6 +84,7 @@ public class MyAnimeListPresenter extends BasePresenter { chapterSync.last_chapter_read = chapterNumber; add(updateSubscription = myAnimeList.update(chapterSync) + .flatMap(response -> db.insertChapterSync(chapterSync).createObservable()) .subscribeOn(Schedulers.io()) .observeOn(AndroidSchedulers.mainThread()) .subscribe(response -> {}, diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/reader/ReaderPresenter.java b/app/src/main/java/eu/kanade/mangafeed/ui/reader/ReaderPresenter.java index f99cc28f6..04de6ee1c 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/reader/ReaderPresenter.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/reader/ReaderPresenter.java @@ -45,6 +45,7 @@ public class ReaderPresenter extends BasePresenter { private Chapter nextChapter; private Chapter previousChapter; private List pageList; + private List nextChapterPageList; private boolean isDownloaded; @State int currentPage; @@ -56,6 +57,7 @@ public class ReaderPresenter extends BasePresenter { private static final int GET_PAGE_LIST = 1; private static final int GET_PAGE_IMAGES = 2; private static final int RETRY_IMAGES = 3; + private static final int PRELOAD_NEXT_CHAPTER = 4; @Override protected void onCreate(Bundle savedState) { @@ -81,7 +83,8 @@ public class ReaderPresenter extends BasePresenter { }); restartableReplay(GET_PAGE_IMAGES, - this::getPageImagesObservable, + () -> getPageImagesObservable() + .doOnCompleted(this::preloadNextChapter), (view, page) -> {}, (view, error) -> Timber.e("An error occurred while downloading an image")); @@ -89,6 +92,11 @@ public class ReaderPresenter extends BasePresenter { this::getRetryPageObservable, (view, page) -> {}, (view, error) -> Timber.e("An error occurred while downloading an image")); + + restartableLatestCache(PRELOAD_NEXT_CHAPTER, + this::getPreloadNextChapterObservable, + (view, pages) -> {}, + (view, error) -> Timber.e("An error occurred while preloading a chapter")); } @Override @@ -105,7 +113,7 @@ public class ReaderPresenter extends BasePresenter { @Override protected void onDestroy() { - onChapterChange(); + onChapterLeft(); super.onDestroy(); } @@ -125,10 +133,73 @@ public class ReaderPresenter extends BasePresenter { retryPageSubject.onNext(page); } + // Returns the page list of a chapter + private Observable> getPageListObservable() { + return 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 getPageImagesObservable() { + Observable pageObservable; + + if (!isDownloaded) { + pageObservable = Observable.from(pageList) + .filter(page -> page.getImageUrl() != null) + .mergeWith(source.getRemainingImageUrlsFromPageList(pageList)) + .flatMap(source::getCachedImage, 3); + } else { + File chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter); + pageObservable = Observable.from(pageList) + .flatMap(page -> downloadManager.getDownloadedImage(page, source, chapterDir)); + } + return pageObservable + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + // Listen for retry page events + private Observable 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 + private Observable getPreloadNextChapterObservable() { + return source.getCachedPageListOrPullFromNetwork(nextChapter.url) + .flatMap(pages -> { + nextChapterPageList = pages; + // Preload at most 5 pages + int pagesToPreload = Math.min(pages.size(), 5); + return Observable.from(pages) + .take(pagesToPreload) + .concatMap(source::getImageUrlFromPage) + .doOnCompleted(this::stopPreloadingNextChapter); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()); + } + + // Loads the given chapter private void loadChapter(Chapter chapter) { + // Before loading the chapter, stop preloading (if it's working) and save current progress + stopPreloadingNextChapter(); + this.chapter = chapter; isDownloaded = isChapterDownloaded(chapter); - if (chapter.last_page_read != 0 && !chapter.read) + + // 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; else currentPage = 0; @@ -136,11 +207,22 @@ public class ReaderPresenter extends BasePresenter { // Reset next and previous chapter. They have to be fetched again nextChapter = null; previousChapter = null; + nextChapterPageList = null; start(GET_PAGE_LIST); } - private void onChapterChange() { + // Check whether the given chapter is downloaded + public boolean isChapterDownloaded(Chapter chapter) { + File dir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter); + List pageList = downloadManager.getSavedPageList(source, manga, chapter); + + return pageList != null && pageList.size() + 1 == dir.listFiles().length; + } + + // Called before loading another chapter or leaving the reader. It allows to do operations + // over the chapter read like saving progress + private void onChapterLeft() { if (pageList == null) return; @@ -158,6 +240,7 @@ public class ReaderPresenter extends BasePresenter { db.insertChapter(chapter).executeAsBlocking(); } + // Check whether the chapter has been read private boolean isChapterFinished() { return !chapter.read && currentPage == pageList.size() - 1; } @@ -185,46 +268,6 @@ public class ReaderPresenter extends BasePresenter { } } - private Observable> getPageListObservable() { - if (!isDownloaded) - return source.getCachedPageListOrPullFromNetwork(chapter.url) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - else - return Observable.just(downloadManager.getSavedPageList(source, manga, chapter)); - } - - private Observable getPageImagesObservable() { - Observable pages; - - if (!isDownloaded) { - pages = Observable.from(pageList) - .filter(page -> page.getImageUrl() != null) - .mergeWith(source.getRemainingImageUrlsFromPageList(pageList)) - .flatMap(source::getCachedImage, 3); - } else { - File chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter); - - pages = Observable.from(pageList) - .flatMap(page -> downloadManager.getDownloadedImage(page, source, chapterDir)); - } - return pages - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } - - private Observable getRetryPageObservable() { - return retryPageSubject - .flatMap(page -> { - if (page.getImageUrl() == null) - return source.getImageUrlFromPage(page); - return Observable.just(page); - }) - .flatMap(source::getCachedImage) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } - public void setCurrentPage(int currentPage) { this.currentPage = currentPage; } @@ -247,33 +290,49 @@ public class ReaderPresenter extends BasePresenter { .subscribe(result -> previousChapter = result)); } - public boolean isChapterDownloaded(Chapter chapter) { - File dir = downloadManager.getAbsoluteChapterDirectory(source, manga, chapter); - List pageList = downloadManager.getSavedPageList(source, manga, chapter); - - return pageList != null && pageList.size() + 1 == dir.listFiles().length; - } - public void loadNextChapter() { - if (nextChapter != null) { - onChapterChange(); + if (hasNextChapter()) { + onChapterLeft(); loadChapter(nextChapter); } } public void loadPreviousChapter() { - if (previousChapter != null) { - onChapterChange(); + if (hasPreviousChapter()) { + onChapterLeft(); loadChapter(previousChapter); } } - public Manga getManga() { - return manga; + public boolean hasNextChapter() { + return nextChapter != null; + } + + public boolean hasPreviousChapter() { + return previousChapter != null; + } + + private void preloadNextChapter() { + if (hasNextChapter() && !isChapterDownloaded(nextChapter)) { + start(PRELOAD_NEXT_CHAPTER); + } + } + + private void stopPreloadingNextChapter() { + if (isStarted(PRELOAD_NEXT_CHAPTER)) { + stop(PRELOAD_NEXT_CHAPTER); + if (nextChapterPageList != null) + source.savePageList(nextChapter.url, nextChapterPageList); + } } public void updateMangaViewer(int viewer) { manga.viewer = viewer; db.insertManga(manga).executeAsBlocking(); } + + public Manga getManga() { + return manga; + } + } diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/base/BaseReader.java b/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/base/BaseReader.java index 293c67236..a2528855e 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/base/BaseReader.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/base/BaseReader.java @@ -5,18 +5,23 @@ import android.view.ViewGroup; import java.util.List; +import eu.kanade.mangafeed.R; import eu.kanade.mangafeed.data.source.model.Page; import eu.kanade.mangafeed.ui.reader.ReaderActivity; +import eu.kanade.mangafeed.ui.reader.ReaderPresenter; +import eu.kanade.mangafeed.util.ToastUtil; public abstract class BaseReader { protected ReaderActivity activity; + protected ReaderPresenter presenter; protected ViewGroup container; protected int currentPosition; public BaseReader(ReaderActivity activity) { this.activity = activity; this.container = activity.getContainer(); + this.presenter = activity.getPresenter(); } public void updatePageNumber() { @@ -34,13 +39,22 @@ public abstract class BaseReader { } public void requestNextChapter() { - activity.getPresenter().setCurrentPage(getCurrentPosition()); - activity.getPresenter().loadNextChapter(); + if (presenter.hasNextChapter()) { + presenter.setCurrentPage(getCurrentPosition()); + presenter.loadNextChapter(); + } else { + ToastUtil.showShort(activity, R.string.no_next_chapter); + } + } public void requestPreviousChapter() { - activity.getPresenter().setCurrentPage(getCurrentPosition()); - activity.getPresenter().loadPreviousChapter(); + if (presenter.hasPreviousChapter()) { + presenter.setCurrentPage(getCurrentPosition()); + presenter.loadPreviousChapter(); + } else { + ToastUtil.showShort(activity, R.string.no_previous_chapter); + } } public void destroy() {} diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/horizontal/HorizontalReader.java b/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/horizontal/HorizontalReader.java index 75cd304b6..d46c12c48 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/horizontal/HorizontalReader.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/horizontal/HorizontalReader.java @@ -30,7 +30,7 @@ public abstract class HorizontalReader extends BaseReader { transitionsSubscription = activity.getPreferences().enableTransitions().asObservable() .subscribe(value -> transitions = value); - viewPager.setOffscreenPageLimit(3); + viewPager.setOffscreenPageLimit(2); viewPager.addOnPageChangeListener(new HorizontalViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { diff --git a/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/vertical/VerticalReader.java b/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/vertical/VerticalReader.java index 680195bc7..2199b855f 100644 --- a/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/vertical/VerticalReader.java +++ b/app/src/main/java/eu/kanade/mangafeed/ui/reader/viewer/vertical/VerticalReader.java @@ -31,7 +31,7 @@ public class VerticalReader extends BaseReader { transitionsSubscription = activity.getPreferences().enableTransitions().asObservable() .subscribe(value -> transitions = value); - viewPager.setOffscreenPageLimit(3); + viewPager.setOffscreenPageLimit(2); viewPager.setOnPageChangeListener(new ViewPager.SimpleOnPageChangeListener() { @Override public void onPageSelected(int position) { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 8a0081170..7ca3fe7a0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -92,6 +92,8 @@ Page: %1$d Error fetching page list. Is network available? Chapter %1$s + Next chapter not found + Previous chapter not found Update progress: %1$d/%2$d