Reader presenter in Kotlin + remove Icepick
This commit is contained in:
parent
8e0a9d6d66
commit
0d519b3d16
9 changed files with 424 additions and 466 deletions
|
@ -105,7 +105,6 @@ dependencies {
|
|||
final OKHTTP_VERSION = '3.2.0'
|
||||
final RETROFIT_VERSION = '2.0.0'
|
||||
final STORIO_VERSION = '1.8.0'
|
||||
final ICEPICK_VERSION = '3.2.0'
|
||||
final MOCKITO_VERSION = '1.10.19'
|
||||
|
||||
compile fileTree(dir: 'libs', include: ['*.jar'])
|
||||
|
@ -138,8 +137,6 @@ dependencies {
|
|||
compile 'com.github.bumptech.glide:glide:3.7.0'
|
||||
compile 'com.jakewharton.timber:timber:4.1.1'
|
||||
compile 'ch.acra:acra:4.8.3'
|
||||
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.2'
|
||||
|
|
|
@ -58,7 +58,7 @@ public class App extends Application {
|
|||
|
||||
protected void setupEventBus() {
|
||||
EventBus.builder()
|
||||
.addIndex(new EventBusIndex())
|
||||
// .addIndex(new EventBusIndex())
|
||||
.logNoSubscriberMessages(false)
|
||||
.installDefaultEventBus();
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
package eu.kanade.tachiyomi.ui.base.activity
|
||||
|
||||
import android.graphics.Color
|
||||
import android.os.Bundle
|
||||
import android.support.design.widget.Snackbar
|
||||
import android.support.v7.app.AppCompatActivity
|
||||
import android.support.v7.widget.Toolbar
|
||||
|
@ -10,21 +9,10 @@ import android.view.View
|
|||
import android.widget.TextView
|
||||
import eu.kanade.tachiyomi.App
|
||||
import eu.kanade.tachiyomi.R
|
||||
import icepick.Icepick
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
open class BaseActivity : AppCompatActivity() {
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
Icepick.restoreInstanceState(this, savedState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
Icepick.saveInstanceState(this, outState)
|
||||
}
|
||||
|
||||
protected fun setupToolbar(toolbar: Toolbar) {
|
||||
setSupportActionBar(toolbar)
|
||||
supportActionBar?.setDisplayHomeAsUpEnabled(true)
|
||||
|
|
|
@ -1,23 +1,11 @@
|
|||
package eu.kanade.tachiyomi.ui.base.fragment
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.app.Fragment
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity
|
||||
import icepick.Icepick
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
open class BaseFragment : Fragment() {
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
Icepick.restoreInstanceState(this, savedState)
|
||||
}
|
||||
|
||||
override fun onSaveInstanceState(outState: Bundle) {
|
||||
super.onSaveInstanceState(outState)
|
||||
Icepick.saveInstanceState(this, outState)
|
||||
}
|
||||
|
||||
fun setToolbarTitle(title: String) {
|
||||
baseActivity.setToolbarTitle(title)
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package eu.kanade.tachiyomi.ui.base.presenter
|
||||
|
||||
import android.content.Context
|
||||
import android.os.Bundle
|
||||
import icepick.Icepick
|
||||
import nucleus.view.ViewWithPresenter
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
|
||||
|
@ -10,16 +8,6 @@ open class BasePresenter<V : ViewWithPresenter<*>> : RxPresenter<V>() {
|
|||
|
||||
lateinit var context: Context
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
Icepick.restoreInstanceState(this, savedState)
|
||||
}
|
||||
|
||||
override fun onSave(state: Bundle) {
|
||||
super.onSave(state)
|
||||
Icepick.saveInstanceState(this, state)
|
||||
}
|
||||
|
||||
fun registerForEvents() {
|
||||
EventBus.getDefault().register(this)
|
||||
}
|
||||
|
|
|
@ -172,7 +172,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
|
|||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||
|
||||
val onItemSelected = object : AdapterView.OnItemSelectedListener {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View? , position: Int, id: Long) {
|
||||
override fun onItemSelected(parent: AdapterView<*>, view: View?, position: Int, id: Long) {
|
||||
val source = spinnerAdapter.getItem(position)
|
||||
if (selectedIndex != position || adapter.isEmpty) {
|
||||
// Set previous selection if it's not a valid source and notify the user
|
||||
|
|
|
@ -144,7 +144,7 @@ class ReaderActivity : BaseRxActivity<ReaderPresenter>() {
|
|||
override fun onBackPressed() {
|
||||
presenter.onChapterLeft()
|
||||
|
||||
val chapterToUpdate = presenter.mangaSyncChapterToUpdate
|
||||
val chapterToUpdate = presenter.getMangaSyncChapterToUpdate()
|
||||
|
||||
if (chapterToUpdate > 0) {
|
||||
if (presenter.prefs.askUpdateMangaSync()) {
|
||||
|
|
|
@ -1,424 +0,0 @@
|
|||
package eu.kanade.tachiyomi.ui.reader;
|
||||
|
||||
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 eu.kanade.tachiyomi.data.cache.ChapterCache;
|
||||
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.UpdateMangaSyncService;
|
||||
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.data.source.model.Page;
|
||||
import eu.kanade.tachiyomi.event.ReaderEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import icepick.State;
|
||||
import rx.Observable;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import rx.subjects.PublishSubject;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class ReaderPresenter extends BasePresenter<ReaderActivity> {
|
||||
|
||||
@Inject PreferencesHelper prefs;
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject DownloadManager downloadManager;
|
||||
@Inject MangaSyncManager syncManager;
|
||||
@Inject SourceManager sourceManager;
|
||||
@Inject ChapterCache chapterCache;
|
||||
|
||||
@State Manga manga;
|
||||
@State Chapter activeChapter;
|
||||
@State int requestedPage;
|
||||
private Page currentPage;
|
||||
private Source source;
|
||||
private Chapter nextChapter;
|
||||
private Chapter previousChapter;
|
||||
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_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) {
|
||||
source = sourceManager.get(manga.source);
|
||||
initializeSubjects();
|
||||
}
|
||||
|
||||
seamlessMode = prefs.seamlessMode();
|
||||
|
||||
startableLatestCache(GET_ADJACENT_CHAPTERS, this::getAdjacentChaptersObservable,
|
||||
(view, pair) -> view.onAdjacentChapters(pair.first, pair.second));
|
||||
|
||||
startable(PRELOAD_NEXT_CHAPTER, this::getPreloadNextChapterObservable,
|
||||
next -> {},
|
||||
error -> Timber.e("Error preloading chapter"));
|
||||
|
||||
|
||||
restartable(GET_MANGA_SYNC, () -> getMangaSyncObservable().subscribe());
|
||||
|
||||
restartableLatestCache(GET_PAGE_LIST,
|
||||
() -> getPageListObservable(activeChapter),
|
||||
(view, chapter) -> view.onChapterReady(manga, activeChapter, currentPage),
|
||||
(view, error) -> view.onChapterError());
|
||||
|
||||
if (savedState == null) {
|
||||
registerForEvents();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterForEvents();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSave(@NonNull Bundle state) {
|
||||
onChapterLeft();
|
||||
super.onSave(state);
|
||||
}
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
public void onEvent(ReaderEvent event) {
|
||||
EventBus.getDefault().removeStickyEvent(event);
|
||||
manga = event.getManga();
|
||||
source = sourceManager.get(manga.source);
|
||||
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<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())
|
||||
).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(activeChapter).asRxObservable().take(1),
|
||||
db.getNextChapter(activeChapter).asRxObservable().take(1),
|
||||
Pair::create)
|
||||
.doOnNext(pair -> {
|
||||
previousChapter = pair.first;
|
||||
nextChapter = pair.second;
|
||||
})
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
// Preload the first pages of the next chapter. Only for non seamless mode
|
||||
private Observable<Page> getPreloadNextChapterObservable() {
|
||||
return source.getCachedPageListOrPullFromNetwork(nextChapter.url)
|
||||
.flatMap(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);
|
||||
}
|
||||
|
||||
private Observable<List<MangaSync>> getMangaSyncObservable() {
|
||||
return db.getMangasSync(manga).asRxObservable()
|
||||
.take(1)
|
||||
.doOnNext(mangaSync -> this.mangaSyncList = mangaSync);
|
||||
}
|
||||
|
||||
private void loadChapter(Chapter chapter) {
|
||||
loadChapter(chapter, 0);
|
||||
}
|
||||
|
||||
// Loads the given chapter
|
||||
private void loadChapter(Chapter chapter, int requestedPage) {
|
||||
if (seamlessMode) {
|
||||
if (appenderSubscription != null)
|
||||
remove(appenderSubscription);
|
||||
} else {
|
||||
stopPreloadingNextChapter();
|
||||
}
|
||||
|
||||
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)
|
||||
this.requestedPage = chapter.last_page_read;
|
||||
else
|
||||
this.requestedPage = requestedPage;
|
||||
|
||||
// Reset next and previous chapter. They have to be fetched again
|
||||
nextChapter = null;
|
||||
previousChapter = 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
|
||||
public boolean isChapterDownloaded(Chapter chapter) {
|
||||
return downloadManager.isChapterDownloaded(source, manga, chapter);
|
||||
}
|
||||
|
||||
public void retryPage(Page page) {
|
||||
if (page != null) {
|
||||
page.setStatus(Page.QUEUE);
|
||||
if (page.getImagePath() != null) {
|
||||
File file = new File(page.getImagePath());
|
||||
chapterCache.removeFileFromCache(file.getName());
|
||||
}
|
||||
retryPageSubject.onNext(page);
|
||||
}
|
||||
}
|
||||
|
||||
// Called before loading another chapter or leaving the reader. It allows to do operations
|
||||
// over the chapter read like saving progress
|
||||
public void onChapterLeft() {
|
||||
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 (!activeChapter.isDownloaded()) {
|
||||
source.savePageList(activeChapter.url, pages);
|
||||
}
|
||||
|
||||
// Save current progress of the chapter. Mark as read if the chapter is finished
|
||||
if (activePage.isLastPage()) {
|
||||
activeChapter.read = true;
|
||||
}
|
||||
db.insertChapter(activeChapter).asRxObservable().subscribe();
|
||||
}
|
||||
|
||||
public int getMangaSyncChapterToUpdate() {
|
||||
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 (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);
|
||||
|
||||
// We know the chapter we have to check, but we don't know yet if an update is required.
|
||||
// This boolean is used to return 0 if no update is required
|
||||
boolean hasToUpdate = false;
|
||||
|
||||
for (MangaSync mangaSync : mangaSyncList) {
|
||||
if (lastChapterReadLocal > mangaSync.last_chapter_read) {
|
||||
mangaSync.last_chapter_read = lastChapterReadLocal;
|
||||
mangaSync.update = true;
|
||||
hasToUpdate = true;
|
||||
}
|
||||
}
|
||||
return hasToUpdate ? lastChapterReadLocal : 0;
|
||||
}
|
||||
|
||||
public void updateMangaSyncLastChapterRead() {
|
||||
for (MangaSync mangaSync : mangaSyncList) {
|
||||
MangaSyncService service = syncManager.getService(mangaSync.sync_id);
|
||||
if (service.isLogged() && mangaSync.update) {
|
||||
UpdateMangaSyncService.start(getContext(), mangaSync);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setCurrentPage(Page currentPage) {
|
||||
this.currentPage = currentPage;
|
||||
}
|
||||
|
||||
public boolean loadNextChapter() {
|
||||
if (hasNextChapter()) {
|
||||
onChapterLeft();
|
||||
loadChapter(nextChapter, 0);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public boolean loadPreviousChapter() {
|
||||
if (hasPreviousChapter()) {
|
||||
onChapterLeft();
|
||||
loadChapter(previousChapter, -1);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
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 (!isUnsubscribed(PRELOAD_NEXT_CHAPTER)) {
|
||||
stop(PRELOAD_NEXT_CHAPTER);
|
||||
if (nextChapter.getPages() != null)
|
||||
source.savePageList(nextChapter.url, nextChapter.getPages());
|
||||
}
|
||||
}
|
||||
|
||||
public void updateMangaViewer(int viewer) {
|
||||
manga.viewer = viewer;
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
}
|
||||
|
||||
public Manga getManga() {
|
||||
return manga;
|
||||
}
|
||||
|
||||
public Page getCurrentPage() {
|
||||
return currentPage;
|
||||
}
|
||||
|
||||
public boolean isSeamlessMode() {
|
||||
return seamlessMode;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,421 @@
|
|||
package eu.kanade.tachiyomi.ui.reader
|
||||
|
||||
import android.os.Bundle
|
||||
import android.util.Pair
|
||||
import eu.kanade.tachiyomi.data.cache.ChapterCache
|
||||
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.UpdateMangaSyncService
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.source.SourceManager
|
||||
import eu.kanade.tachiyomi.data.source.base.Source
|
||||
import eu.kanade.tachiyomi.data.source.model.Page
|
||||
import eu.kanade.tachiyomi.event.ReaderEvent
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import org.greenrobot.eventbus.Subscribe
|
||||
import org.greenrobot.eventbus.ThreadMode
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import rx.subjects.PublishSubject
|
||||
import timber.log.Timber
|
||||
import java.io.File
|
||||
import javax.inject.Inject
|
||||
|
||||
class ReaderPresenter : BasePresenter<ReaderActivity>() {
|
||||
|
||||
@Inject lateinit var prefs: PreferencesHelper
|
||||
@Inject lateinit var db: DatabaseHelper
|
||||
@Inject lateinit var downloadManager: DownloadManager
|
||||
@Inject lateinit var syncManager: MangaSyncManager
|
||||
@Inject lateinit var sourceManager: SourceManager
|
||||
@Inject lateinit var chapterCache: ChapterCache
|
||||
|
||||
lateinit var manga: Manga
|
||||
private set
|
||||
|
||||
lateinit var chapter: Chapter
|
||||
private set
|
||||
|
||||
lateinit var source: Source
|
||||
private set
|
||||
|
||||
var requestedPage: Int = 0
|
||||
var currentPage: Page? = null
|
||||
private var nextChapter: Chapter? = null
|
||||
private var previousChapter: Chapter? = null
|
||||
private var mangaSyncList: List<MangaSync>? = null
|
||||
|
||||
private lateinit var retryPageSubject: PublishSubject<Page>
|
||||
private lateinit var pageInitializerSubject: PublishSubject<Chapter>
|
||||
|
||||
val isSeamlessMode by lazy { prefs.seamlessMode() }
|
||||
|
||||
private var appenderSubscription: Subscription? = null
|
||||
|
||||
private val GET_PAGE_LIST = 1
|
||||
private val GET_ADJACENT_CHAPTERS = 2
|
||||
private val GET_MANGA_SYNC = 3
|
||||
private val PRELOAD_NEXT_CHAPTER = 4
|
||||
|
||||
private val MANGA_KEY = "manga_key"
|
||||
private val CHAPTER_KEY = "chapter_key"
|
||||
private val PAGE_KEY = "page_key"
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
if (savedState != null) {
|
||||
source = sourceManager.get(manga.source)!!
|
||||
manga = savedState.getSerializable(MANGA_KEY) as Manga
|
||||
chapter = savedState.getSerializable(CHAPTER_KEY) as Chapter
|
||||
requestedPage = savedState.getInt(PAGE_KEY)
|
||||
initializeSubjects()
|
||||
}
|
||||
|
||||
startableLatestCache(GET_ADJACENT_CHAPTERS,
|
||||
{ getAdjacentChaptersObservable() },
|
||||
{ view, pair -> view.onAdjacentChapters(pair.first, pair.second) })
|
||||
|
||||
startable(PRELOAD_NEXT_CHAPTER, { getPreloadNextChapterObservable() },
|
||||
{ },
|
||||
{ error -> Timber.e("Error preloading chapter") })
|
||||
|
||||
|
||||
restartable(GET_MANGA_SYNC,
|
||||
{ getMangaSyncObservable().subscribe() })
|
||||
|
||||
restartableLatestCache(GET_PAGE_LIST,
|
||||
{ getPageListObservable(chapter) },
|
||||
{ view, chapter -> view.onChapterReady(manga, chapter, currentPage) },
|
||||
{ view, error -> view.onChapterError() })
|
||||
|
||||
if (savedState == null) {
|
||||
registerForEvents()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroy() {
|
||||
unregisterForEvents()
|
||||
super.onDestroy()
|
||||
}
|
||||
|
||||
override fun onSave(state: Bundle) {
|
||||
onChapterLeft()
|
||||
state.putSerializable(MANGA_KEY, manga)
|
||||
state.putSerializable(CHAPTER_KEY, chapter)
|
||||
state.putSerializable(PAGE_KEY, requestedPage)
|
||||
super.onSave(state)
|
||||
}
|
||||
|
||||
@Subscribe(sticky = true, threadMode = ThreadMode.MAIN)
|
||||
fun onEvent(event: ReaderEvent) {
|
||||
EventBus.getDefault().removeStickyEvent(event)
|
||||
manga = event.manga
|
||||
source = sourceManager.get(manga.source)!!
|
||||
initializeSubjects()
|
||||
loadChapter(event.chapter)
|
||||
if (prefs.autoUpdateMangaSync()) {
|
||||
start(GET_MANGA_SYNC)
|
||||
}
|
||||
}
|
||||
|
||||
private fun initializeSubjects() {
|
||||
// Listen for pages initialization events
|
||||
pageInitializerSubject = PublishSubject.create<Chapter>()
|
||||
add(pageInitializerSubject.observeOn(Schedulers.io())
|
||||
.concatMap { ch ->
|
||||
val observable: Observable<Page>
|
||||
if (ch.isDownloaded) {
|
||||
val chapterDir = downloadManager.getAbsoluteChapterDirectory(source, manga, ch)
|
||||
observable = Observable.from(ch.pages)
|
||||
.flatMap { downloadManager.getDownloadedImage(it, chapterDir) }
|
||||
} else {
|
||||
observable = source.getAllImageUrlsFromPageList(ch.pages)
|
||||
.flatMap({ source.getCachedImage(it) }, 2)
|
||||
.doOnCompleted { source.savePageList(ch.url, ch.pages) }
|
||||
}
|
||||
observable.doOnCompleted {
|
||||
if (!isSeamlessMode && chapter === ch) {
|
||||
preloadNextChapter()
|
||||
}
|
||||
}
|
||||
}.subscribe())
|
||||
|
||||
// Listen por retry events
|
||||
retryPageSubject = PublishSubject.create<Page>()
|
||||
add(retryPageSubject.observeOn(Schedulers.io())
|
||||
.flatMap { page ->
|
||||
if (page.imageUrl == null)
|
||||
source.getImageUrlFromPage(page)
|
||||
else
|
||||
Observable.just<Page>(page)
|
||||
}
|
||||
.flatMap { source.getCachedImage(it) }
|
||||
.subscribe())
|
||||
}
|
||||
|
||||
// Returns the page list of a chapter
|
||||
private fun getPageListObservable(chapter: Chapter): Observable<Chapter> {
|
||||
val observable: Observable<List<Page>> = if (chapter.isDownloaded)
|
||||
// Fetch the page list from disk
|
||||
Observable.just(downloadManager.getSavedPageList(source, manga, chapter)!!)
|
||||
else
|
||||
// Fetch the page list from cache or fallback to network
|
||||
source.getCachedPageListOrPullFromNetwork(chapter.url)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
||||
return observable.map { pages ->
|
||||
for (page in pages) {
|
||||
page.chapter = chapter
|
||||
}
|
||||
chapter.pages = pages
|
||||
if (requestedPage >= -1 || currentPage == null) {
|
||||
if (requestedPage == -1) {
|
||||
currentPage = pages[pages.size - 1]
|
||||
} else {
|
||||
currentPage = pages[requestedPage]
|
||||
}
|
||||
}
|
||||
requestedPage = -2
|
||||
pageInitializerSubject.onNext(chapter)
|
||||
chapter
|
||||
}
|
||||
}
|
||||
|
||||
private fun getAdjacentChaptersObservable(): Observable<Pair<Chapter, Chapter>> {
|
||||
return Observable.zip(
|
||||
db.getPreviousChapter(chapter).asRxObservable().take(1),
|
||||
db.getNextChapter(chapter).asRxObservable().take(1),
|
||||
{ a, b -> Pair.create(a, b) })
|
||||
.doOnNext { pair ->
|
||||
previousChapter = pair.first
|
||||
nextChapter = pair.second
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
}
|
||||
|
||||
// Preload the first pages of the next chapter. Only for non seamless mode
|
||||
private fun getPreloadNextChapterObservable(): Observable<Page> {
|
||||
return source.getCachedPageListOrPullFromNetwork(nextChapter!!.url)
|
||||
.flatMap { pages ->
|
||||
nextChapter!!.pages = pages
|
||||
val pagesToPreload = Math.min(pages.size, 5)
|
||||
Observable.from(pages).take(pagesToPreload)
|
||||
}
|
||||
// Preload up to 5 images
|
||||
.concatMap { page ->
|
||||
if (page.imageUrl == null)
|
||||
source.getImageUrlFromPage(page)
|
||||
else
|
||||
Observable.just<Page>(page)
|
||||
}
|
||||
// Download the first image
|
||||
.concatMap { page ->
|
||||
if (page.pageNumber == 0)
|
||||
source.getCachedImage(page)
|
||||
else
|
||||
Observable.just<Page>(page)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnCompleted { stopPreloadingNextChapter() }
|
||||
}
|
||||
|
||||
private fun getMangaSyncObservable(): Observable<List<MangaSync>> {
|
||||
return db.getMangasSync(manga).asRxObservable()
|
||||
.take(1)
|
||||
.doOnNext { mangaSyncList = it }
|
||||
}
|
||||
|
||||
// Loads the given chapter
|
||||
private fun loadChapter(chapter: Chapter, requestedPage: Int = 0) {
|
||||
if (isSeamlessMode) {
|
||||
if (appenderSubscription != null)
|
||||
remove(appenderSubscription)
|
||||
} else {
|
||||
stopPreloadingNextChapter()
|
||||
}
|
||||
|
||||
this.chapter = chapter
|
||||
chapter.status = if (isChapterDownloaded(chapter)) Download.DOWNLOADED else 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)
|
||||
this.requestedPage = chapter.last_page_read
|
||||
else
|
||||
this.requestedPage = requestedPage
|
||||
|
||||
// Reset next and previous chapter. They have to be fetched again
|
||||
nextChapter = null
|
||||
previousChapter = null
|
||||
|
||||
start(GET_PAGE_LIST)
|
||||
start(GET_ADJACENT_CHAPTERS)
|
||||
}
|
||||
|
||||
fun setActiveChapter(chapter: Chapter) {
|
||||
onChapterLeft()
|
||||
this.chapter = chapter
|
||||
nextChapter = null
|
||||
previousChapter = null
|
||||
start(GET_ADJACENT_CHAPTERS)
|
||||
}
|
||||
|
||||
fun appendNextChapter() {
|
||||
if (nextChapter == null)
|
||||
return
|
||||
|
||||
if (appenderSubscription != null)
|
||||
remove(appenderSubscription)
|
||||
|
||||
nextChapter?.let {
|
||||
if (appenderSubscription != null)
|
||||
remove(appenderSubscription)
|
||||
|
||||
it.status = if (isChapterDownloaded(it)) Download.DOWNLOADED else Download.NOT_DOWNLOADED
|
||||
|
||||
appenderSubscription = getPageListObservable(it).subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.compose(deliverLatestCache<Chapter>())
|
||||
.subscribe(split({ view, chapter ->
|
||||
view.onAppendChapter(chapter)
|
||||
}, { view, error ->
|
||||
view.onChapterAppendError()
|
||||
}))
|
||||
|
||||
add(appenderSubscription)
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Check whether the given chapter is downloaded
|
||||
fun isChapterDownloaded(chapter: Chapter): Boolean {
|
||||
return downloadManager.isChapterDownloaded(source, manga, chapter)
|
||||
}
|
||||
|
||||
fun retryPage(page: Page?) {
|
||||
if (page != null) {
|
||||
page.status = Page.QUEUE
|
||||
if (page.imagePath != null) {
|
||||
val file = File(page.imagePath)
|
||||
chapterCache.removeFileFromCache(file.name)
|
||||
}
|
||||
retryPageSubject.onNext(page)
|
||||
}
|
||||
}
|
||||
|
||||
// Called before loading another chapter or leaving the reader. It allows to do operations
|
||||
// over the chapter read like saving progress
|
||||
fun onChapterLeft() {
|
||||
val pages = chapter.pages ?: return
|
||||
|
||||
// Get the last page read
|
||||
var activePageNumber = chapter.last_page_read
|
||||
|
||||
// Just in case, avoid out of index exceptions
|
||||
if (activePageNumber >= pages.size) {
|
||||
activePageNumber = pages.size - 1
|
||||
}
|
||||
val activePage = pages[activePageNumber]
|
||||
|
||||
// Cache current page list progress for online chapters to allow a faster reopen
|
||||
if (!chapter.isDownloaded) {
|
||||
source.savePageList(chapter.url, pages)
|
||||
}
|
||||
|
||||
// Save current progress of the chapter. Mark as read if the chapter is finished
|
||||
if (activePage.isLastPage) {
|
||||
chapter.read = true
|
||||
}
|
||||
db.insertChapter(chapter).asRxObservable().subscribe()
|
||||
}
|
||||
|
||||
// If the current chapter has been read, we check with this one
|
||||
// If not, we check if the previous chapter has been read
|
||||
// We know the chapter we have to check, but we don't know yet if an update is required.
|
||||
// This boolean is used to return 0 if no update is required
|
||||
fun getMangaSyncChapterToUpdate(): Int {
|
||||
if (chapter.pages == null || mangaSyncList == null || mangaSyncList!!.isEmpty())
|
||||
return 0
|
||||
|
||||
var lastChapterReadLocal = 0
|
||||
if (chapter.read)
|
||||
lastChapterReadLocal = Math.floor(chapter.chapter_number.toDouble()).toInt()
|
||||
else if (previousChapter != null && previousChapter!!.read)
|
||||
lastChapterReadLocal = Math.floor(previousChapter!!.chapter_number.toDouble()).toInt()
|
||||
var hasToUpdate = false
|
||||
|
||||
for (mangaSync in mangaSyncList!!) {
|
||||
if (lastChapterReadLocal > mangaSync.last_chapter_read) {
|
||||
mangaSync.last_chapter_read = lastChapterReadLocal
|
||||
mangaSync.update = true
|
||||
hasToUpdate = true
|
||||
}
|
||||
}
|
||||
return if (hasToUpdate) lastChapterReadLocal else 0
|
||||
}
|
||||
|
||||
fun updateMangaSyncLastChapterRead() {
|
||||
for (mangaSync in mangaSyncList!!) {
|
||||
val service = syncManager.getService(mangaSync.sync_id)
|
||||
if (service.isLogged && mangaSync.update) {
|
||||
UpdateMangaSyncService.start(context, mangaSync)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun loadNextChapter(): Boolean {
|
||||
nextChapter?.let {
|
||||
onChapterLeft()
|
||||
loadChapter(it, 0)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun loadPreviousChapter(): Boolean {
|
||||
previousChapter?.let {
|
||||
onChapterLeft()
|
||||
loadChapter(it, 0)
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
fun hasNextChapter(): Boolean {
|
||||
return nextChapter != null
|
||||
}
|
||||
|
||||
fun hasPreviousChapter(): Boolean {
|
||||
return previousChapter != null
|
||||
}
|
||||
|
||||
private fun preloadNextChapter() {
|
||||
if (hasNextChapter() && !isChapterDownloaded(nextChapter!!)) {
|
||||
start(PRELOAD_NEXT_CHAPTER)
|
||||
}
|
||||
}
|
||||
|
||||
private fun stopPreloadingNextChapter() {
|
||||
if (!isUnsubscribed(PRELOAD_NEXT_CHAPTER)) {
|
||||
stop(PRELOAD_NEXT_CHAPTER)
|
||||
if (nextChapter!!.pages != null)
|
||||
source.savePageList(nextChapter!!.url, nextChapter!!.pages)
|
||||
}
|
||||
}
|
||||
|
||||
fun updateMangaViewer(viewer: Int) {
|
||||
manga.viewer = viewer
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
}
|
Reference in a new issue