Rename project

This commit is contained in:
inorichi
2016-01-15 15:18:19 +01:00
parent 1508bf42fb
commit 70f4c7fcc3
167 changed files with 647 additions and 654 deletions
@@ -0,0 +1,358 @@
package eu.kanade.tachiyomi.ui.reader;
import android.content.Context;
import android.content.Intent;
import android.content.pm.ActivityInfo;
import android.graphics.Color;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.Toolbar;
import android.view.Menu;
import android.view.MenuItem;
import android.view.Surface;
import android.view.View;
import android.view.WindowManager;
import android.widget.TextView;
import com.afollestad.materialdialogs.MaterialDialog;
import java.util.List;
import javax.inject.Inject;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity;
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.LeftToRightReader;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal.RightToLeftReader;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical.VerticalReader;
import eu.kanade.tachiyomi.ui.reader.viewer.webtoon.WebtoonReader;
import eu.kanade.tachiyomi.util.GLUtil;
import eu.kanade.tachiyomi.util.ToastUtil;
import icepick.Icepick;
import nucleus.factory.RequiresPresenter;
import rx.Subscription;
import rx.subscriptions.CompositeSubscription;
@RequiresPresenter(ReaderPresenter.class)
public class ReaderActivity extends BaseRxActivity<ReaderPresenter> {
@Bind(R.id.page_number) TextView pageNumber;
@Bind(R.id.toolbar) Toolbar toolbar;
@Inject PreferencesHelper preferences;
private BaseReader viewer;
private ReaderMenu readerMenu;
private int uiFlags;
private int readerTheme;
protected CompositeSubscription subscriptions;
private Subscription customBrightnessSubscription;
private int maxBitmapSize;
public static final int LEFT_TO_RIGHT = 1;
public static final int RIGHT_TO_LEFT = 2;
public static final int VERTICAL = 3;
public static final int WEBTOON = 4;
public static final int BLACK_THEME = 1;
public static Intent newIntent(Context context) {
return new Intent(context, ReaderActivity.class);
}
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
App.get(this).getComponent().inject(this);
setContentView(R.layout.activity_reader);
ButterKnife.bind(this);
setupToolbar(toolbar);
subscriptions = new CompositeSubscription();
readerMenu = new ReaderMenu(this);
Icepick.restoreInstanceState(readerMenu, savedState);
if (savedState != null && readerMenu.showing)
readerMenu.show(false);
initializeSettings();
maxBitmapSize = GLUtil.getMaxTextureSize();
}
@Override
protected void onResume() {
super.onResume();
setSystemUiVisibility();
}
@Override
protected void onPause() {
if (viewer != null)
getPresenter().setCurrentPage(viewer.getCurrentPage());
super.onPause();
}
@Override
protected void onDestroy() {
subscriptions.unsubscribe();
viewer = null;
super.onDestroy();
}
@Override
public boolean onCreateOptionsMenu(Menu menu) {
return readerMenu.onCreateOptionsMenu(menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
return readerMenu.onOptionsItemSelected(item) || super.onOptionsItemSelected(item);
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
Icepick.saveInstanceState(readerMenu, outState);
super.onSaveInstanceState(outState);
}
@Override
public void onBackPressed() {
if (viewer != null)
getPresenter().setCurrentPage(viewer.getCurrentPage());
getPresenter().onChapterLeft();
int chapterToUpdate = getPresenter().getMangaSyncChapterToUpdate();
if (chapterToUpdate > 0) {
if (getPresenter().prefs.askUpdateMangaSync()) {
new MaterialDialog.Builder(this)
.content(getString(R.string.confirm_update_manga_sync, chapterToUpdate))
.positiveText(R.string.button_yes)
.negativeText(R.string.button_no)
.onPositive((dialog, which) -> {
getPresenter().updateMangaSyncLastChapterRead();
})
.onAny((dialog1, which1) -> {
finish();
})
.show();
} else {
getPresenter().updateMangaSyncLastChapterRead();
finish();
}
} else {
super.onBackPressed();
}
}
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 (viewer == null) {
viewer = createViewer(manga);
getSupportFragmentManager().beginTransaction().replace(R.id.reader, viewer).commit();
}
viewer.onPageListReady(pages, currentPage);
readerMenu.onChapterReady(pages.size(), manga, chapter, currentPage);
}
public void onAdjacentChapters(Chapter previous, Chapter next) {
readerMenu.onAdjacentChapters(previous, next);
}
private BaseReader createViewer(Manga manga) {
int mangaViewer = manga.viewer == 0 ? preferences.getDefaultViewer() : manga.viewer;
switch (mangaViewer) {
case LEFT_TO_RIGHT: default:
return new LeftToRightReader();
case RIGHT_TO_LEFT:
return new RightToLeftReader();
case VERTICAL:
return new VerticalReader();
case WEBTOON:
return new WebtoonReader();
}
}
public void onPageChanged(int currentPageIndex, int totalPages) {
String page = (currentPageIndex + 1) + "/" + totalPages;
pageNumber.setText(page);
readerMenu.onPageChanged(currentPageIndex);
}
public void setSelectedPage(int pageIndex) {
viewer.setSelectedPage(pageIndex);
}
public void onCenterSingleTap() {
readerMenu.toggle();
}
public void requestNextChapter() {
getPresenter().setCurrentPage(viewer != null ? viewer.getCurrentPage() : 0);
if (!getPresenter().loadNextChapter()) {
ToastUtil.showShort(this, R.string.no_next_chapter);
}
}
public void requestPreviousChapter() {
getPresenter().setCurrentPage(viewer != null ? viewer.getCurrentPage() : 0);
if (!getPresenter().loadPreviousChapter()) {
ToastUtil.showShort(this, R.string.no_previous_chapter);
}
}
private void initializeSettings() {
subscriptions.add(preferences.showPageNumber()
.asObservable()
.subscribe(this::setPageNumberVisibility));
subscriptions.add(preferences.lockOrientation()
.asObservable()
.subscribe(this::setOrientation));
subscriptions.add(preferences.hideStatusBar()
.asObservable()
.subscribe(this::setStatusBarVisibility));
subscriptions.add(preferences.keepScreenOn()
.asObservable()
.subscribe(this::setKeepScreenOn));
subscriptions.add(preferences.customBrightness()
.asObservable()
.subscribe(this::setCustomBrightness));
subscriptions.add(preferences.readerTheme()
.asObservable()
.distinctUntilChanged()
.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 setPageNumberVisibility(boolean visible) {
pageNumber.setVisibility(visible ? View.VISIBLE : View.INVISIBLE);
}
private void setKeepScreenOn(boolean enabled) {
if (enabled) {
getWindow().addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
} else {
getWindow().clearFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON);
}
}
private void setCustomBrightness(boolean enabled) {
if (enabled) {
subscriptions.add(customBrightnessSubscription = preferences.customBrightnessValue()
.asObservable()
.subscribe(this::setCustomBrightnessValue));
} else {
if (customBrightnessSubscription != null)
subscriptions.remove(customBrightnessSubscription);
setCustomBrightnessValue(-1);
}
}
private void setCustomBrightnessValue(float value) {
WindowManager.LayoutParams layout = getWindow().getAttributes();
layout.screenBrightness = value;
getWindow().setAttributes(layout);
}
private void setStatusBarVisibility(boolean hidden) {
createUiHideFlags(hidden);
setSystemUiVisibility();
}
private void createUiHideFlags(boolean statusBarHidden) {
uiFlags = 0;
uiFlags |= View.SYSTEM_UI_FLAG_HIDE_NAVIGATION;
if (statusBarHidden)
uiFlags |= View.SYSTEM_UI_FLAG_FULLSCREEN;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT)
uiFlags |= View.SYSTEM_UI_FLAG_IMMERSIVE_STICKY;
}
public void setSystemUiVisibility() {
getWindow().getDecorView().setSystemUiVisibility(uiFlags);
}
protected void setMangaDefaultViewer(int viewer) {
getPresenter().updateMangaViewer(viewer);
recreate();
}
private void applyTheme(int theme) {
readerTheme = theme;
View rootView = getWindow().getDecorView().getRootView();
if (theme == BLACK_THEME) {
rootView.setBackgroundColor(Color.BLACK);
pageNumber.setTextColor(ContextCompat.getColor(this, R.color.light_grey));
pageNumber.setBackgroundColor(ContextCompat.getColor(this, R.color.page_number_background_black));
} else {
rootView.setBackgroundColor(Color.WHITE);
pageNumber.setTextColor(ContextCompat.getColor(this, R.color.primary_text));
pageNumber.setBackgroundColor(ContextCompat.getColor(this, R.color.page_number_background));
}
}
public int getReaderTheme() {
return readerTheme;
}
public PreferencesHelper getPreferences() {
return preferences;
}
public BaseReader getViewer() {
return viewer;
}
public int getMaxBitmapSize() {
return maxBitmapSize;
}
}
@@ -0,0 +1,402 @@
package eu.kanade.tachiyomi.ui.reader;
import android.app.Dialog;
import android.content.Context;
import android.support.v7.widget.Toolbar;
import android.view.Gravity;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.WindowManager;
import android.view.WindowManager.LayoutParams;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.CheckBox;
import android.widget.ImageButton;
import android.widget.LinearLayout;
import android.widget.PopupWindow;
import android.widget.RelativeLayout;
import android.widget.SeekBar;
import android.widget.TextView;
import com.afollestad.materialdialogs.MaterialDialog;
import java.text.DecimalFormat;
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.preference.PreferencesHelper;
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
import icepick.State;
import rx.Subscription;
public class ReaderMenu {
@Bind(R.id.reader_menu) RelativeLayout menu;
@Bind(R.id.reader_menu_bottom) LinearLayout bottomMenu;
@Bind(R.id.toolbar) Toolbar toolbar;
@Bind(R.id.current_page) TextView currentPage;
@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_selector) ImageButton readerSelector;
@Bind(R.id.reader_extra_settings) ImageButton extraSettings;
@Bind(R.id.reader_brightness) ImageButton brightnessSettings;
private MenuItem nextChapterBtn;
private MenuItem prevChapterBtn;
private Chapter prevChapter;
private Chapter nextChapter;
private ReaderActivity activity;
private PreferencesHelper preferences;
@State boolean showing;
private PopupWindow settingsPopup;
private PopupWindow brightnessPopup;
private boolean inverted;
private DecimalFormat decimalFormat;
public ReaderMenu(ReaderActivity activity) {
this.activity = activity;
this.preferences = activity.getPreferences();
ButterKnife.bind(this, activity);
// Intercept all image events in this layout
bottomMenu.setOnTouchListener((v, event) -> true);
seekBar.setOnSeekBarChangeListener(new PageSeekBarChangeListener());
decimalFormat = new DecimalFormat("#.##");
inverted = false;
initializeOptions();
}
public void add(Subscription subscription) {
activity.subscriptions.add(subscription);
}
public void toggle() {
if (showing)
hide();
else
show(true);
}
public void show(boolean animate) {
menu.setVisibility(View.VISIBLE);
if (animate) {
Animation toolbarAnimation = AnimationUtils.loadAnimation(activity, R.anim.enter_from_top);
toolbar.startAnimation(toolbarAnimation);
Animation bottomMenuAnimation = AnimationUtils.loadAnimation(activity, R.anim.enter_from_bottom);
bottomMenu.startAnimation(bottomMenuAnimation);
}
showing = true;
}
public void hide() {
Animation toolbarAnimation = AnimationUtils.loadAnimation(activity, R.anim.exit_to_top);
toolbarAnimation.setAnimationListener(new HideMenuAnimationListener());
toolbar.startAnimation(toolbarAnimation);
Animation bottomMenuAnimation = AnimationUtils.loadAnimation(activity, R.anim.exit_to_bottom);
bottomMenu.startAnimation(bottomMenuAnimation);
settingsPopup.dismiss();
brightnessPopup.dismiss();
showing = false;
}
public boolean onCreateOptionsMenu(Menu menu) {
activity.getMenuInflater().inflate(R.menu.reader, menu);
nextChapterBtn = menu.findItem(R.id.action_next_chapter);
prevChapterBtn = menu.findItem(R.id.action_previous_chapter);
setAdjacentChaptersVisibility();
return true;
}
public boolean onOptionsItemSelected(MenuItem item) {
if (item == prevChapterBtn) {
activity.requestPreviousChapter();
} else if (item == nextChapterBtn) {
activity.requestNextChapter();
} else {
return false;
}
return true;
}
public void onChapterReady(int numPages, Manga manga, Chapter chapter, int currentPageIndex) {
if (manga.viewer == ReaderActivity.RIGHT_TO_LEFT && !inverted) {
// Invert the seekbar and textview fields for the right to left reader
seekBar.setRotation(180);
TextView aux = currentPage;
currentPage = totalPages;
totalPages = aux;
// Don't invert again on chapter change
inverted = true;
}
// Set initial values
totalPages.setText("" + numPages);
currentPage.setText("" + (currentPageIndex + 1));
seekBar.setProgress(currentPageIndex);
seekBar.setMax(numPages - 1);
activity.setToolbarTitle(manga.title);
activity.setToolbarSubtitle(chapter.chapter_number != -1 ?
activity.getString(R.string.chapter_subtitle,
decimalFormat.format(chapter.chapter_number)) :
chapter.name);
}
public void onPageChanged(int pageIndex) {
currentPage.setText("" + (pageIndex + 1));
seekBar.setProgress(pageIndex);
}
public void onAdjacentChapters(Chapter previous, Chapter next) {
prevChapter = previous;
nextChapter = next;
setAdjacentChaptersVisibility();
}
private void setAdjacentChaptersVisibility() {
if (prevChapterBtn != null) prevChapterBtn.setVisible(prevChapter != null);
if (nextChapterBtn != null) nextChapterBtn.setVisible(nextChapter != null);
}
private void initializeOptions() {
// Orientation changes
add(preferences.lockOrientation().asObservable()
.subscribe(locked -> {
int resourceId = !locked ? R.drawable.ic_screen_rotation :
activity.getResources().getConfiguration().orientation == 1 ?
R.drawable.ic_screen_lock_portrait :
R.drawable.ic_screen_lock_landscape;
lockOrientation.setImageResource(resourceId);
}));
lockOrientation.setOnClickListener(v ->
preferences.lockOrientation().set(!preferences.lockOrientation().get()));
// Reader selector
readerSelector.setOnClickListener(v -> {
final Manga manga = activity.getPresenter().getManga();
showImmersiveDialog(new MaterialDialog.Builder(activity)
.items(R.array.viewers_selector)
.itemsCallbackSingleChoice(manga.viewer,
(d, itemView, which, text) -> {
activity.setMangaDefaultViewer(which);
return true;
})
.build());
});
// Extra settings menu
final View popupView = activity.getLayoutInflater().inflate(R.layout.reader_popup, null);
settingsPopup = new SettingsPopupWindow(popupView);
extraSettings.setOnClickListener(v -> {
if (!settingsPopup.isShowing())
settingsPopup.showAtLocation(extraSettings,
Gravity.BOTTOM | Gravity.RIGHT, 0, bottomMenu.getHeight());
else
settingsPopup.dismiss();
});
// Brightness popup
final View brightnessView = activity.getLayoutInflater().inflate(R.layout.reader_brightness, null);
brightnessPopup = new BrightnessPopupWindow(brightnessView);
brightnessSettings.setOnClickListener(v -> {
if (!brightnessPopup.isShowing())
brightnessPopup.showAtLocation(brightnessSettings,
Gravity.BOTTOM | Gravity.LEFT, 0, bottomMenu.getHeight());
else
brightnessPopup.dismiss();
});
}
private void showImmersiveDialog(Dialog dialog) {
// Hack to not leave immersive mode
dialog.getWindow().setFlags(LayoutParams.FLAG_NOT_FOCUSABLE,
LayoutParams.FLAG_NOT_FOCUSABLE);
dialog.getWindow().getDecorView().setSystemUiVisibility(
activity.getWindow().getDecorView().getSystemUiVisibility());
dialog.show();
dialog.getWindow().clearFlags(LayoutParams.FLAG_NOT_FOCUSABLE);
WindowManager wm = (WindowManager) activity.getSystemService(Context.WINDOW_SERVICE);
wm.updateViewLayout(activity.getWindow().getDecorView(), activity.getWindow().getAttributes());
}
class SettingsPopupWindow extends PopupWindow {
@Bind(R.id.enable_transitions) CheckBox enableTransitions;
@Bind(R.id.show_page_number) CheckBox showPageNumber;
@Bind(R.id.hide_status_bar) CheckBox hideStatusBar;
@Bind(R.id.keep_screen_on) CheckBox keepScreenOn;
@Bind(R.id.reader_theme) CheckBox readerTheme;
@Bind(R.id.image_decoder) TextView imageDecoder;
@Bind(R.id.image_decoder_initial) TextView imageDecoderInitial;
public SettingsPopupWindow(View view) {
super(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
setAnimationStyle(R.style.reader_settings_popup_animation);
ButterKnife.bind(this, view);
initializePopupMenu();
}
private void initializePopupMenu() {
// Load values from preferences
enableTransitions.setChecked(preferences.enableTransitions().get());
showPageNumber.setChecked(preferences.showPageNumber().get());
hideStatusBar.setChecked(preferences.hideStatusBar().get());
keepScreenOn.setChecked(preferences.keepScreenOn().get());
readerTheme.setChecked(preferences.readerTheme().get() == 1);
setDecoderInitial(preferences.imageDecoder().get());
// Add a listener to change the corresponding setting
enableTransitions.setOnCheckedChangeListener((view, isChecked) ->
preferences.enableTransitions().set(isChecked));
showPageNumber.setOnCheckedChangeListener((view, isChecked) ->
preferences.showPageNumber().set(isChecked));
hideStatusBar.setOnCheckedChangeListener((view, isChecked) ->
preferences.hideStatusBar().set(isChecked));
keepScreenOn.setOnCheckedChangeListener((view, isChecked) ->
preferences.keepScreenOn().set(isChecked));
readerTheme.setOnCheckedChangeListener((view, isChecked) ->
preferences.readerTheme().set(isChecked ? 1 : 0));
imageDecoder.setOnClickListener(v -> {
showImmersiveDialog(new MaterialDialog.Builder(activity)
.title(R.string.pref_image_decoder)
.items(R.array.image_decoders)
.itemsCallbackSingleChoice(preferences.imageDecoder().get(),
(dialog, itemView, which, text) -> {
preferences.imageDecoder().set(which);
setDecoderInitial(which);
return true;
})
.build());
});
}
private void setDecoderInitial(int decoder) {
String initial;
switch (decoder) {
case BaseReader.SKIA_DECODER:
initial = "S";
break;
case BaseReader.RAPID_DECODER:
initial = "R";
break;
default:
initial = "";
break;
}
imageDecoderInitial.setText(initial);
}
}
class BrightnessPopupWindow extends PopupWindow {
@Bind(R.id.custom_brightness) CheckBox customBrightness;
@Bind(R.id.brightness_seekbar) SeekBar brightnessSeekbar;
public BrightnessPopupWindow(View view) {
super(view, LayoutParams.WRAP_CONTENT, LayoutParams.WRAP_CONTENT);
setAnimationStyle(R.style.reader_brightness_popup_animation);
ButterKnife.bind(this, view);
initializePopupMenu();
}
private void initializePopupMenu() {
add(preferences.customBrightness()
.asObservable()
.subscribe(isEnabled -> {
customBrightness.setChecked(isEnabled);
brightnessSeekbar.setEnabled(isEnabled);
}));
customBrightness.setOnCheckedChangeListener((view, isChecked) ->
preferences.customBrightness().set(isChecked));
brightnessSeekbar.setMax(100);
brightnessSeekbar.setProgress(Math.round(
preferences.customBrightnessValue().get() * brightnessSeekbar.getMax()));
brightnessSeekbar.setOnSeekBarChangeListener(new BrightnessSeekBarChangeListener());
}
}
class PageSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
activity.setSelectedPage(progress);
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {}
}
class BrightnessSeekBarChangeListener implements SeekBar.OnSeekBarChangeListener {
@Override
public void onProgressChanged(SeekBar seekBar, int progress, boolean fromUser) {
if (fromUser) {
preferences.customBrightnessValue().set((float) progress / seekBar.getMax());
}
}
@Override
public void onStartTrackingTouch(SeekBar seekBar) {
}
@Override
public void onStopTrackingTouch(SeekBar seekBar) {
}
}
class HideMenuAnimationListener implements Animation.AnimationListener {
@Override
public void onAnimationStart(Animation animation) {
}
@Override
public void onAnimationEnd(Animation animation) {
menu.setVisibility(View.GONE);
}
@Override
public void onAnimationRepeat(Animation animation) {
}
}
}
@@ -0,0 +1,361 @@
package eu.kanade.tachiyomi.ui.reader;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.util.Pair;
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.mangasync.MangaSyncManager;
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.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.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;
@State Manga manga;
@State Chapter chapter;
@State int sourceId;
@State boolean isDownloaded;
@State int 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 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;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
if (savedState != null) {
onProcessRestart();
}
retryPageSubject = PublishSubject.create();
restartableLatestCache(PRELOAD_NEXT_CHAPTER,
this::getPreloadNextChapterObservable,
(view, pages) -> {},
(view, error) -> Timber.e("An error occurred while preloading a chapter"));
restartableLatestCache(GET_PAGE_IMAGES,
this::getPageImagesObservable,
(view, page) -> {},
(view, error) -> Timber.e("An error occurred while downloading an image"));
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"));
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),
(view, error) -> view.onChapterError());
restartableFirst(GET_MANGA_SYNC, this::getMangaSyncObservable,
(view, mangaSync) -> {},
(view, error) -> {});
registerForStickyEvents();
}
@Override
protected void onDestroy() {
unregisterForEvents();
super.onDestroy();
}
@Override
protected void onSave(@NonNull Bundle state) {
onChapterLeft();
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) {
EventBus.getDefault().removeStickyEvent(event);
manga = event.getManga();
source = event.getSource();
sourceId = source.getId();
loadChapter(event.getChapter());
if (prefs.autoUpdateMangaSync()) {
start(GET_MANGA_SYNC);
}
}
// Returns the page list of a chapter
private Observable<List<Page>> 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<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);
}
private Observable<Pair<Chapter, Chapter>> getAdjacentChaptersObservable() {
return Observable.zip(
db.getPreviousChapter(chapter).createObservable().take(1),
db.getNextChapter(chapter).createObservable().take(1),
Pair::create)
.doOnNext(pair -> {
previousChapter = pair.first;
nextChapter = pair.second;
})
.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
private Observable<Page> 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(page -> page.getImageUrl() == null ?
source.getImageUrlFromPage(page) :
Observable.just(page))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnCompleted(this::stopPreloadingNextChapter);
}
private Observable<List<MangaSync>> getMangaSyncObservable() {
return db.getMangasSync(manga).createObservable()
.doOnNext(mangaSync -> this.mangaSyncList = mangaSync);
}
// 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 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;
// Reset next and previous chapter. They have to be fetched again
nextChapter = null;
previousChapter = null;
nextChapterPageList = null;
start(GET_PAGE_LIST);
}
// Check whether the given chapter is downloaded
public boolean isChapterDownloaded(Chapter chapter) {
return downloadManager.isChapterDownloaded(source, manga, chapter);
}
public void retryPage(Page page) {
page.setStatus(Page.QUEUE);
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() {
if (pageList == null)
return;
// Cache current page list progress for online chapters to allow a faster reopen
if (!isDownloaded)
source.savePageList(chapter.url, pageList);
// Save current progress of the chapter. Mark as read if the chapter is finished
chapter.last_page_read = currentPage;
if (isChapterFinished()) {
chapter.read = true;
}
db.insertChapter(chapter).createObservable().subscribe();
}
// Check whether the chapter has been read
private boolean isChapterFinished() {
return !chapter.read && currentPage == pageList.size() - 1;
}
public int getMangaSyncChapterToUpdate() {
if (pageList == 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 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.getSyncService(mangaSync.sync_id);
if (service.isLogged() && mangaSync.update) {
UpdateMangaSyncService.start(getContext(), mangaSync);
}
}
}
public void setCurrentPage(int currentPage) {
this.currentPage = currentPage;
}
public boolean loadNextChapter() {
if (hasNextChapter()) {
onChapterLeft();
loadChapter(nextChapter);
return true;
}
return false;
}
public boolean loadPreviousChapter() {
if (hasPreviousChapter()) {
onChapterLeft();
loadChapter(previousChapter);
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 (isSubscribed(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;
}
}
@@ -0,0 +1,72 @@
package eu.kanade.tachiyomi.ui.reader.viewer.base;
import android.view.MotionEvent;
import com.davemorrissey.labs.subscaleview.decoder.ImageRegionDecoder;
import com.davemorrissey.labs.subscaleview.decoder.RapidImageRegionDecoder;
import com.davemorrissey.labs.subscaleview.decoder.SkiaImageRegionDecoder;
import java.util.List;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
public abstract class BaseReader extends BaseFragment {
protected int currentPage;
protected List<Page> pages;
protected Class<? extends ImageRegionDecoder> regionDecoderClass;
public static final int RAPID_DECODER = 0;
public static final int SKIA_DECODER = 1;
public void updatePageNumber() {
getReaderActivity().onPageChanged(getCurrentPage(), getTotalPages());
}
public int getCurrentPage() {
return currentPage;
}
public int getPageForPosition(int position) {
return position;
}
public int getPositionForPage(int page) {
return page;
}
public void onPageChanged(int position) {
currentPage = getPageForPosition(position);
updatePageNumber();
}
public int getTotalPages() {
return pages == null ? 0 : pages.size();
}
public abstract void setSelectedPage(int pageNumber);
public abstract void onPageListReady(List<Page> pages, int currentPage);
public abstract boolean onImageTouch(MotionEvent motionEvent);
public void setRegionDecoderClass(int value) {
switch (value) {
case RAPID_DECODER:
default:
regionDecoderClass = RapidImageRegionDecoder.class;
break;
case SKIA_DECODER:
regionDecoderClass = SkiaImageRegionDecoder.class;
break;
}
}
public Class<? extends ImageRegionDecoder> getRegionDecoderClass() {
return regionDecoderClass;
}
public ReaderActivity getReaderActivity() {
return (ReaderActivity) getActivity();
}
}
@@ -0,0 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.base;
public interface OnChapterBoundariesOutListener {
void onFirstPageOutEvent();
void onLastPageOutEvent();
}
@@ -0,0 +1,7 @@
package eu.kanade.tachiyomi.ui.reader.viewer.base;
public interface OnChapterSingleTapListener {
void onCenterTap();
void onLeftSideTap();
void onRightSideTap();
}
@@ -0,0 +1,37 @@
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 {
void setId(int id);
void setLayoutParams(ViewGroup.LayoutParams layoutParams);
void setOffscreenPageLimit(int limit);
int getCurrentItem();
void setCurrentItem(int item, boolean smoothScroll);
int getWidth();
int getHeight();
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();
}
@@ -0,0 +1,71 @@
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();
}
}
}
@@ -0,0 +1,116 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager;
import android.view.MotionEvent;
import android.view.ViewGroup;
import java.util.List;
import eu.kanade.tachiyomi.R;
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 rx.subscriptions.CompositeSubscription;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
public abstract class PagerReader extends BaseReader {
protected PagerReaderAdapter adapter;
protected Pager pager;
protected boolean transitions;
protected CompositeSubscription subscriptions;
protected void initializePager(Pager pager) {
this.pager = pager;
pager.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
pager.setOffscreenPageLimit(1);
pager.setId(R.id.view_pager);
pager.setOnChapterBoundariesOutListener(new OnChapterBoundariesOutListener() {
@Override
public void onFirstPageOutEvent() {
onFirstPageOut();
}
@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);
}
});
adapter = new PagerReaderAdapter(getChildFragmentManager());
pager.setAdapter(adapter);
subscriptions = new CompositeSubscription();
subscriptions.add(getReaderActivity().getPreferences().imageDecoder()
.asObservable()
.doOnNext(this::setRegionDecoderClass)
.skip(1)
.distinctUntilChanged()
.subscribe(v -> adapter.notifyDataSetChanged()));
subscriptions.add(getReaderActivity().getPreferences().enableTransitions()
.asObservable()
.subscribe(value -> transitions = value));
setPages();
}
@Override
public void onDestroyView() {
subscriptions.unsubscribe();
super.onDestroyView();
}
@Override
public void onPageListReady(List<Page> pages, int currentPage) {
if (this.pages != pages) {
this.pages = pages;
this.currentPage = currentPage;
if (isResumed()) {
setPages();
}
}
}
protected void setPages() {
if (pages != null) {
pager.clearOnPageChangeListeners();
adapter.setPages(pages);
setSelectedPage(currentPage);
updatePageNumber();
pager.setOnPageChangeListener(this::onPageChanged);
}
}
@Override
public void setSelectedPage(int pageNumber) {
pager.setCurrentItem(getPositionForPage(pageNumber), false);
}
@Override
public boolean onImageTouch(MotionEvent motionEvent) {
return pager.onImageTouch(motionEvent);
}
public abstract void onFirstPageOut();
public abstract void onLastPageOut();
}
@@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import java.util.List;
import eu.kanade.tachiyomi.data.source.model.Page;
public class PagerReaderAdapter extends FragmentStatePagerAdapter {
private List<Page> pages;
public PagerReaderAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
@Override
public int getCount() {
return pages == null ? 0 : pages.size();
}
@Override
public Fragment getItem(int position) {
return PagerReaderFragment.newInstance(pages.get(position));
}
public List<Page> getPages() {
return pages;
}
public void setPages(List<Page> pages) {
this.pages = pages;
notifyDataSetChanged();
}
@Override
public int getItemPosition(Object object) {
return POSITION_NONE;
}
}
@@ -0,0 +1,219 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.LinearLayout;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.davemorrissey.labs.subscaleview.ImageSource;
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subjects.PublishSubject;
public class PagerReaderFragment extends BaseFragment {
@Bind(R.id.page_image_view) SubsamplingScaleImageView imageView;
@Bind(R.id.progress_container) LinearLayout progressContainer;
@Bind(R.id.progress) ProgressBar progressBar;
@Bind(R.id.progress_text) TextView progressText;
@Bind(R.id.retry_button) Button retryButton;
private Page page;
private Subscription progressSubscription;
private Subscription statusSubscription;
public static PagerReaderFragment newInstance(Page page) {
PagerReaderFragment fragment = new PagerReaderFragment();
fragment.setPage(page);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, @Nullable ViewGroup container, @Nullable Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.item_pager_reader, container, false);
ButterKnife.bind(this, view);
ReaderActivity activity = getReaderActivity();
BaseReader parentFragment = (BaseReader) getParentFragment();
if (activity.getReaderTheme() == ReaderActivity.BLACK_THEME) {
progressText.setTextColor(ContextCompat.getColor(getContext(), R.color.light_grey));
}
imageView.setParallelLoadingEnabled(true);
imageView.setMaxDimensions(activity.getMaxBitmapSize(), activity.getMaxBitmapSize());
imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED);
imageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE);
imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE);
imageView.setRegionDecoderClass(parentFragment.getRegionDecoderClass());
imageView.setOnTouchListener((v, motionEvent) -> parentFragment.onImageTouch(motionEvent));
imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() {
@Override
public void onImageLoadError(Exception e) {
showImageLoadError();
}
});
retryButton.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
if (page != null)
activity.getPresenter().retryPage(page);
}
return true;
});
observeStatus();
return view;
}
@Override
public void onDestroyView() {
unsubscribeProgress();
unsubscribeStatus();
ButterKnife.unbind(this);
super.onDestroyView();
}
public void setPage(Page page) {
this.page = page;
}
private void showImage() {
if (page == null || page.getImagePath() == null)
return;
imageView.setImage(ImageSource.uri(page.getImagePath()));
progressContainer.setVisibility(View.GONE);
}
private void showDownloading() {
progressContainer.setVisibility(View.VISIBLE);
progressText.setVisibility(View.VISIBLE);
}
private void showLoading() {
progressContainer.setVisibility(View.VISIBLE);
progressText.setVisibility(View.VISIBLE);
progressText.setText(R.string.downloading);
}
private void showError() {
progressContainer.setVisibility(View.GONE);
retryButton.setVisibility(View.VISIBLE);
}
private void hideError() {
retryButton.setVisibility(View.GONE);
}
private void showImageLoadError() {
ViewGroup view = (ViewGroup) getView();
if (view == null)
return;
TextView errorText = new TextView(getContext());
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));
view.addView(errorText);
}
private void processStatus(int status) {
switch (status) {
case Page.QUEUE:
hideError();
break;
case Page.LOAD_PAGE:
showLoading();
break;
case Page.DOWNLOAD_IMAGE:
observeProgress();
showDownloading();
break;
case Page.READY:
showImage();
unsubscribeProgress();
unsubscribeStatus();
break;
case Page.ERROR:
showError();
unsubscribeProgress();
break;
}
}
private void observeStatus() {
if (page == null || statusSubscription != null)
return;
PublishSubject<Integer> statusSubject = PublishSubject.create();
page.setStatusSubject(statusSubject);
statusSubscription = statusSubject
.startWith(page.getStatus())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::processStatus);
}
private void observeProgress() {
if (progressSubscription != null)
return;
final AtomicInteger currentValue = new AtomicInteger(-1);
progressSubscription = Observable.interval(75, TimeUnit.MILLISECONDS, Schedulers.newThread())
.onBackpressureDrop()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(tick -> {
// Refresh UI only if progress change
if (page.getProgress() != currentValue.get()) {
currentValue.set(page.getProgress());
progressText.setText(getString(R.string.download_progress, page.getProgress()));
}
});
}
private void unsubscribeStatus() {
if (statusSubscription != null) {
page.setStatusSubject(null);
statusSubscription.unsubscribe();
statusSubscription = null;
}
}
private void unsubscribeProgress() {
if (progressSubscription != null) {
progressSubscription.unsubscribe();
progressSubscription = null;
}
}
private ReaderActivity getReaderActivity() {
return (ReaderActivity) getActivity();
}
}
@@ -0,0 +1,124 @@
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.PagerGestureListener;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager;
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
public boolean onInterceptTouchEvent(MotionEvent ev) {
try {
if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
if (getCurrentItem() == 0 || getCurrentItem() == getAdapter().getCount() - 1) {
startDragX = ev.getX();
}
}
return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
return true;
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
try {
if (onChapterBoundariesOutListener != null) {
if (getCurrentItem() == 0) {
if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
float displacement = ev.getX() - startDragX;
if (ev.getX() > startDragX && displacement > getWidth() * SWIPE_TOLERANCE) {
onChapterBoundariesOutListener.onFirstPageOutEvent();
return true;
}
startDragX = 0;
}
} else if (getCurrentItem() == getAdapter().getCount() - 1) {
if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
float displacement = startDragX - ev.getX();
if (ev.getX() < startDragX && displacement > getWidth() * SWIPE_TOLERANCE) {
onChapterBoundariesOutListener.onLastPageOutEvent();
return true;
}
startDragX = 0;
}
}
}
return super.onTouchEvent(ev);
} catch (IllegalArgumentException e) {
return true;
}
}
@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() {
@Override
public void onPageSelected(int position) {
function.call(position);
}
});
}
}
@@ -0,0 +1,19 @@
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;
}
}
@@ -0,0 +1,15 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
public class LeftToRightReader extends HorizontalReader {
@Override
public void onFirstPageOut() {
getReaderActivity().requestPreviousChapter();
}
@Override
public void onLastPageOut() {
getReaderActivity().requestNextChapter();
}
}
@@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager.horizontal;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import eu.kanade.tachiyomi.data.source.model.Page;
public class RightToLeftReader extends HorizontalReader {
@Override
public void onPageListReady(List<Page> pages, int currentPage) {
ArrayList<Page> inversedPages = new ArrayList<>(pages);
Collections.reverse(inversedPages);
super.onPageListReady(inversedPages, currentPage);
}
@Override
public int getPageForPosition(int position) {
return (getTotalPages() - 1) - position;
}
@Override
public int getPositionForPage(int page) {
return (getTotalPages() - 1) - page;
}
@Override
public void onFirstPageOut() {
getReaderActivity().requestNextChapter();
}
@Override
public void onLastPageOut() {
getReaderActivity().requestPreviousChapter();
}
}
@@ -0,0 +1,138 @@
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.PagerGestureListener;
import eu.kanade.tachiyomi.ui.reader.viewer.pager.Pager;
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 VerticalPagerGestureListener(this));
}
@Override
public boolean onInterceptTouchEvent(MotionEvent ev) {
try {
if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_DOWN) {
if (getCurrentItem() == 0 || getCurrentItem() == getAdapter().getCount() - 1) {
startDragY = ev.getY();
}
}
return super.onInterceptTouchEvent(ev);
} catch (IllegalArgumentException e) {
return true;
}
}
@Override
public boolean onTouchEvent(MotionEvent ev) {
try {
if (onChapterBoundariesOutListener != null) {
if (getCurrentItem() == 0) {
if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
float displacement = ev.getY() - startDragY;
if (ev.getY() > startDragY && displacement > getHeight() * SWIPE_TOLERANCE) {
onChapterBoundariesOutListener.onFirstPageOutEvent();
return true;
}
startDragY = 0;
}
} else if (getCurrentItem() == getAdapter().getCount() - 1) {
if ((ev.getAction() & MotionEvent.ACTION_MASK) == MotionEvent.ACTION_UP) {
float displacement = startDragY - ev.getY();
if (ev.getY() < startDragY && displacement > getHeight() * SWIPE_TOLERANCE) {
onChapterBoundariesOutListener.onLastPageOutEvent();
return true;
}
startDragY = 0;
}
}
}
return super.onTouchEvent(ev);
} catch (IllegalArgumentException e) {
return true;
}
}
@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() {
@Override
public void onPageSelected(int position) {
function.call(position);
}
});
}
private static class VerticalPagerGestureListener extends PagerGestureListener {
public VerticalPagerGestureListener(Pager pager) {
super(pager);
}
@Override
public boolean onDown(MotionEvent e) {
// Vertical view pager ignores scrolling events sometimes.
// Returning true here fixes it, but we lose touch events on the image like
// double tap to zoom
return true;
}
}
}
@@ -0,0 +1,29 @@
package eu.kanade.tachiyomi.ui.reader.viewer.pager.vertical;
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 VerticalReader extends PagerReader {
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
VerticalPager pager = new VerticalPager(getActivity());
initializePager(pager);
return pager;
}
@Override
public void onFirstPageOut() {
getReaderActivity().requestPreviousChapter();
}
@Override
public void onLastPageOut() {
getReaderActivity().requestNextChapter();
}
}
@@ -0,0 +1,68 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.source.model.Page;
public class WebtoonAdapter extends RecyclerView.Adapter<WebtoonHolder> {
private WebtoonReader fragment;
private List<Page> pages;
private View.OnTouchListener touchListener;
public WebtoonAdapter(WebtoonReader fragment) {
this.fragment = fragment;
pages = new ArrayList<>();
touchListener = (v, event) -> fragment.onImageTouch(event);
}
public Page getItem(int position) {
return pages.get(position);
}
@Override
public WebtoonHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = fragment.getActivity().getLayoutInflater();
View v = inflater.inflate(R.layout.item_webtoon_reader, parent, false);
return new WebtoonHolder(v, this, touchListener);
}
@Override
public void onBindViewHolder(WebtoonHolder holder, int position) {
final Page page = getItem(position);
holder.onSetValues(page);
}
@Override
public int getItemCount() {
return pages.size();
}
public void setPages(List<Page> pages) {
this.pages = pages;
notifyDataSetChanged();
}
public void clear() {
if (pages != null) {
pages.clear();
notifyDataSetChanged();
}
}
public void retryPage(Page page) {
fragment.getReaderActivity().getPresenter().retryPage(page);
}
public WebtoonReader getReader() {
return fragment;
}
}
@@ -0,0 +1,120 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon;
import android.support.v7.widget.RecyclerView;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.view.animation.Animation;
import android.view.animation.AnimationUtils;
import android.widget.Button;
import android.widget.ProgressBar;
import com.davemorrissey.labs.subscaleview.ImageSource;
import com.davemorrissey.labs.subscaleview.SubsamplingScaleImageView;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.source.model.Page;
public class WebtoonHolder extends RecyclerView.ViewHolder {
@Bind(R.id.page_image_view) SubsamplingScaleImageView imageView;
@Bind(R.id.frame_container) ViewGroup container;
@Bind(R.id.progress) ProgressBar progressBar;
@Bind(R.id.retry_button) Button retryButton;
private Animation fadeInAnimation;
private Page page;
private WebtoonAdapter adapter;
public WebtoonHolder(View view, WebtoonAdapter adapter, View.OnTouchListener touchListener) {
super(view);
this.adapter = adapter;
ButterKnife.bind(this, view);
fadeInAnimation = AnimationUtils.loadAnimation(view.getContext(), R.anim.fade_in);
imageView.setParallelLoadingEnabled(true);
imageView.setDoubleTapZoomStyle(SubsamplingScaleImageView.ZOOM_FOCUS_FIXED);
imageView.setPanLimit(SubsamplingScaleImageView.PAN_LIMIT_INSIDE);
imageView.setMinimumScaleType(SubsamplingScaleImageView.SCALE_TYPE_CENTER_INSIDE);
imageView.setOnTouchListener(touchListener);
imageView.setOnImageEventListener(new SubsamplingScaleImageView.DefaultOnImageEventListener() {
@Override
public void onImageLoaded() {
imageView.startAnimation(fadeInAnimation);
}
});
progressBar.setMinimumHeight(view.getResources().getDisplayMetrics().heightPixels);
container.setOnTouchListener(touchListener);
retryButton.setOnTouchListener((v, event) -> {
if (event.getAction() == MotionEvent.ACTION_UP) {
if (page != null)
adapter.retryPage(page);
return true;
}
return true;
});
}
public void onSetValues(Page page) {
this.page = page;
switch (page.getStatus()) {
case Page.QUEUE:
onQueue();
break;
case Page.LOAD_PAGE:
onLoading();
break;
case Page.DOWNLOAD_IMAGE:
onLoading();
break;
case Page.READY:
onReady();
break;
case Page.ERROR:
onError();
break;
}
}
private void onLoading() {
setErrorButtonVisible(false);
setImageVisible(false);
setProgressVisible(true);
}
private void onReady() {
setErrorButtonVisible(false);
setProgressVisible(false);
setImageVisible(true);
imageView.setRegionDecoderClass(adapter.getReader().getRegionDecoderClass());
imageView.setImage(ImageSource.uri(page.getImagePath()));
}
private void onError() {
setImageVisible(false);
setProgressVisible(false);
setErrorButtonVisible(true);
}
private void onQueue() {
setImageVisible(false);
setErrorButtonVisible(false);
setProgressVisible(false);
}
private void setProgressVisible(boolean visible) {
progressBar.setVisibility(visible ? View.VISIBLE : View.GONE);
}
private void setImageVisible(boolean visible) {
imageView.setVisibility(visible ? View.VISIBLE : View.GONE);
}
private void setErrorButtonVisible(boolean visible) {
retryButton.setVisibility(visible ? View.VISIBLE : View.GONE);
}
}
@@ -0,0 +1,160 @@
package eu.kanade.tachiyomi.ui.reader.viewer.webtoon;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.GestureDetector;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.reader.viewer.base.BaseReader;
import eu.kanade.tachiyomi.widget.PreCachingLayoutManager;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.subjects.PublishSubject;
import static android.view.GestureDetector.SimpleOnGestureListener;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
public class WebtoonReader extends BaseReader {
private WebtoonAdapter adapter;
private RecyclerView recycler;
private PreCachingLayoutManager layoutManager;
private Subscription subscription;
private Subscription decoderSubscription;
private GestureDetector gestureDetector;
@Nullable
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
adapter = new WebtoonAdapter(this);
layoutManager = new PreCachingLayoutManager(getActivity());
layoutManager.setExtraLayoutSpace(getResources().getDisplayMetrics().heightPixels);
recycler = new RecyclerView(getActivity());
recycler.setLayoutParams(new ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT));
recycler.setLayoutManager(layoutManager);
recycler.setItemAnimator(null);
recycler.setAdapter(adapter);
decoderSubscription = getReaderActivity().getPreferences().imageDecoder()
.asObservable()
.doOnNext(this::setRegionDecoderClass)
.skip(1)
.distinctUntilChanged()
.subscribe(v -> adapter.notifyDataSetChanged());
gestureDetector = new GestureDetector(getActivity(), new SimpleOnGestureListener() {
@Override
public boolean onSingleTapConfirmed(MotionEvent e) {
getReaderActivity().onCenterSingleTap();
return true;
}
@Override
public boolean onDown(MotionEvent e) {
// The only way I've found to allow panning. Double tap event (zoom) is lost
// but panning should be the most used one
return true;
}
});
setPages();
return recycler;
}
@Override
public void onDestroyView() {
decoderSubscription.unsubscribe();
super.onDestroyView();
}
@Override
public void onPause() {
unsubscribeStatus();
super.onPause();
}
private void unsubscribeStatus() {
if (subscription != null && !subscription.isUnsubscribed())
subscription.unsubscribe();
}
@Override
public void setSelectedPage(int pageNumber) {
recycler.scrollToPosition(getPositionForPage(pageNumber));
}
@Override
public void onPageListReady(List<Page> pages, int currentPage) {
if (this.pages != pages) {
this.pages = pages;
if (isResumed()) {
setPages();
}
}
}
private void setPages() {
if (pages != null) {
unsubscribeStatus();
recycler.clearOnScrollListeners();
adapter.clear();
recycler.scrollTo(0, 0);
adapter.setPages(pages);
setScrollListener();
observeStatus(0);
}
}
private void setScrollListener() {
recycler.addOnScrollListener(new RecyclerView.OnScrollListener() {
@Override
public void onScrolled(RecyclerView recyclerView, int dx, int dy) {
super.onScrolled(recyclerView, dx, dy);
currentPage = layoutManager.findLastVisibleItemPosition();
updatePageNumber();
}
});
}
@Override
public boolean onImageTouch(MotionEvent motionEvent) {
return gestureDetector.onTouchEvent(motionEvent);
}
private void observeStatus(int position) {
if (position == pages.size())
return;
final Page page = pages.get(position);
PublishSubject<Integer> statusSubject = PublishSubject.create();
page.setStatusSubject(statusSubject);
// Unsubscribe from the previous page
unsubscribeStatus();
subscription = statusSubject
.startWith(page.getStatus())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(status -> processStatus(position, status));
}
private void processStatus(int position, int status) {
adapter.notifyItemChanged(position);
if (status == Page.READY) {
observeStatus(position + 1);
}
}
}