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

View File

@@ -0,0 +1,81 @@
package eu.kanade.tachiyomi.ui.base.activity;
import android.os.Bundle;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.view.MenuItem;
import de.greenrobot.event.EventBus;
import icepick.Icepick;
public class BaseActivity extends AppCompatActivity {
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
Icepick.restoreInstanceState(this, savedState);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
protected void setupToolbar(Toolbar toolbar) {
setSupportActionBar(toolbar);
if (getSupportActionBar() != null)
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
}
public void setToolbarTitle(String title) {
if (getSupportActionBar() != null)
getSupportActionBar().setTitle(title);
}
public void setToolbarTitle(int titleResource) {
if (getSupportActionBar() != null)
getSupportActionBar().setTitle(getString(titleResource));
}
public void setToolbarSubtitle(String title) {
if (getSupportActionBar() != null)
getSupportActionBar().setSubtitle(title);
}
public void setToolbarSubtitle(int titleResource) {
if (getSupportActionBar() != null)
getSupportActionBar().setSubtitle(getString(titleResource));
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case android.R.id.home:
onBackPressed();
return true;
}
return super.onOptionsItemSelected(item);
}
public void registerForStickyEvents() {
registerForStickyEvents(0);
}
public void registerForStickyEvents(int priority) {
EventBus.getDefault().registerSticky(this, priority);
}
public void registerForEvents() {
registerForEvents(0);
}
public void registerForEvents(int priority) {
EventBus.getDefault().register(this, priority);
}
public void unregisterForEvents() {
EventBus.getDefault().unregister(this);
}
}

View File

@@ -0,0 +1,91 @@
package eu.kanade.tachiyomi.ui.base.activity;
import android.os.Bundle;
import android.support.annotation.NonNull;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import nucleus.factory.PresenterFactory;
import nucleus.factory.ReflectionPresenterFactory;
import nucleus.presenter.Presenter;
import nucleus.view.PresenterLifecycleDelegate;
import nucleus.view.ViewWithPresenter;
/**
* This class is an example of how an activity could controls it's presenter.
* You can inherit from this class or copy/paste this class's code to
* create your own view implementation.
*
* @param <P> a type of presenter to return with {@link #getPresenter}.
*/
public abstract class BaseRxActivity<P extends Presenter> extends BaseActivity implements ViewWithPresenter<P> {
private static final String PRESENTER_STATE_KEY = "presenter_state";
private PresenterLifecycleDelegate<P> presenterDelegate =
new PresenterLifecycleDelegate<>(ReflectionPresenterFactory.<P>fromViewClass(getClass()));
/**
* Returns a current presenter factory.
*/
public PresenterFactory<P> getPresenterFactory() {
return presenterDelegate.getPresenterFactory();
}
/**
* Sets a presenter factory.
* Call this method before onCreate/onFinishInflate to override default {@link ReflectionPresenterFactory} presenter factory.
* Use this method for presenter dependency injection.
*/
@Override
public void setPresenterFactory(PresenterFactory<P> presenterFactory) {
presenterDelegate.setPresenterFactory(presenterFactory);
}
/**
* Returns a current attached presenter.
* This method is guaranteed to return a non-null value between
* onResume/onPause and onAttachedToWindow/onDetachedFromWindow calls
* if the presenter factory returns a non-null value.
*
* @return a currently attached presenter or null.
*/
public P getPresenter() {
return presenterDelegate.getPresenter();
}
@Override
protected void onCreate(Bundle savedInstanceState) {
final PresenterFactory<P> superFactory = getPresenterFactory();
setPresenterFactory(() -> {
P presenter = superFactory.createPresenter();
App app = (App) getApplication();
app.getComponentReflection().inject(presenter);
((BasePresenter)presenter).setContext(app.getApplicationContext());
return presenter;
});
super.onCreate(savedInstanceState);
if (savedInstanceState != null)
presenterDelegate.onRestoreInstanceState(savedInstanceState.getBundle(PRESENTER_STATE_KEY));
}
@Override
protected void onSaveInstanceState(@NonNull Bundle outState) {
super.onSaveInstanceState(outState);
outState.putBundle(PRESENTER_STATE_KEY, presenterDelegate.onSaveInstanceState());
}
@Override
protected void onResume() {
super.onResume();
presenterDelegate.onResume(this);
}
@Override
protected void onPause() {
super.onPause();
presenterDelegate.onPause(isFinishing());
}
}

View File

@@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.ui.base.adapter;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import eu.davidea.flexibleadapter.FlexibleAdapter;
public abstract class FlexibleViewHolder extends RecyclerView.ViewHolder
implements View.OnClickListener, View.OnLongClickListener {
private final FlexibleAdapter adapter;
private final OnListItemClickListener onListItemClickListener;
public FlexibleViewHolder(View itemView,FlexibleAdapter adapter,
OnListItemClickListener onListItemClickListener) {
super(itemView);
this.adapter = adapter;
this.onListItemClickListener = onListItemClickListener;
this.itemView.setOnClickListener(this);
this.itemView.setOnLongClickListener(this);
}
@Override
public void onClick(View view) {
if (onListItemClickListener.onListItemClick(getAdapterPosition())) {
toggleActivation();
}
}
@Override
public boolean onLongClick(View view) {
onListItemClickListener.onListItemLongClick(getAdapterPosition());
toggleActivation();
return true;
}
protected void toggleActivation() {
itemView.setActivated(adapter.isSelected(getAdapterPosition()));
}
public interface OnListItemClickListener {
boolean onListItemClick(int position);
void onListItemLongClick(int position);
}
}

View File

@@ -0,0 +1,57 @@
/*
* Copyright (C) 2015 Paul Burke
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.kanade.tachiyomi.ui.base.adapter;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
/**
* Interface to listen for a move or dismissal event from a {@link ItemTouchHelper.Callback}.
*
* @author Paul Burke (ipaulpro)
*/
public interface ItemTouchHelperAdapter {
/**
* Called when an item has been dragged far enough to trigger a move. This is called every time
* an item is shifted, and <strong>not</strong> at the end of a "drop" event.<br/>
* <br/>
* Implementations should call {@link RecyclerView.Adapter#notifyItemMoved(int, int)} after
* adjusting the underlying data to reflect this move.
*
* @param fromPosition The start position of the moved item.
* @param toPosition Then resolved position of the moved item.
*
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
*/
void onItemMove(int fromPosition, int toPosition);
/**
* Called when an item has been dismissed by a swipe.<br/>
* <br/>
* Implementations should call {@link RecyclerView.Adapter#notifyItemRemoved(int)} after
* adjusting the underlying data to reflect this removal.
*
* @param position The position of the item dismissed.
*
* @see RecyclerView#getAdapterPositionFor(RecyclerView.ViewHolder)
* @see RecyclerView.ViewHolder#getAdapterPosition()
*/
void onItemDismiss(int position);
}

View File

@@ -0,0 +1,13 @@
package eu.kanade.tachiyomi.ui.base.adapter;
import android.support.v7.widget.RecyclerView;
public interface OnStartDragListener {
/**
* Called when a view is requesting a start of a drag.
*
* @param viewHolder The holder of the view to drag.
*/
void onStartDrag(RecyclerView.ViewHolder viewHolder);
}

View File

@@ -0,0 +1,43 @@
package eu.kanade.tachiyomi.ui.base.adapter;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.helper.ItemTouchHelper;
public class SimpleItemTouchHelperCallback extends ItemTouchHelper.Callback {
private final ItemTouchHelperAdapter adapter;
public SimpleItemTouchHelperCallback(ItemTouchHelperAdapter adapter) {
this.adapter = adapter;
}
@Override
public boolean isLongPressDragEnabled() {
return true;
}
@Override
public boolean isItemViewSwipeEnabled() {
return true;
}
@Override
public int getMovementFlags(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder) {
int dragFlags = ItemTouchHelper.UP | ItemTouchHelper.DOWN;
int swipeFlags = ItemTouchHelper.START | ItemTouchHelper.END;
return makeMovementFlags(dragFlags, swipeFlags);
}
@Override
public boolean onMove(RecyclerView recyclerView, RecyclerView.ViewHolder viewHolder,
RecyclerView.ViewHolder target) {
adapter.onItemMove(viewHolder.getAdapterPosition(), target.getAdapterPosition());
return true;
}
@Override
public void onSwiped(RecyclerView.ViewHolder viewHolder, int direction) {
adapter.onItemDismiss(viewHolder.getAdapterPosition());
}
}

View File

@@ -0,0 +1,48 @@
package eu.kanade.tachiyomi.ui.base.adapter;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentStatePagerAdapter;
import android.util.SparseArray;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
public abstract class SmartFragmentStatePagerAdapter extends FragmentStatePagerAdapter {
// Sparse array to keep track of registered fragments in memory
private SparseArray<Fragment> registeredFragments = new SparseArray<Fragment>();
public SmartFragmentStatePagerAdapter(FragmentManager fragmentManager) {
super(fragmentManager);
}
// Register the fragment when the item is instantiated
@Override
public Object instantiateItem(ViewGroup container, int position) {
Fragment fragment = (Fragment) super.instantiateItem(container, position);
registeredFragments.put(position, fragment);
return fragment;
}
// Unregister when the item is inactive
@Override
public void destroyItem(ViewGroup container, int position, Object object) {
registeredFragments.remove(position);
super.destroyItem(container, position, object);
}
// Returns the fragment for the position (if instantiated)
public Fragment getRegisteredFragment(int position) {
return registeredFragments.get(position);
}
public List<Fragment> getRegisteredFragments() {
ArrayList<Fragment> fragments = new ArrayList<>();
for (int i = 0; i < registeredFragments.size(); i++) {
fragments.add(registeredFragments.valueAt(i));
}
return fragments;
}
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2015 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.kanade.tachiyomi.ui.base.fab;
import android.content.Context;
import android.support.design.widget.CoordinatorLayout;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.view.ViewCompat;
import android.util.AttributeSet;
import android.view.View;
public class ScrollAwareFABBehavior extends FloatingActionButton.Behavior {
public ScrollAwareFABBehavior(Context context, AttributeSet attrs) {
super();
}
@Override
public boolean onStartNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,
final View directTargetChild, final View target, final int nestedScrollAxes) {
// Ensure we react to vertical scrolling
return nestedScrollAxes == ViewCompat.SCROLL_AXIS_VERTICAL
|| super.onStartNestedScroll(coordinatorLayout, child, directTargetChild, target, nestedScrollAxes);
}
@Override
public void onNestedScroll(final CoordinatorLayout coordinatorLayout, final FloatingActionButton child,
final View target, final int dxConsumed, final int dyConsumed,
final int dxUnconsumed, final int dyUnconsumed) {
super.onNestedScroll(coordinatorLayout, child, target, dxConsumed, dyConsumed, dxUnconsumed, dyUnconsumed);
if (dyConsumed > 0 && child.getVisibility() == View.VISIBLE) {
// User scrolled down and the FAB is currently visible -> hide the FAB
child.hide();
} else if (dyConsumed < 0 && child.getVisibility() != View.VISIBLE) {
// User scrolled up and the FAB is currently not visible -> show the FAB
child.show();
}
}
}

View File

@@ -0,0 +1,56 @@
package eu.kanade.tachiyomi.ui.base.fragment;
import android.os.Bundle;
import android.support.v4.app.Fragment;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity;
import icepick.Icepick;
public class BaseFragment extends Fragment {
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
Icepick.restoreInstanceState(this, savedState);
}
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
Icepick.saveInstanceState(this, outState);
}
public void setToolbarTitle(String title) {
getBaseActivity().setToolbarTitle(title);
}
public void setToolbarTitle(int resourceId) {
getBaseActivity().setToolbarTitle(getString(resourceId));
}
public BaseActivity getBaseActivity() {
return (BaseActivity) getActivity();
}
public void registerForStickyEvents() {
registerForStickyEvents(0);
}
public void registerForStickyEvents(int priority) {
EventBus.getDefault().registerSticky(this, priority);
}
public void registerForEvents() {
registerForEvents(0);
}
public void registerForEvents(int priority) {
EventBus.getDefault().register(this, priority);
}
public void unregisterForEvents() {
EventBus.getDefault().unregister(this);
}
}

View File

@@ -0,0 +1,88 @@
package eu.kanade.tachiyomi.ui.base.fragment;
import android.os.Bundle;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import nucleus.factory.PresenterFactory;
import nucleus.factory.ReflectionPresenterFactory;
import nucleus.presenter.Presenter;
import nucleus.view.PresenterLifecycleDelegate;
import nucleus.view.ViewWithPresenter;
/**
* This class is an example of how an activity could controls it's presenter.
* You can inherit from this class or copy/paste this class's code to
* create your own view implementation.
*
* @param <P> a type of presenter to return with {@link #getPresenter}.
*/
public abstract class BaseRxFragment<P extends Presenter> extends BaseFragment implements ViewWithPresenter<P> {
private static final String PRESENTER_STATE_KEY = "presenter_state";
private PresenterLifecycleDelegate<P> presenterDelegate =
new PresenterLifecycleDelegate<>(ReflectionPresenterFactory.<P>fromViewClass(getClass()));
/**
* Returns a current presenter factory.
*/
public PresenterFactory<P> getPresenterFactory() {
return presenterDelegate.getPresenterFactory();
}
/**
* Sets a presenter factory.
* Call this method before onCreate/onFinishInflate to override default {@link ReflectionPresenterFactory} presenter factory.
* Use this method for presenter dependency injection.
*/
@Override
public void setPresenterFactory(PresenterFactory<P> presenterFactory) {
presenterDelegate.setPresenterFactory(presenterFactory);
}
/**
* Returns a current attached presenter.
* This method is guaranteed to return a non-null value between
* onResume/onPause and onAttachedToWindow/onDetachedFromWindow calls
* if the presenter factory returns a non-null value.
*
* @return a currently attached presenter or null.
*/
public P getPresenter() {
return presenterDelegate.getPresenter();
}
@Override
public void onCreate(Bundle bundle) {
final PresenterFactory<P> superFactory = getPresenterFactory();
setPresenterFactory(() -> {
P presenter = superFactory.createPresenter();
App app = (App) getActivity().getApplication();
app.getComponentReflection().inject(presenter);
((BasePresenter)presenter).setContext(app.getApplicationContext());
return presenter;
});
super.onCreate(bundle);
if (bundle != null)
presenterDelegate.onRestoreInstanceState(bundle.getBundle(PRESENTER_STATE_KEY));
}
@Override
public void onSaveInstanceState(Bundle bundle) {
super.onSaveInstanceState(bundle);
bundle.putBundle(PRESENTER_STATE_KEY, presenterDelegate.onSaveInstanceState());
}
@Override
public void onResume() {
super.onResume();
presenterDelegate.onResume(this);
}
@Override
public void onPause() {
super.onPause();
presenterDelegate.onPause(getActivity().isFinishing());
}
}

View File

@@ -0,0 +1,47 @@
package eu.kanade.tachiyomi.ui.base.presenter;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import de.greenrobot.event.EventBus;
import icepick.Icepick;
import nucleus.view.ViewWithPresenter;
public class BasePresenter<V extends ViewWithPresenter> extends RxPresenter<V> {
private Context context;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
Icepick.restoreInstanceState(this, savedState);
}
@Override
protected void onSave(@NonNull Bundle state) {
super.onSave(state);
Icepick.saveInstanceState(this, state);
}
public void registerForStickyEvents() {
EventBus.getDefault().registerSticky(this);
}
public void registerForEvents() {
EventBus.getDefault().register(this);
}
public void unregisterForEvents() {
EventBus.getDefault().unregister(this);
}
public void setContext(Context applicationContext) {
context = applicationContext;
}
public Context getContext() {
return context;
}
}

View File

@@ -0,0 +1,343 @@
package eu.kanade.tachiyomi.ui.base.presenter;
import android.os.Bundle;
import android.support.annotation.CallSuper;
import android.support.annotation.Nullable;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Map;
import nucleus.presenter.Presenter;
import nucleus.presenter.delivery.DeliverFirst;
import nucleus.presenter.delivery.DeliverLatestCache;
import nucleus.presenter.delivery.DeliverReplay;
import nucleus.presenter.delivery.Delivery;
import rx.Observable;
import rx.Subscription;
import rx.functions.Action1;
import rx.functions.Action2;
import rx.functions.Func0;
import rx.internal.util.SubscriptionList;
import rx.subjects.BehaviorSubject;
/**
* This is an extension of {@link Presenter} which provides RxJava functionality.
*
* @param <View> a type of view.
*/
public class RxPresenter<View> extends Presenter<View> {
private static final String REQUESTED_KEY = RxPresenter.class.getName() + "#requested";
private final BehaviorSubject<View> views = BehaviorSubject.create();
private final SubscriptionList subscriptions = new SubscriptionList();
private final HashMap<Integer, Func0<Subscription>> restartables = new HashMap<>();
private final HashMap<Integer, Subscription> restartableSubscriptions = new HashMap<>();
private final ArrayList<Integer> requested = new ArrayList<>();
/**
* Returns an {@link rx.Observable} that emits the current attached view or null.
* See {@link BehaviorSubject} for more information.
*
* @return an observable that emits the current attached view or null.
*/
public Observable<View> view() {
return views;
}
/**
* Registers a subscription to automatically unsubscribe it during onDestroy.
* See {@link SubscriptionList#add(Subscription) for details.}
*
* @param subscription a subscription to add.
*/
public void add(Subscription subscription) {
subscriptions.add(subscription);
}
/**
* Removes and unsubscribes a subscription that has been registered with {@link #add} previously.
* See {@link SubscriptionList#remove(Subscription)} for details.
*
* @param subscription a subscription to remove.
*/
public void remove(Subscription subscription) {
subscriptions.remove(subscription);
}
/**
* A restartable is any RxJava observable that can be started (subscribed) and
* should be automatically restarted (re-subscribed) after a process restart if
* it was still subscribed at the moment of saving presenter's state.
*
* Registers a factory. Re-subscribes the restartable after the process restart.
*
* @param restartableId id of the restartable
* @param factory factory of the restartable
*/
public void restartable(int restartableId, Func0<Subscription> factory) {
restartables.put(restartableId, factory);
if (requested.contains(restartableId))
start(restartableId);
}
/**
* Starts the given restartable.
*
* @param restartableId id of the restartable
*/
public void start(int restartableId) {
stop(restartableId);
requested.add(restartableId);
restartableSubscriptions.put(restartableId, restartables.get(restartableId).call());
}
/**
* Unsubscribes a restartable
*
* @param restartableId id of a restartable.
*/
public void stop(int restartableId) {
requested.remove((Integer) restartableId);
Subscription subscription = restartableSubscriptions.get(restartableId);
if (subscription != null)
subscription.unsubscribe();
}
/**
* Checks if a restartable is subscribed.
*
* @param restartableId id of a restartable.
* @return True if the restartable is subscribed, false otherwise.
*/
public boolean isSubscribed(int restartableId) {
Subscription s = restartableSubscriptions.get(restartableId);
return s != null && !s.isUnsubscribed();
}
/**
* This is a shortcut that can be used instead of combining together
* {@link #restartable(int, Func0)},
* {@link #deliverFirst()},
* {@link #split(Action2, Action2)}.
*
* @param restartableId an id of the restartable.
* @param observableFactory a factory that should return an Observable when the restartable should run.
* @param onNext a callback that will be called when received data should be delivered to view.
* @param onError a callback that will be called if the source observable emits onError.
* @param <T> the type of the observable.
*/
public <T> void restartableFirst(int restartableId, final Func0<Observable<T>> observableFactory,
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
restartable(restartableId, new Func0<Subscription>() {
@Override
public Subscription call() {
return observableFactory.call()
.compose(RxPresenter.this.<T>deliverFirst())
.subscribe(split(onNext, onError));
}
});
}
/**
* This is a shortcut for calling {@link #restartableFirst(int, Func0, Action2, Action2)} with the last parameter = null.
*/
public <T> void restartableFirst(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
restartableFirst(restartableId, observableFactory, onNext, null);
}
/**
* This is a shortcut that can be used instead of combining together
* {@link #restartable(int, Func0)},
* {@link #deliverLatestCache()},
* {@link #split(Action2, Action2)}.
*
* @param restartableId an id of the restartable.
* @param observableFactory a factory that should return an Observable when the restartable should run.
* @param onNext a callback that will be called when received data should be delivered to view.
* @param onError a callback that will be called if the source observable emits onError.
* @param <T> the type of the observable.
*/
public <T> void restartableLatestCache(int restartableId, final Func0<Observable<T>> observableFactory,
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
restartable(restartableId, new Func0<Subscription>() {
@Override
public Subscription call() {
return observableFactory.call()
.compose(RxPresenter.this.<T>deliverLatestCache())
.subscribe(split(onNext, onError));
}
});
}
/**
* This is a shortcut for calling {@link #restartableLatestCache(int, Func0, Action2, Action2)} with the last parameter = null.
*/
public <T> void restartableLatestCache(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
restartableLatestCache(restartableId, observableFactory, onNext, null);
}
/**
* This is a shortcut that can be used instead of combining together
* {@link #restartable(int, Func0)},
* {@link #deliverReplay()},
* {@link #split(Action2, Action2)}.
*
* @param restartableId an id of the restartable.
* @param observableFactory a factory that should return an Observable when the restartable should run.
* @param onNext a callback that will be called when received data should be delivered to view.
* @param onError a callback that will be called if the source observable emits onError.
* @param <T> the type of the observable.
*/
public <T> void restartableReplay(int restartableId, final Func0<Observable<T>> observableFactory,
final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
restartable(restartableId, new Func0<Subscription>() {
@Override
public Subscription call() {
return observableFactory.call()
.compose(RxPresenter.this.<T>deliverReplay())
.subscribe(split(onNext, onError));
}
});
}
/**
* This is a shortcut for calling {@link #restartableReplay(int, Func0, Action2, Action2)} with the last parameter = null.
*/
public <T> void restartableReplay(int restartableId, final Func0<Observable<T>> observableFactory, final Action2<View, T> onNext) {
restartableReplay(restartableId, observableFactory, onNext, null);
}
/**
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
* the source {@link rx.Observable}.
*
* {@link #deliverLatestCache} keeps the latest onNext value and emits it each time a new view gets attached.
* If a new onNext value appears while a view is attached, it will be delivered immediately.
*
* @param <T> the type of source observable emissions
*/
public <T> DeliverLatestCache<View, T> deliverLatestCache() {
return new DeliverLatestCache<>(views);
}
/**
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
* the source {@link rx.Observable}.
*
* {@link #deliverFirst} delivers only the first onNext value that has been emitted by the source observable.
*
* @param <T> the type of source observable emissions
*/
public <T> DeliverFirst<View, T> deliverFirst() {
return new DeliverFirst<>(views);
}
/**
* Returns an {@link rx.Observable.Transformer} that couples views with data that has been emitted by
* the source {@link rx.Observable}.
*
* {@link #deliverReplay} keeps all onNext values and emits them each time a new view gets attached.
* If a new onNext value appears while a view is attached, it will be delivered immediately.
*
* @param <T> the type of source observable emissions
*/
public <T> DeliverReplay<View, T> deliverReplay() {
return new DeliverReplay<>(views);
}
/**
* Returns a method that can be used for manual restartable chain build. It returns an Action1 that splits
* a received {@link Delivery} into two {@link Action2} onNext and onError calls.
*
* @param onNext a method that will be called if the delivery contains an emitted onNext value.
* @param onError a method that will be called if the delivery contains an onError throwable.
* @param <T> a type on onNext value.
* @return an Action1 that splits a received {@link Delivery} into two {@link Action2} onNext and onError calls.
*/
public <T> Action1<Delivery<View, T>> split(final Action2<View, T> onNext, @Nullable final Action2<View, Throwable> onError) {
return new Action1<Delivery<View, T>>() {
@Override
public void call(Delivery<View, T> delivery) {
delivery.split(onNext, onError);
}
};
}
/**
* This is a shortcut for calling {@link #split(Action2, Action2)} when the second parameter is null.
*/
public <T> Action1<Delivery<View, T>> split(Action2<View, T> onNext) {
return split(onNext, null);
}
/**
* {@inheritDoc}
*/
@CallSuper
@Override
protected void onCreate(Bundle savedState) {
if (savedState != null)
requested.addAll(savedState.getIntegerArrayList(REQUESTED_KEY));
}
/**
* {@inheritDoc}
*/
@CallSuper
@Override
protected void onDestroy() {
views.onCompleted();
subscriptions.unsubscribe();
for (Map.Entry<Integer, Subscription> entry : restartableSubscriptions.entrySet())
entry.getValue().unsubscribe();
}
/**
* {@inheritDoc}
*/
@CallSuper
@Override
protected void onSave(Bundle state) {
for (int i = requested.size() - 1; i >= 0; i--) {
int restartableId = requested.get(i);
Subscription subscription = restartableSubscriptions.get(restartableId);
if (subscription != null && subscription.isUnsubscribed())
requested.remove(i);
}
state.putIntegerArrayList(REQUESTED_KEY, requested);
}
/**
* {@inheritDoc}
*/
@CallSuper
@Override
protected void onTakeView(View view) {
views.onNext(view);
}
/**
* {@inheritDoc}
*/
@CallSuper
@Override
protected void onDropView() {
views.onNext(null);
}
/**
* Please, use restartableXX and deliverXX methods for pushing data from RxPresenter into View.
*/
@Deprecated
@Nullable
@Override
public View getView() {
return super.getView();
}
}

View File

@@ -0,0 +1,60 @@
package eu.kanade.tachiyomi.ui.catalogue;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Manga;
public class CatalogueAdapter extends FlexibleAdapter<CatalogueHolder, Manga> {
private CatalogueFragment fragment;
public CatalogueAdapter(CatalogueFragment fragment) {
this.fragment = fragment;
mItems = new ArrayList<>();
setHasStableIds(true);
}
public void addItems(List<Manga> list) {
mItems.addAll(list);
notifyDataSetChanged();
}
public void clear() {
mItems.clear();
notifyDataSetChanged();
}
@Override
public long getItemId(int position) {
return mItems.get(position).id;
}
@Override
public void updateDataSet(String param) {
}
@Override
public CatalogueHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = fragment.getActivity().getLayoutInflater();
View v = inflater.inflate(R.layout.item_catalogue, parent, false);
return new CatalogueHolder(v, this, fragment);
}
@Override
public void onBindViewHolder(CatalogueHolder holder, int position) {
final Manga manga = getItem(position);
holder.onSetValues(manga, fragment.getPresenter());
//When user scrolls this bind the correct selection status
//holder.itemView.setActivated(isSelected(position));
}
}

View File

@@ -0,0 +1,262 @@
package eu.kanade.tachiyomi.ui.catalogue;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.SearchView;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AdapterView;
import android.widget.ProgressBar;
import android.widget.Spinner;
import java.util.List;
import java.util.concurrent.TimeUnit;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
import eu.kanade.tachiyomi.ui.main.MainActivity;
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
import eu.kanade.tachiyomi.util.ToastUtil;
import eu.kanade.tachiyomi.widget.AutofitRecyclerView;
import eu.kanade.tachiyomi.widget.EndlessRecyclerScrollListener;
import icepick.State;
import nucleus.factory.RequiresPresenter;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.subjects.PublishSubject;
@RequiresPresenter(CataloguePresenter.class)
public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
implements FlexibleViewHolder.OnListItemClickListener {
@Bind(R.id.recycler) AutofitRecyclerView recycler;
@Bind(R.id.progress) ProgressBar progress;
@Bind(R.id.progress_grid) ProgressBar progressGrid;
private Toolbar toolbar;
private Spinner spinner;
private CatalogueAdapter adapter;
private EndlessRecyclerScrollListener scrollListener;
@State String query = "";
@State int selectedIndex = -1;
private final int SEARCH_TIMEOUT = 1000;
private PublishSubject<String> queryDebouncerSubject;
private Subscription queryDebouncerSubscription;
public static CatalogueFragment newInstance() {
return new CatalogueFragment();
}
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_catalogue, container, false);
ButterKnife.bind(this, view);
// Initialize adapter and scroll listener
GridLayoutManager layoutManager = (GridLayoutManager) recycler.getLayoutManager();
adapter = new CatalogueAdapter(this);
scrollListener = new EndlessRecyclerScrollListener(layoutManager, this::requestNextPage);
recycler.setHasFixedSize(true);
recycler.setAdapter(adapter);
recycler.addOnScrollListener(scrollListener);
// Create toolbar spinner
Context themedContext = getBaseActivity().getSupportActionBar() != null ?
getBaseActivity().getSupportActionBar().getThemedContext() : getActivity();
spinner = new Spinner(themedContext);
CatalogueSpinnerAdapter spinnerAdapter = new CatalogueSpinnerAdapter(themedContext,
android.R.layout.simple_spinner_item, getPresenter().getEnabledSources());
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
if (savedState == null) selectedIndex = spinnerAdapter.getEmptyIndex();
spinner.setAdapter(spinnerAdapter);
spinner.setSelection(selectedIndex);
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
@Override
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
Source source = spinnerAdapter.getItem(position);
// We add an empty source with id -1 that acts as a placeholder to show a hint
// that asks to select a source
if (source.getId() != -1 && (selectedIndex != position || adapter.isEmpty())) {
// Set previous selection if it's not a valid source and notify the user
if (!getPresenter().isValidSource(source)) {
spinner.setSelection(spinnerAdapter.getEmptyIndex());
ToastUtil.showShort(getActivity(), R.string.source_requires_login);
} else {
selectedIndex = position;
showProgressBar();
getPresenter().startRequesting(source);
}
}
}
@Override
public void onNothingSelected(AdapterView<?> parent) {}
});
setToolbarTitle("");
toolbar = ((MainActivity)getActivity()).getToolbar();
toolbar.addView(spinner);
return view;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.catalogue_list, menu);
// Initialize search menu
MenuItem searchItem = menu.findItem(R.id.action_search);
final SearchView searchView = (SearchView) searchItem.getActionView();
if (!TextUtils.isEmpty(query)) {
searchItem.expandActionView();
searchView.setQuery(query, true);
searchView.clearFocus();
}
searchView.setOnQueryTextListener(new SearchView.OnQueryTextListener() {
@Override
public boolean onQueryTextSubmit(String query) {
onSearchEvent(query, true);
return true;
}
@Override
public boolean onQueryTextChange(String newText) {
onSearchEvent(newText, false);
return true;
}
});
}
@Override
public void onStart() {
super.onStart();
initializeSearchSubscription();
}
@Override
public void onStop() {
destroySearchSubscription();
super.onStop();
}
@Override
public void onDestroyView() {
toolbar.removeView(spinner);
super.onDestroyView();
}
private void initializeSearchSubscription() {
queryDebouncerSubject = PublishSubject.create();
queryDebouncerSubscription = queryDebouncerSubject
.debounce(SEARCH_TIMEOUT, TimeUnit.MILLISECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::restartRequest);
}
private void destroySearchSubscription() {
queryDebouncerSubscription.unsubscribe();
}
private void onSearchEvent(String query, boolean now) {
// If the query is not debounced, resolve it instantly
if (now)
restartRequest(query);
else if (queryDebouncerSubject != null)
queryDebouncerSubject.onNext(query);
}
private void restartRequest(String newQuery) {
// If text didn't change, do nothing
if (query.equals(newQuery)) return;
query = newQuery;
showProgressBar();
recycler.getLayoutManager().scrollToPosition(0);
getPresenter().restartRequest(query);
}
private void requestNextPage() {
if (getPresenter().hasNextPage()) {
showGridProgressBar();
getPresenter().requestNext();
}
}
public void onAddPage(int page, List<Manga> mangas) {
hideProgressBar();
if (page == 1) {
adapter.clear();
scrollListener.resetScroll();
}
adapter.addItems(mangas);
}
public void onAddPageError() {
hideProgressBar();
}
public void updateImage(Manga manga) {
CatalogueHolder holder = getHolder(manga);
if (holder != null) {
holder.setImage(manga, getPresenter());
}
}
@Nullable
private CatalogueHolder getHolder(Manga manga) {
return (CatalogueHolder) recycler.findViewHolderForItemId(manga.id);
}
private void showProgressBar() {
progress.setVisibility(ProgressBar.VISIBLE);
}
private void showGridProgressBar() {
progressGrid.setVisibility(ProgressBar.VISIBLE);
}
private void hideProgressBar() {
progress.setVisibility(ProgressBar.GONE);
progressGrid.setVisibility(ProgressBar.GONE);
}
@Override
public boolean onListItemClick(int position) {
final Manga selectedManga = adapter.getItem(position);
Intent intent = MangaActivity.newIntent(getActivity(), selectedManga);
intent.putExtra(MangaActivity.MANGA_ONLINE, true);
startActivity(intent);
return false;
}
@Override
public void onListItemLongClick(int position) {
// Do nothing
}
}

View File

@@ -0,0 +1,38 @@
package eu.kanade.tachiyomi.ui.catalogue;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
public class CatalogueHolder extends FlexibleViewHolder {
@Bind(R.id.title) TextView title;
@Bind(R.id.thumbnail) ImageView thumbnail;
@Bind(R.id.favorite_sticker) ImageView favoriteSticker;
public CatalogueHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) {
super(view, adapter, listener);
ButterKnife.bind(this, view);
}
public void onSetValues(Manga manga, CataloguePresenter presenter) {
title.setText(manga.title);
favoriteSticker.setVisibility(manga.favorite ? View.VISIBLE : View.GONE);
setImage(manga, presenter);
}
public void setImage(Manga manga, CataloguePresenter presenter) {
if (manga.thumbnail_url != null) {
presenter.coverCache.loadFromNetwork(thumbnail, manga.thumbnail_url,
presenter.getSource().getGlideHeaders());
} else {
thumbnail.setImageResource(android.R.color.transparent);
}
}
}

View File

@@ -0,0 +1,173 @@
package eu.kanade.tachiyomi.ui.catalogue;
import android.os.Bundle;
import android.text.TextUtils;
import android.util.Pair;
import com.pushtorefresh.storio.sqlite.operations.put.PutResult;
import java.util.List;
import javax.inject.Inject;
import eu.kanade.tachiyomi.data.cache.CoverCache;
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.Manga;
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.MangasPage;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import eu.kanade.tachiyomi.util.RxPager;
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 CataloguePresenter extends BasePresenter<CatalogueFragment> {
@Inject SourceManager sourceManager;
@Inject DatabaseHelper db;
@Inject CoverCache coverCache;
@Inject PreferencesHelper prefs;
private Source source;
@State int sourceId;
private String query;
private int currentPage;
private RxPager pager;
private MangasPage lastMangasPage;
private PublishSubject<List<Manga>> mangaDetailSubject;
private static final int GET_MANGA_LIST = 1;
private static final int GET_MANGA_DETAIL = 2;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
if (savedState != null) {
onProcessRestart();
}
mangaDetailSubject = PublishSubject.create();
restartableReplay(GET_MANGA_LIST,
() -> pager.pages().concatMap(page -> getMangasPageObservable(page + 1)),
(view, pair) -> view.onAddPage(pair.first, pair.second),
(view, error) -> {
view.onAddPageError();
Timber.e(error.getMessage());
});
restartableLatestCache(GET_MANGA_DETAIL,
() -> mangaDetailSubject
.observeOn(Schedulers.io())
.flatMap(Observable::from)
.filter(manga -> !manga.initialized)
.window(3)
.concatMap(pack -> pack.concatMap(this::getMangaDetails))
.onBackpressureBuffer()
.observeOn(AndroidSchedulers.mainThread()),
CatalogueFragment::updateImage,
(view, error) -> Timber.e(error.getMessage()));
}
private void onProcessRestart() {
source = sourceManager.get(sourceId);
stop(GET_MANGA_LIST);
stop(GET_MANGA_DETAIL);
}
public void startRequesting(Source source) {
this.source = source;
sourceId = source.getId();
restartRequest(null);
}
public void restartRequest(String query) {
this.query = query;
stop(GET_MANGA_LIST);
currentPage = 1;
pager = new RxPager();
start(GET_MANGA_DETAIL);
start(GET_MANGA_LIST);
}
public void requestNext() {
if (hasNextPage())
pager.requestNext(++currentPage);
}
private Observable<Pair<Integer, List<Manga>>> getMangasPageObservable(int page) {
MangasPage nextMangasPage = new MangasPage(page);
if (page != 1) {
nextMangasPage.url = lastMangasPage.nextPageUrl;
}
Observable<MangasPage> obs = !TextUtils.isEmpty(query) ?
source.searchMangasFromNetwork(nextMangasPage, query) :
source.pullPopularMangasFromNetwork(nextMangasPage);
return obs.subscribeOn(Schedulers.io())
.doOnNext(mangasPage -> lastMangasPage = mangasPage)
.flatMap(mangasPage -> Observable.from(mangasPage.mangas))
.map(this::networkToLocalManga)
.toList()
.map(mangas -> Pair.create(page, mangas))
.doOnNext(pair -> {
if (mangaDetailSubject != null)
mangaDetailSubject.onNext(pair.second);
})
.observeOn(AndroidSchedulers.mainThread());
}
private Manga networkToLocalManga(Manga networkManga) {
Manga localManga = db.getManga(networkManga.url, source.getId()).executeAsBlocking();
if (localManga == null) {
PutResult result = db.insertManga(networkManga).executeAsBlocking();
networkManga.id = result.insertedId();
localManga = networkManga;
}
return localManga;
}
private Observable<Manga> getMangaDetails(final Manga manga) {
return source.pullMangaFromNetwork(manga.url)
.subscribeOn(Schedulers.io())
.flatMap(networkManga -> {
manga.copyFrom(networkManga);
db.insertManga(manga).executeAsBlocking();
return Observable.just(manga);
})
.onErrorResumeNext(error -> Observable.just(manga));
}
public Source getSource() {
return source;
}
public boolean hasNextPage() {
return lastMangasPage != null && lastMangasPage.nextPageUrl != null;
}
public boolean isValidSource(Source source) {
if (!source.isLoginRequired() || source.isLogged())
return true;
return !(prefs.getSourceUsername(source).equals("")
|| prefs.getSourcePassword(source).equals(""));
}
public List<Source> getEnabledSources() {
// TODO filter by enabled source
return sourceManager.getSources();
}
}

View File

@@ -0,0 +1,120 @@
package eu.kanade.tachiyomi.ui.catalogue;
import android.content.Context;
import android.view.View;
import android.view.ViewGroup;
import android.widget.ArrayAdapter;
import android.widget.TextView;
import org.jsoup.nodes.Document;
import java.util.List;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.data.source.model.MangasPage;
public class CatalogueSpinnerAdapter extends ArrayAdapter<Source> {
public CatalogueSpinnerAdapter(Context context, int resource, List<Source> sources) {
super(context, resource, sources);
sources.add(new SimpleSource());
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
View v = super.getView(position, convertView, parent);
if (position == getCount()) {
((TextView)v.findViewById(android.R.id.text1)).setText("");
((TextView)v.findViewById(android.R.id.text1)).setHint(getItem(getCount()).getName());
}
return v;
}
@Override
public int getCount() {
return super.getCount()-1; // you dont display last item. It is used as hint.
}
public int getEmptyIndex() {
return getCount();
}
private class SimpleSource extends Source {
@Override
public String getName() {
return getContext().getString(R.string.select_source);
}
@Override
public int getId() {
return -1;
}
@Override
public String getBaseUrl() {
return null;
}
@Override
public boolean isLoginRequired() {
return false;
}
@Override
protected String getInitialPopularMangasUrl() {
return null;
}
@Override
protected String getInitialSearchUrl(String query) {
return null;
}
@Override
protected List<Manga> parsePopularMangasFromHtml(Document parsedHtml) {
return null;
}
@Override
protected String parseNextPopularMangasUrl(Document parsedHtml, MangasPage page) {
return null;
}
@Override
protected List<Manga> parseSearchFromHtml(Document parsedHtml) {
return null;
}
@Override
protected String parseNextSearchUrl(Document parsedHtml, MangasPage page, String query) {
return null;
}
@Override
protected Manga parseHtmlToManga(String mangaUrl, String unparsedHtml) {
return null;
}
@Override
protected List<Chapter> parseHtmlToChapters(String unparsedHtml) {
return null;
}
@Override
protected List<String> parseHtmlToPageUrls(String unparsedHtml) {
return null;
}
@Override
protected String parseHtmlToImageUrl(String unparsedHtml) {
return null;
}
}
}

View File

@@ -0,0 +1,78 @@
package eu.kanade.tachiyomi.ui.decoration;
import android.content.Context;
import android.content.res.TypedArray;
import android.util.AttributeSet;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.LinearLayoutManager;
import android.view.View;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
import android.graphics.Canvas;
public class DividerItemDecoration extends RecyclerView.ItemDecoration {
private Drawable mDivider;
public DividerItemDecoration(Context context, AttributeSet attrs) {
final TypedArray a = context.obtainStyledAttributes(attrs, new int [] { android.R.attr.listDivider });
mDivider = a.getDrawable(0);
a.recycle();
}
public DividerItemDecoration(Drawable divider) { mDivider = divider; }
@Override
public void getItemOffsets (Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
if (mDivider == null) return;
if (parent.getChildPosition(view) < 1) return;
if (getOrientation(parent) == LinearLayoutManager.VERTICAL) outRect.top = mDivider.getIntrinsicHeight();
else outRect.left = mDivider.getIntrinsicWidth();
}
@Override
public void onDrawOver(Canvas c, RecyclerView parent, RecyclerView.State state) {
if (mDivider == null) { super.onDrawOver(c, parent, state); return; }
if (getOrientation(parent) == LinearLayoutManager.VERTICAL) {
final int left = parent.getPaddingLeft();
final int right = parent.getWidth() - parent.getPaddingRight();
final int childCount = parent.getChildCount();
final int dividerHeight = mDivider.getIntrinsicHeight();
for (int i=1; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int ty = (int)(child.getTranslationY() + 0.5f);
final int top = child.getTop() - params.topMargin + ty;
final int bottom = top + dividerHeight;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
} else { //horizontal
final int top = parent.getPaddingTop();
final int bottom = parent.getHeight() - parent.getPaddingBottom();
final int childCount = parent.getChildCount();
for (int i=1; i < childCount; i++) {
final View child = parent.getChildAt(i);
final RecyclerView.LayoutParams params = (RecyclerView.LayoutParams) child.getLayoutParams();
final int size = mDivider.getIntrinsicWidth();
final int left = child.getLeft() - params.leftMargin;
final int right = left + size;
mDivider.setBounds(left, top, right, bottom);
mDivider.draw(c);
}
}
}
private int getOrientation(RecyclerView parent) {
if (parent.getLayoutManager() instanceof LinearLayoutManager) {
LinearLayoutManager layoutManager = (LinearLayoutManager) parent.getLayoutManager();
return layoutManager.getOrientation();
} else throw new IllegalStateException("DividerItemDecoration can only be used with a LinearLayoutManager.");
}
}

View File

@@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.ui.download;
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.download.model.Download;
public class DownloadAdapter extends FlexibleAdapter<DownloadHolder, Download> {
private Context context;
public DownloadAdapter(Context context) {
this.context = context;
mItems = new ArrayList<>();
setHasStableIds(true);
}
@Override
public DownloadHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(context).inflate(R.layout.item_download, parent, false);
return new DownloadHolder(v);
}
@Override
public void onBindViewHolder(DownloadHolder holder, int position) {
final Download download = getItem(position);
holder.onSetValues(download);
}
@Override
public long getItemId(int position) {
return getItem(position).chapter.id;
}
public void setItems(List<Download> downloads) {
mItems = downloads;
notifyDataSetChanged();
}
@Override
public void updateDataSet(String param) {}
}

View File

@@ -0,0 +1,136 @@
package eu.kanade.tachiyomi.ui.download;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.download.DownloadService;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
import nucleus.factory.RequiresPresenter;
import rx.Subscription;
@RequiresPresenter(DownloadPresenter.class)
public class DownloadFragment extends BaseRxFragment<DownloadPresenter> {
@Bind(R.id.download_list) RecyclerView recyclerView;
private DownloadAdapter adapter;
private MenuItem startButton;
private MenuItem stopButton;
private Subscription queueStatusSubscription;
private boolean isRunning;
public static DownloadFragment newInstance() {
return new DownloadFragment();
}
@Override
public void onCreate(Bundle bundle) {
super.onCreate(bundle);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_download_queue, container, false);
ButterKnife.bind(this, view);
setToolbarTitle(R.string.label_download_queue);
recyclerView.setLayoutManager(new LinearLayoutManager(getActivity()));
recyclerView.setHasFixedSize(true);
createAdapter();
return view;
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.download_queue, menu);
startButton = menu.findItem(R.id.start_queue);
stopButton = menu.findItem(R.id.stop_queue);
// Menu seems to be inflated after onResume in fragments, so we initialize them here
startButton.setVisible(!isRunning && !getPresenter().downloadManager.getQueue().isEmpty());
stopButton.setVisible(isRunning);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.start_queue:
DownloadService.start(getActivity());
break;
case R.id.stop_queue:
DownloadService.stop(getActivity());
break;
}
return super.onOptionsItemSelected(item);
}
@Override
public void onResume() {
super.onResume();
queueStatusSubscription = getPresenter().downloadManager.getRunningSubject()
.subscribe(this::onRunningChange);
}
@Override
public void onPause() {
queueStatusSubscription.unsubscribe();
super.onPause();
}
private void onRunningChange(boolean running) {
isRunning = running;
if (startButton != null)
startButton.setVisible(!running && !getPresenter().downloadManager.getQueue().isEmpty());
if (stopButton != null)
stopButton.setVisible(running);
}
private void createAdapter() {
adapter = new DownloadAdapter(getActivity());
recyclerView.setAdapter(adapter);
}
public void onNextDownloads(List<Download> downloads) {
adapter.setItems(downloads);
}
public void updateProgress(Download download) {
DownloadHolder holder = getHolder(download);
if (holder != null) {
holder.setDownloadProgress(download);
}
}
public void updateDownloadedPages(Download download) {
DownloadHolder holder = getHolder(download);
if (holder != null) {
holder.setDownloadedPages(download);
}
}
@Nullable
private DownloadHolder getHolder(Download download) {
return (DownloadHolder) recyclerView.findViewHolderForItemId(download.chapter.id);
}
}

View File

@@ -0,0 +1,49 @@
package eu.kanade.tachiyomi.ui.download;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ProgressBar;
import android.widget.TextView;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.download.model.Download;
public class DownloadHolder extends RecyclerView.ViewHolder {
@Bind(R.id.download_title) TextView downloadTitle;
@Bind(R.id.download_progress) ProgressBar downloadProgress;
@Bind(R.id.download_progress_text) TextView downloadProgressText;
public DownloadHolder(View view) {
super(view);
ButterKnife.bind(this, view);
}
public void onSetValues(Download download) {
downloadTitle.setText(download.chapter.name);
if (download.pages == null) {
downloadProgress.setProgress(0);
downloadProgress.setMax(1);
downloadProgressText.setText("");
} else {
downloadProgress.setMax(download.pages.size() * 100);
setDownloadProgress(download);
setDownloadedPages(download);
}
}
public void setDownloadedPages(Download download) {
String progressText = download.downloadedImages + "/" + download.pages.size();
downloadProgressText.setText(progressText);
}
public void setDownloadProgress(Download download) {
if (downloadProgress.getMax() == 1)
downloadProgress.setMax(download.pages.size() * 100);
downloadProgress.setProgress(download.totalProgress);
}
}

View File

@@ -0,0 +1,124 @@
package eu.kanade.tachiyomi.ui.download;
import android.os.Bundle;
import java.util.HashMap;
import java.util.concurrent.TimeUnit;
import javax.inject.Inject;
import eu.kanade.tachiyomi.data.download.DownloadManager;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.data.download.model.DownloadQueue;
import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import timber.log.Timber;
public class DownloadPresenter extends BasePresenter<DownloadFragment> {
@Inject DownloadManager downloadManager;
private DownloadQueue downloadQueue;
private Subscription statusSubscription;
private Subscription pageProgressSubscription;
private HashMap<Download, Subscription> progressSubscriptions;
public final static int GET_DOWNLOAD_QUEUE = 1;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
downloadQueue = downloadManager.getQueue();
progressSubscriptions = new HashMap<>();
restartableLatestCache(GET_DOWNLOAD_QUEUE,
() -> Observable.just(downloadQueue),
DownloadFragment::onNextDownloads,
(view, error) -> Timber.e(error.getMessage()));
if (savedState == null)
start(GET_DOWNLOAD_QUEUE);
}
@Override
protected void onTakeView(DownloadFragment view) {
super.onTakeView(view);
add(statusSubscription = downloadQueue.getStatusObservable()
.startWith(downloadQueue.getActiveDownloads())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(download -> {
processStatus(download, view);
}));
add(pageProgressSubscription = downloadQueue.getProgressObservable()
.observeOn(AndroidSchedulers.mainThread())
.subscribe(view::updateDownloadedPages));
}
@Override
protected void onDropView() {
destroySubscriptions();
super.onDropView();
}
private void processStatus(Download download, DownloadFragment view) {
switch (download.getStatus()) {
case Download.DOWNLOADING:
observeProgress(download, view);
// Initial update of the downloaded pages
view.updateDownloadedPages(download);
break;
case Download.DOWNLOADED:
unsubscribeProgress(download);
view.updateProgress(download);
view.updateDownloadedPages(download);
break;
case Download.ERROR:
unsubscribeProgress(download);
break;
}
}
private void observeProgress(Download download, DownloadFragment view) {
Subscription subscription = Observable.interval(50, TimeUnit.MILLISECONDS, Schedulers.newThread())
.flatMap(tick -> Observable.from(download.pages)
.map(Page::getProgress)
.reduce((x, y) -> x + y))
.observeOn(AndroidSchedulers.mainThread())
.subscribe(progress -> {
if (download.totalProgress != progress) {
download.totalProgress = progress;
view.updateProgress(download);
}
});
// Avoid leaking subscriptions
Subscription oldSubscription = progressSubscriptions.remove(download);
if (oldSubscription != null) oldSubscription.unsubscribe();
progressSubscriptions.put(download, subscription);
}
private void unsubscribeProgress(Download download) {
Subscription subscription = progressSubscriptions.remove(download);
if (subscription != null)
subscription.unsubscribe();
}
private void destroySubscriptions() {
for (Subscription subscription : progressSubscriptions.values()) {
subscription.unsubscribe();
}
progressSubscriptions.clear();
remove(pageProgressSubscription);
remove(statusSubscription);
}
}

View File

@@ -0,0 +1,45 @@
package eu.kanade.tachiyomi.ui.library;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import java.util.List;
import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.ui.base.adapter.SmartFragmentStatePagerAdapter;
public class LibraryAdapter extends SmartFragmentStatePagerAdapter {
protected List<Category> categories;
public LibraryAdapter(FragmentManager fm) {
super(fm);
}
@Override
public Fragment getItem(int position) {
return LibraryCategoryFragment.newInstance(position);
}
@Override
public int getCount() {
return categories == null ? 0 : categories.size();
}
@Override
public CharSequence getPageTitle(int position) {
return categories.get(position).name;
}
public void setCategories(List<Category> categories) {
this.categories = categories;
notifyDataSetChanged();
}
public void setSelectionMode(int mode) {
for (Fragment fragment : getRegisteredFragments()) {
((LibraryCategoryFragment) fragment).setMode(mode);
}
}
}

View File

@@ -0,0 +1,109 @@
package eu.kanade.tachiyomi.ui.library;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Filter;
import android.widget.Filterable;
import java.util.ArrayList;
import java.util.List;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Manga;
import rx.Observable;
public class LibraryCategoryAdapter extends FlexibleAdapter<LibraryHolder, Manga>
implements Filterable {
List<Manga> mangas;
Filter filter;
private LibraryCategoryFragment fragment;
public LibraryCategoryAdapter(LibraryCategoryFragment fragment) {
this.fragment = fragment;
mItems = new ArrayList<>();
filter = new LibraryFilter();
setHasStableIds(true);
}
public void setItems(List<Manga> list) {
mItems = list;
notifyDataSetChanged();
// TODO needed for filtering?
mangas = list;
}
public void clear() {
mItems.clear();
}
@Override
public long getItemId(int position) {
return mItems.get(position).id;
}
@Override
public void updateDataSet(String param) {
}
@Override
public LibraryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(fragment.getActivity()).inflate(R.layout.item_catalogue, parent, false);
return new LibraryHolder(v, this, fragment);
}
@Override
public void onBindViewHolder(LibraryHolder holder, int position) {
final LibraryPresenter presenter = ((LibraryFragment) fragment.getParentFragment()).getPresenter();
final Manga manga = getItem(position);
holder.onSetValues(manga, presenter);
//When user scrolls this bind the correct selection status
holder.itemView.setActivated(isSelected(position));
}
public int getCoverHeight() {
return fragment.recycler.getItemWidth() / 9 * 12;
}
@Override
public Filter getFilter() {
return filter;
}
private class LibraryFilter extends Filter {
@Override
protected FilterResults performFiltering(CharSequence charSequence) {
FilterResults results = new FilterResults();
String query = charSequence.toString().toLowerCase();
if (query.length() == 0) {
results.values = mangas;
results.count = mangas.size();
} else {
List<Manga> filteredMangas = Observable.from(mangas)
.filter(x ->
(x.title != null && x.title.toLowerCase().contains(query)) ||
(x.author != null && x.author.toLowerCase().contains(query)) ||
(x.artist != null && x.artist.toLowerCase().contains(query)))
.toList()
.toBlocking()
.single();
results.values = filteredMangas;
results.count = filteredMangas.size();
}
return results;
}
@Override
public void publishResults(CharSequence constraint, FilterResults results) {
setItems((List<Manga>) results.values);
}
}
}

View File

@@ -0,0 +1,181 @@
package eu.kanade.tachiyomi.ui.library;
import android.content.Intent;
import android.content.res.Configuration;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.f2prateek.rx.preferences.Preference;
import java.util.ArrayList;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.event.LibraryMangasEvent;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import eu.kanade.tachiyomi.ui.base.fragment.BaseFragment;
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
import eu.kanade.tachiyomi.util.EventBusHook;
import eu.kanade.tachiyomi.widget.AutofitRecyclerView;
import icepick.State;
import rx.Subscription;
public class LibraryCategoryFragment extends BaseFragment
implements FlexibleViewHolder.OnListItemClickListener {
@Bind(R.id.library_mangas) AutofitRecyclerView recycler;
@State int position;
private LibraryCategoryAdapter adapter;
private Subscription numColumnsSubscription;
public static LibraryCategoryFragment newInstance(int position) {
LibraryCategoryFragment fragment = new LibraryCategoryFragment();
fragment.position = position;
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_library_category, container, false);
ButterKnife.bind(this, view);
adapter = new LibraryCategoryAdapter(this);
recycler.setHasFixedSize(true);
recycler.setAdapter(adapter);
if (getLibraryFragment().getActionMode() != null) {
setMode(FlexibleAdapter.MODE_MULTI);
}
Preference<Integer> columnsPref = getResources().getConfiguration()
.orientation == Configuration.ORIENTATION_PORTRAIT ?
getLibraryPresenter().preferences.portraitColumns() :
getLibraryPresenter().preferences.landscapeColumns();
numColumnsSubscription = columnsPref.asObservable()
.doOnNext(recycler::setSpanCount)
.skip(1)
// Set again the adapter to recalculate the covers height
.subscribe(count -> recycler.setAdapter(adapter));
if (savedState != null) {
adapter.onRestoreInstanceState(savedState);
if (adapter.getMode() == FlexibleAdapter.MODE_SINGLE) {
adapter.clearSelection();
}
}
return view;
}
@Override
public void onDestroyView() {
numColumnsSubscription.unsubscribe();
super.onDestroyView();
}
@Override
public void onResume() {
super.onResume();
registerForStickyEvents();
}
@Override
public void onPause() {
unregisterForEvents();
super.onPause();
}
@Override
public void onSaveInstanceState(Bundle outState) {
adapter.onSaveInstanceState(outState);
super.onSaveInstanceState(outState);
}
@EventBusHook
public void onEventMainThread(LibraryMangasEvent event) {
List<Category> categories = getLibraryFragment().getAdapter().categories;
// When a category is deleted, the index can be greater than the number of categories
if (position >= categories.size())
return;
Category category = categories.get(position);
List<Manga> mangas = event.getMangasForCategory(category);
if (mangas == null) {
mangas = new ArrayList<>();
}
setMangas(mangas);
}
protected void openManga(Manga manga) {
getLibraryPresenter().onOpenManga(manga);
Intent intent = MangaActivity.newIntent(getActivity(), manga);
startActivity(intent);
}
public void setMangas(List<Manga> mangas) {
if (mangas != null) {
adapter.setItems(mangas);
} else {
adapter.clear();
}
}
@Override
public boolean onListItemClick(int position) {
if (getLibraryFragment().getActionMode() != null && position != -1) {
toggleSelection(position);
return true;
} else {
openManga(adapter.getItem(position));
return false;
}
}
@Override
public void onListItemLongClick(int position) {
getLibraryFragment().createActionModeIfNeeded();
toggleSelection(position);
}
private void toggleSelection(int position) {
LibraryFragment f = getLibraryFragment();
adapter.toggleSelection(position, false);
f.getPresenter().setSelection(adapter.getItem(position), adapter.isSelected(position));
int count = f.getPresenter().selectedMangas.size();
if (count == 0) {
f.destroyActionModeIfNeeded();
} else {
f.setContextTitle(count);
f.invalidateActionMode();
}
}
public void setMode(int mode) {
adapter.setMode(mode);
if (mode == FlexibleAdapter.MODE_SINGLE) {
adapter.clearSelection();
}
}
private LibraryFragment getLibraryFragment() {
return (LibraryFragment) getParentFragment();
}
private LibraryPresenter getLibraryPresenter() {
return getLibraryFragment().getPresenter();
}
}

View File

@@ -0,0 +1,233 @@
package eu.kanade.tachiyomi.ui.library;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.design.widget.AppBarLayout;
import android.support.design.widget.TabLayout;
import android.support.v4.view.ViewPager;
import android.support.v7.view.ActionMode;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuInflater;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import com.afollestad.materialdialogs.MaterialDialog;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import butterknife.Bind;
import butterknife.ButterKnife;
import de.greenrobot.event.EventBus;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.sync.LibraryUpdateService;
import eu.kanade.tachiyomi.event.LibraryMangasEvent;
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
import eu.kanade.tachiyomi.ui.library.category.CategoryActivity;
import eu.kanade.tachiyomi.ui.main.MainActivity;
import icepick.State;
import nucleus.factory.RequiresPresenter;
@RequiresPresenter(LibraryPresenter.class)
public class LibraryFragment extends BaseRxFragment<LibraryPresenter>
implements ActionMode.Callback {
@Bind(R.id.view_pager) ViewPager viewPager;
private TabLayout tabs;
private AppBarLayout appBar;
protected LibraryAdapter adapter;
private ActionMode actionMode;
@State int activeCategory;
public static LibraryFragment newInstance() {
return new LibraryFragment();
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_library, container, false);
setToolbarTitle(getString(R.string.label_library));
ButterKnife.bind(this, view);
appBar = ((MainActivity) getActivity()).getAppBar();
tabs = (TabLayout) inflater.inflate(R.layout.library_tab_layout, appBar, false);
appBar.addView(tabs);
adapter = new LibraryAdapter(getChildFragmentManager());
viewPager.setAdapter(adapter);
tabs.setupWithViewPager(viewPager);
return view;
}
@Override
public void onDestroyView() {
appBar.removeView(tabs);
super.onDestroyView();
}
@Override
public void onPause() {
EventBus.getDefault().removeStickyEvent(LibraryMangasEvent.class);
super.onPause();
}
@Override
public void onSaveInstanceState(Bundle bundle) {
activeCategory = viewPager.getCurrentItem();
super.onSaveInstanceState(bundle);
}
@Override
public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) {
inflater.inflate(R.menu.library, menu);
}
@Override
public boolean onOptionsItemSelected(MenuItem item) {
switch (item.getItemId()) {
case R.id.action_refresh:
LibraryUpdateService.start(getActivity());
return true;
case R.id.action_edit_categories:
onEditCategories();
return true;
}
return super.onOptionsItemSelected(item);
}
private void onEditCategories() {
Intent intent = CategoryActivity.newIntent(getActivity());
startActivity(intent);
}
public void onNextLibraryUpdate(List<Category> categories, Map<Integer, List<Manga>> mangas) {
boolean hasMangasInDefaultCategory = mangas.get(0) != null;
int activeCat = adapter.categories != null ? viewPager.getCurrentItem() : activeCategory;
if (hasMangasInDefaultCategory) {
setCategoriesWithDefault(categories);
} else {
setCategories(categories);
}
// Restore active category
viewPager.setCurrentItem(activeCat, false);
if (tabs.getTabCount() > 0) {
TabLayout.Tab tab = tabs.getTabAt(viewPager.getCurrentItem());
if (tab != null) tab.select();
}
// Send the mangas to child fragments after the adapter is updated
EventBus.getDefault().postSticky(new LibraryMangasEvent(mangas));
}
private void setCategoriesWithDefault(List<Category> categories) {
List<Category> categoriesWithDefault = new ArrayList<>();
categoriesWithDefault.add(Category.createDefault());
categoriesWithDefault.addAll(categories);
setCategories(categoriesWithDefault);
}
private void setCategories(List<Category> categories) {
adapter.setCategories(categories);
tabs.setTabsFromPagerAdapter(adapter);
tabs.setVisibility(categories.size() <= 1 ? View.GONE : View.VISIBLE);
}
public void setContextTitle(int count) {
actionMode.setTitle(getString(R.string.label_selected, count));
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.library_selection, menu);
adapter.setSelectionMode(FlexibleAdapter.MODE_MULTI);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.action_move_to_category:
moveMangasToCategories(getPresenter().selectedMangas);
return true;
case R.id.action_delete:
getPresenter().deleteMangas();
destroyActionModeIfNeeded();
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
adapter.setSelectionMode(FlexibleAdapter.MODE_SINGLE);
getPresenter().selectedMangas.clear();
actionMode = null;
}
public void destroyActionModeIfNeeded() {
if (actionMode != null) {
actionMode.finish();
}
}
private void moveMangasToCategories(List<Manga> mangas) {
new MaterialDialog.Builder(getActivity())
.title(R.string.action_move_category)
.items(getPresenter().getCategoriesNames())
.itemsCallbackMultiChoice(null, (dialog, which, text) -> {
getPresenter().moveMangasToCategories(which, mangas);
destroyActionModeIfNeeded();
return true;
})
.positiveText(R.string.button_ok)
.negativeText(R.string.button_cancel)
.show();
}
@Nullable
public ActionMode getActionMode() {
return actionMode;
}
public LibraryAdapter getAdapter() {
return adapter;
}
public void createActionModeIfNeeded() {
if (actionMode == null) {
actionMode = getBaseActivity().startSupportActionMode(this);
}
}
public void invalidateActionMode() {
actionMode.invalidate();
}
}

View File

@@ -0,0 +1,51 @@
package eu.kanade.tachiyomi.ui.library;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.cache.CoverCache;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import static android.view.ViewGroup.LayoutParams.MATCH_PARENT;
import static android.widget.RelativeLayout.LayoutParams;
public class LibraryHolder extends FlexibleViewHolder {
@Bind(R.id.thumbnail) ImageView thumbnail;
@Bind(R.id.title) TextView title;
@Bind(R.id.unreadText) TextView unreadText;
public LibraryHolder(View view, LibraryCategoryAdapter adapter, OnListItemClickListener listener) {
super(view, adapter, listener);
ButterKnife.bind(this, view);
thumbnail.setLayoutParams(new LayoutParams(MATCH_PARENT, adapter.getCoverHeight()));
}
public void onSetValues(Manga manga, LibraryPresenter presenter) {
title.setText(manga.title);
if (manga.unread > 0) {
unreadText.setVisibility(View.VISIBLE);
unreadText.setText(Integer.toString(manga.unread));
} else {
unreadText.setVisibility(View.GONE);
}
loadCover(manga, presenter.sourceManager.get(manga.source), presenter.coverCache);
}
private void loadCover(Manga manga, Source source, CoverCache coverCache) {
if (manga.thumbnail_url != null) {
coverCache.saveAndLoadFromCache(thumbnail, manga.thumbnail_url, source.getGlideHeaders());
} else {
thumbnail.setImageResource(android.R.color.transparent);
}
}
}

View File

@@ -0,0 +1,138 @@
package eu.kanade.tachiyomi.ui.library;
import android.os.Bundle;
import android.util.Pair;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import javax.inject.Inject;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.data.cache.CoverCache;
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.database.models.MangaCategory;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.event.LibraryMangasEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
public class LibraryPresenter extends BasePresenter<LibraryFragment> {
@Inject DatabaseHelper db;
@Inject PreferencesHelper preferences;
@Inject CoverCache coverCache;
@Inject SourceManager sourceManager;
protected List<Category> categories;
protected List<Manga> selectedMangas;
private static final int GET_LIBRARY = 1;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
selectedMangas = new ArrayList<>();
restartableLatestCache(GET_LIBRARY,
this::getLibraryObservable,
(view, pair) -> view.onNextLibraryUpdate(pair.first, pair.second));
if (savedState == null) {
start(GET_LIBRARY);
}
}
@Override
protected void onDestroy() {
EventBus.getDefault().removeStickyEvent(LibraryMangasEvent.class);
super.onDestroy();
}
@Override
protected void onTakeView(LibraryFragment libraryFragment) {
super.onTakeView(libraryFragment);
if (!isSubscribed(GET_LIBRARY)) {
start(GET_LIBRARY);
}
}
private Observable<Pair<List<Category>, Map<Integer, List<Manga>>>> getLibraryObservable() {
return Observable.combineLatest(getCategoriesObservable(), getLibraryMangasObservable(),
Pair::create)
.observeOn(AndroidSchedulers.mainThread());
}
private Observable<List<Category>> getCategoriesObservable() {
return db.getCategories().createObservable()
.doOnNext(categories -> this.categories = categories);
}
private Observable<Map<Integer, List<Manga>>> getLibraryMangasObservable() {
return db.getLibraryMangas().createObservable()
.flatMap(mangas -> Observable.from(mangas)
.groupBy(manga -> manga.category)
.flatMap(group -> group.toList()
.map(list -> Pair.create(group.getKey(), list)))
.toMap(pair -> pair.first, pair -> pair.second));
}
public void onOpenManga(Manga manga) {
// Avoid further db updates for the library when it's not needed
stop(GET_LIBRARY);
}
public void setSelection(Manga manga, boolean selected) {
if (selected) {
selectedMangas.add(manga);
} else {
selectedMangas.remove(manga);
}
}
public String[] getCategoriesNames() {
int count = categories.size();
String[] names = new String[count];
for (int i = 0; i < count; i++) {
names[i] = categories.get(i).name;
}
return names;
}
public void deleteMangas() {
for (Manga manga : selectedMangas) {
manga.favorite = false;
}
db.insertMangas(selectedMangas).executeAsBlocking();
}
public void moveMangasToCategories(Integer[] positions, List<Manga> mangas) {
List<Category> categoriesToAdd = new ArrayList<>();
for (Integer index : positions) {
categoriesToAdd.add(categories.get(index));
}
moveMangasToCategories(categoriesToAdd, mangas);
}
public void moveMangasToCategories(List<Category> categories, List<Manga> mangas) {
List<MangaCategory> mc = new ArrayList<>();
for (Manga manga : mangas) {
for (Category cat : categories) {
mc.add(MangaCategory.create(manga, cat));
}
}
db.setMangaCategories(mc, mangas);
}
}

View File

@@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.ui.library.category;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.FloatingActionButton;
import android.support.v4.content.res.ResourcesCompat;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.support.v7.widget.helper.ItemTouchHelper;
import android.view.Menu;
import android.view.MenuItem;
import com.afollestad.materialdialogs.MaterialDialog;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import eu.kanade.tachiyomi.ui.base.adapter.OnStartDragListener;
import eu.kanade.tachiyomi.ui.decoration.DividerItemDecoration;
import eu.kanade.tachiyomi.ui.library.LibraryCategoryAdapter;
import nucleus.factory.RequiresPresenter;
import rx.Observable;
@RequiresPresenter(CategoryPresenter.class)
public class CategoryActivity extends BaseRxActivity<CategoryPresenter> implements
ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener, OnStartDragListener {
@Bind(R.id.toolbar) Toolbar toolbar;
@Bind(R.id.categories_list) RecyclerView recycler;
@Bind(R.id.fab) FloatingActionButton fab;
private CategoryAdapter adapter;
private ActionMode actionMode;
private ItemTouchHelper touchHelper;
public static Intent newIntent(Context context) {
return new Intent(context, CategoryActivity.class);
}
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
setContentView(R.layout.activity_edit_categories);
ButterKnife.bind(this);
setupToolbar(toolbar);
adapter = new CategoryAdapter(this);
recycler.setLayoutManager(new LinearLayoutManager(this));
recycler.setHasFixedSize(true);
recycler.setAdapter(adapter);
recycler.addItemDecoration(new DividerItemDecoration(
ResourcesCompat.getDrawable(getResources(), R.drawable.line_divider, null)));
// Touch helper to drag and reorder categories
touchHelper = new ItemTouchHelper(new CategoryItemTouchHelper(adapter));
touchHelper.attachToRecyclerView(recycler);
fab.setOnClickListener(v -> {
new MaterialDialog.Builder(this)
.title(R.string.action_add_category)
.input(R.string.name, 0, false, (dialog, input) -> {
getPresenter().createCategory(input.toString());
})
.show();
});
}
public void setCategories(List<Category> categories) {
destroyActionModeIfNeeded();
adapter.setItems(categories);
}
private List<Category> getSelectedCategories() {
// Create a blocking copy of the selected categories
return Observable.from(adapter.getSelectedItems())
.map(adapter::getItem).toList().toBlocking().single();
}
@Override
public boolean onListItemClick(int position) {
if (actionMode != null && position != -1) {
toggleSelection(position);
return true;
} else {
return false;
}
}
@Override
public void onListItemLongClick(int position) {
if (actionMode == null)
actionMode = startSupportActionMode(this);
toggleSelection(position);
}
private void toggleSelection(int position) {
adapter.toggleSelection(position, false);
int count = adapter.getSelectedItemCount();
if (count == 0) {
actionMode.finish();
} else {
setContextTitle(count);
actionMode.invalidate();
MenuItem editItem = actionMode.getMenu().findItem(R.id.action_edit);
editItem.setVisible(count == 1);
}
}
private void setContextTitle(int count) {
actionMode.setTitle(getString(R.string.label_selected, count));
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.category_selection, menu);
adapter.setMode(LibraryCategoryAdapter.MODE_MULTI);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.action_delete:
deleteCategories(getSelectedCategories());
return true;
case R.id.action_edit:
editCategory(getSelectedCategories().get(0));
return true;
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
adapter.setMode(LibraryCategoryAdapter.MODE_SINGLE);
adapter.clearSelection();
actionMode = null;
}
public void destroyActionModeIfNeeded() {
if (actionMode != null) {
actionMode.finish();
}
}
private void deleteCategories(List<Category> categories) {
getPresenter().deleteCategories(categories);
}
private void editCategory(Category category) {
new MaterialDialog.Builder(this)
.title(R.string.action_rename_category)
.input(getString(R.string.name), category.name, false, (dialog, input) -> {
getPresenter().renameCategory(category, input.toString());
})
.show();
}
@Override
public void onStartDrag(RecyclerView.ViewHolder viewHolder) {
touchHelper.startDrag(viewHolder);
}
}

View File

@@ -0,0 +1,80 @@
package eu.kanade.tachiyomi.ui.library.category;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.amulyakhare.textdrawable.util.ColorGenerator;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.ui.base.adapter.ItemTouchHelperAdapter;
public class CategoryAdapter extends FlexibleAdapter<CategoryHolder, Category> implements
ItemTouchHelperAdapter {
private final CategoryActivity activity;
private final ColorGenerator generator;
public CategoryAdapter(CategoryActivity activity) {
this.activity = activity;
generator = ColorGenerator.DEFAULT;
setHasStableIds(true);
}
public void setItems(List<Category> items) {
mItems = new ArrayList<>(items);
notifyDataSetChanged();
}
@Override
public long getItemId(int position) {
return mItems.get(position).id;
}
@Override
public void updateDataSet(String param) {
}
@Override
public CategoryHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = activity.getLayoutInflater();
View v = inflater.inflate(R.layout.item_edit_categories, parent, false);
return new CategoryHolder(v, this, activity, activity);
}
@Override
public void onBindViewHolder(CategoryHolder holder, int position) {
final Category category = getItem(position);
holder.onSetValues(category, generator);
//When user scrolls this bind the correct selection status
holder.itemView.setActivated(isSelected(position));
}
@Override
public void onItemMove(int fromPosition, int toPosition) {
if (fromPosition < toPosition) {
for (int i = fromPosition; i < toPosition; i++) {
Collections.swap(mItems, i, i + 1);
}
} else {
for (int i = fromPosition; i > toPosition; i--) {
Collections.swap(mItems, i, i - 1);
}
}
activity.getPresenter().reorderCategories(mItems);
}
@Override
public void onItemDismiss(int position) {
}
}

View File

@@ -0,0 +1,58 @@
package eu.kanade.tachiyomi.ui.library.category;
import android.support.v4.view.MotionEventCompat;
import android.view.MotionEvent;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.amulyakhare.textdrawable.TextDrawable;
import com.amulyakhare.textdrawable.util.ColorGenerator;
import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import eu.kanade.tachiyomi.ui.base.adapter.OnStartDragListener;
public class CategoryHolder extends FlexibleViewHolder {
private View view;
@Bind(R.id.image) ImageView image;
@Bind(R.id.title) TextView title;
@Bind(R.id.reorder) ImageView reorder;
public CategoryHolder(View view, CategoryAdapter adapter,
OnListItemClickListener listener, OnStartDragListener dragListener) {
super(view, adapter, listener);
ButterKnife.bind(this, view);
this.view = view;
reorder.setOnTouchListener((v, event) -> {
if (MotionEventCompat.getActionMasked(event) == MotionEvent.ACTION_DOWN) {
dragListener.onStartDrag(this);
return true;
}
return false;
});
}
public void onSetValues(Category category, ColorGenerator generator) {
title.setText(category.name);
image.setImageDrawable(getRound(category.name.substring(0, 1), generator));
}
private TextDrawable getRound(String text, ColorGenerator generator) {
return TextDrawable.builder().buildRound(text, generator.getColor(text));
}
@OnClick(R.id.image)
void onImageClick() {
// Simulate long click on this view to enter selection mode
onLongClick(view);
}
}

View File

@@ -0,0 +1,16 @@
package eu.kanade.tachiyomi.ui.library.category;
import eu.kanade.tachiyomi.ui.base.adapter.ItemTouchHelperAdapter;
import eu.kanade.tachiyomi.ui.base.adapter.SimpleItemTouchHelperCallback;
public class CategoryItemTouchHelper extends SimpleItemTouchHelperCallback {
public CategoryItemTouchHelper(ItemTouchHelperAdapter adapter) {
super(adapter);
}
@Override
public boolean isItemViewSwipeEnabled() {
return false;
}
}

View File

@@ -0,0 +1,68 @@
package eu.kanade.tachiyomi.ui.library.category;
import android.os.Bundle;
import java.util.List;
import javax.inject.Inject;
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.Category;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import rx.android.schedulers.AndroidSchedulers;
public class CategoryPresenter extends BasePresenter<CategoryActivity> {
@Inject DatabaseHelper db;
private List<Category> categories;
private static final int GET_CATEGORIES = 1;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
restartableLatestCache(GET_CATEGORIES,
() -> db.getCategories().createObservable()
.doOnNext(categories -> this.categories = categories)
.observeOn(AndroidSchedulers.mainThread()),
CategoryActivity::setCategories);
start(GET_CATEGORIES);
}
public void createCategory(String name) {
Category cat = Category.create(name);
// Set the new item in the last position
int max = 0;
if (categories != null) {
for (Category cat2 : categories) {
if (cat2.order > max) {
max = cat2.order + 1;
}
}
}
cat.order = max;
db.insertCategory(cat).createObservable().subscribe();
}
public void deleteCategories(List<Category> categories) {
db.deleteCategories(categories).createObservable().subscribe();
}
public void reorderCategories(List<Category> categories) {
for (int i = 0; i < categories.size(); i++) {
categories.get(i).order = i;
}
db.insertCategories(categories).createObservable().subscribe();
}
public void renameCategory(Category category, String name) {
category.name = name;
db.insertCategory(category).createObservable().subscribe();
}
}

View File

@@ -0,0 +1,179 @@
package eu.kanade.tachiyomi.ui.main;
import android.app.Activity;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import java.util.ArrayList;
import java.util.List;
import eu.kanade.tachiyomi.R;
/**
* Why this class is needed.
*
* FragmentManager does not supply a developer with a fragment stack.
* It gives us a fragment *transaction* stack.
*
* To be sane, we need *fragment* stack.
*
* This implementation also handles NucleusSupportFragment presenter`s lifecycle correctly.
*/
public class FragmentStack {
public interface OnBackPressedHandlingFragment {
boolean onBackPressed();
}
public interface OnFragmentRemovedListener {
void onFragmentRemoved(Fragment fragment);
}
private Activity activity;
private FragmentManager manager;
private int containerId;
@Nullable private OnFragmentRemovedListener onFragmentRemovedListener;
public FragmentStack(Activity activity, FragmentManager manager, int containerId, @Nullable OnFragmentRemovedListener onFragmentRemovedListener) {
this.activity = activity;
this.manager = manager;
this.containerId = containerId;
this.onFragmentRemovedListener = onFragmentRemovedListener;
}
/**
* Returns the number of fragments in the stack.
*
* @return the number of fragments in the stack.
*/
public int size() {
return getFragments().size();
}
/**
* Pushes a fragment to the top of the stack.
*/
public void push(Fragment fragment) {
Fragment top = peek();
if (top != null) {
manager.beginTransaction()
.setCustomAnimations(R.anim.enter_from_right, R.anim.exit_to_left, R.anim.enter_from_left, R.anim.exit_to_right)
.remove(top)
.add(containerId, fragment, indexToTag(manager.getBackStackEntryCount() + 1))
.addToBackStack(null)
.commit();
}
else {
manager.beginTransaction()
.add(containerId, fragment, indexToTag(0))
.commit();
}
manager.executePendingTransactions();
}
/**
* Pops the top item if the stack.
* If the fragment implements {@link OnBackPressedHandlingFragment}, calls {@link OnBackPressedHandlingFragment#onBackPressed()} instead.
* If {@link OnBackPressedHandlingFragment#onBackPressed()} returns false the fragment gets popped.
*
* @return true if a fragment has been popped or if {@link OnBackPressedHandlingFragment#onBackPressed()} returned true;
*/
public boolean back() {
Fragment top = peek();
if (top instanceof OnBackPressedHandlingFragment) {
if (((OnBackPressedHandlingFragment)top).onBackPressed())
return true;
}
return pop();
}
/**
* Pops the topmost fragment from the stack.
* The lowest fragment can't be popped, it can only be replaced.
*
* @return false if the stack can't pop or true if a top fragment has been popped.
*/
public boolean pop() {
if (manager.getBackStackEntryCount() == 0)
return false;
Fragment top = peek();
manager.popBackStackImmediate();
if (onFragmentRemovedListener != null)
onFragmentRemovedListener.onFragmentRemoved(top);
return true;
}
/**
* Replaces stack contents with just one fragment.
*/
public void replace(Fragment fragment) {
List<Fragment> fragments = getFragments();
manager.popBackStackImmediate(null, FragmentManager.POP_BACK_STACK_INCLUSIVE);
manager.beginTransaction()
.replace(containerId, fragment, indexToTag(0))
.commit();
manager.executePendingTransactions();
if (onFragmentRemovedListener != null) {
for (Fragment fragment1 : fragments)
onFragmentRemovedListener.onFragmentRemoved(fragment1);
}
}
/**
* Returns the topmost fragment in the stack.
*/
public Fragment peek() {
return manager.findFragmentById(containerId);
}
/**
* Returns a back fragment if the fragment is of given class.
* If such fragment does not exist and activity implements the given class then the activity will be returned.
*
* @param fragment a fragment to search from.
* @param callbackType a class of type for callback to search.
* @param <T> a type of callback.
* @return a back fragment or activity.
*/
@SuppressWarnings("unchecked")
public <T> T findCallback(Fragment fragment, Class<T> callbackType) {
Fragment back = getBackFragment(fragment);
if (back != null && callbackType.isAssignableFrom(back.getClass()))
return (T)back;
if (callbackType.isAssignableFrom(activity.getClass()))
return (T)activity;
return null;
}
private Fragment getBackFragment(Fragment fragment) {
List<Fragment> fragments = getFragments();
for (int f = fragments.size() - 1; f >= 0; f--) {
if (fragments.get(f) == fragment && f > 0)
return fragments.get(f - 1);
}
return null;
}
private List<Fragment> getFragments() {
List<Fragment> fragments = new ArrayList<>(manager.getBackStackEntryCount() + 1);
for (int i = 0; i < manager.getBackStackEntryCount() + 1; i++) {
Fragment fragment = manager.findFragmentByTag(indexToTag(i));
if (fragment != null)
fragments.add(fragment);
}
return fragments;
}
private String indexToTag(int index) {
return Integer.toString(index);
}
}

View File

@@ -0,0 +1,180 @@
package eu.kanade.tachiyomi.ui.main;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.AppBarLayout;
import android.support.v4.app.Fragment;
import android.support.v4.widget.DrawerLayout;
import android.support.v7.widget.Toolbar;
import android.widget.FrameLayout;
import com.mikepenz.materialdrawer.Drawer;
import com.mikepenz.materialdrawer.DrawerBuilder;
import com.mikepenz.materialdrawer.model.PrimaryDrawerItem;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity;
import eu.kanade.tachiyomi.ui.catalogue.CatalogueFragment;
import eu.kanade.tachiyomi.ui.download.DownloadFragment;
import eu.kanade.tachiyomi.ui.library.LibraryFragment;
import eu.kanade.tachiyomi.ui.setting.SettingsActivity;
import icepick.State;
import nucleus.view.ViewWithPresenter;
public class MainActivity extends BaseActivity {
@Bind(R.id.appbar) AppBarLayout appBar;
@Bind(R.id.toolbar) Toolbar toolbar;
@Bind(R.id.drawer_container) FrameLayout container;
private Drawer drawer;
private FragmentStack fragmentStack;
@State int selectedItem;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
// Do not let the launcher create a new activity
if ((getIntent().getFlags() & Intent.FLAG_ACTIVITY_BROUGHT_TO_FRONT) != 0) {
finish();
return;
}
setContentView(R.layout.activity_main);
ButterKnife.bind(this);
setupToolbar(toolbar);
fragmentStack = new FragmentStack(this, getSupportFragmentManager(), R.id.content_layout,
fragment -> {
if (fragment instanceof ViewWithPresenter)
((ViewWithPresenter)fragment).getPresenter().destroy();
});
drawer = new DrawerBuilder()
.withActivity(this)
.withRootView(container)
.withToolbar(toolbar)
.withActionBarDrawerToggleAnimated(true)
.withOnDrawerNavigationListener(view -> {
if (fragmentStack.size() > 1) {
onBackPressed();
return true;
}
return false;
})
.addDrawerItems(
new PrimaryDrawerItem()
.withName(R.string.label_library)
.withIdentifier(R.id.nav_drawer_library),
// new PrimaryDrawerItem()
// .withName(R.string.recent_updates_title)
// .withIdentifier(R.id.nav_drawer_recent_updates),
new PrimaryDrawerItem()
.withName(R.string.label_catalogues)
.withIdentifier(R.id.nav_drawer_catalogues),
new PrimaryDrawerItem()
.withName(R.string.label_download_queue)
.withIdentifier(R.id.nav_drawer_downloads),
new PrimaryDrawerItem()
.withName(R.string.label_settings)
.withIdentifier(R.id.nav_drawer_settings)
.withSelectable(false)
)
.withSavedInstance(savedState)
.withOnDrawerItemClickListener(
(view, position, drawerItem) -> {
if (drawerItem != null) {
int identifier = drawerItem.getIdentifier();
switch (identifier) {
case R.id.nav_drawer_library:
setFragment(LibraryFragment.newInstance());
break;
case R.id.nav_drawer_recent_updates:
break;
case R.id.nav_drawer_catalogues:
setFragment(CatalogueFragment.newInstance());
break;
case R.id.nav_drawer_downloads:
setFragment(DownloadFragment.newInstance());
break;
case R.id.nav_drawer_settings:
startActivity(new Intent(this, SettingsActivity.class));
break;
}
}
return false;
}
)
.build();
if (savedState != null) {
// Recover icon state after rotation
if (fragmentStack.size() > 1) {
showBackArrow();
}
// Set saved selection
drawer.setSelection(selectedItem, false);
} else {
// Set default selection
drawer.setSelection(R.id.nav_drawer_library);
}
}
@Override
protected void onSaveInstanceState(Bundle outState) {
selectedItem = drawer.getCurrentSelection();
super.onSaveInstanceState(outState);
}
public void setFragment(Fragment fragment) {
fragmentStack.replace(fragment);
}
public void pushFragment(Fragment fragment) {
fragmentStack.push(fragment);
if (fragmentStack.size() > 1) {
showBackArrow();
}
}
@Override
public void onBackPressed() {
if (!fragmentStack.pop()) {
super.onBackPressed();
} else if (fragmentStack.size() == 1) {
showHamburgerIcon();
drawer.getActionBarDrawerToggle().syncState();
}
}
private void showHamburgerIcon() {
if (getSupportActionBar() != null) {
getSupportActionBar().setDisplayHomeAsUpEnabled(false);
drawer.getActionBarDrawerToggle().setDrawerIndicatorEnabled(true);
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_UNLOCKED);
}
}
private void showBackArrow() {
if (getSupportActionBar() != null) {
drawer.getActionBarDrawerToggle().setDrawerIndicatorEnabled(false);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
drawer.getDrawerLayout().setDrawerLockMode(DrawerLayout.LOCK_MODE_LOCKED_CLOSED);
}
}
public Toolbar getToolbar() {
return toolbar;
}
public AppBarLayout getAppBar() {
return appBar;
}
}

View File

@@ -0,0 +1,137 @@
package eu.kanade.tachiyomi.ui.manga;
import android.content.Context;
import android.content.Intent;
import android.os.Bundle;
import android.support.design.widget.TabLayout;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.FragmentPagerAdapter;
import android.support.v4.view.ViewPager;
import android.support.v7.widget.Toolbar;
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.Manga;
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity;
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment;
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment;
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListFragment;
import nucleus.factory.RequiresPresenter;
@RequiresPresenter(MangaPresenter.class)
public class MangaActivity extends BaseRxActivity<MangaPresenter> {
@Bind(R.id.toolbar) Toolbar toolbar;
@Bind(R.id.tabs) TabLayout tabs;
@Bind(R.id.view_pager) ViewPager view_pager;
@Inject PreferencesHelper preferences;
@Inject MangaSyncManager mangaSyncManager;
private MangaDetailAdapter adapter;
private long manga_id;
private boolean is_online;
public final static String MANGA_ID = "manga_id";
public final static String MANGA_ONLINE = "manga_online";
public static Intent newIntent(Context context, Manga manga) {
Intent intent = new Intent(context, MangaActivity.class);
intent.putExtra(MANGA_ID, manga.id);
return intent;
}
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
App.get(this).getComponent().inject(this);
setContentView(R.layout.activity_manga);
ButterKnife.bind(this);
setupToolbar(toolbar);
Intent intent = getIntent();
manga_id = intent.getLongExtra(MANGA_ID, -1);
is_online = intent.getBooleanExtra(MANGA_ONLINE, false);
setupViewPager();
if (savedState == null)
getPresenter().queryManga(manga_id);
}
private void setupViewPager() {
adapter = new MangaDetailAdapter(getSupportFragmentManager(), this);
view_pager.setAdapter(adapter);
tabs.setupWithViewPager(view_pager);
if (!is_online)
view_pager.setCurrentItem(MangaDetailAdapter.CHAPTERS_FRAGMENT);
}
public void setManga(Manga manga) {
setToolbarTitle(manga.title);
}
public boolean isCatalogueManga() {
return is_online;
}
class MangaDetailAdapter extends FragmentPagerAdapter {
private int pageCount;
private String tabTitles[];
final static int INFO_FRAGMENT = 0;
final static int CHAPTERS_FRAGMENT = 1;
final static int MYANIMELIST_FRAGMENT = 2;
public MangaDetailAdapter(FragmentManager fm, Context context) {
super(fm);
tabTitles = new String[]{
context.getString(R.string.manga_detail_tab),
context.getString(R.string.manga_chapters_tab),
"MAL"
};
pageCount = 2;
if (!is_online && mangaSyncManager.getMyAnimeList().isLogged())
pageCount++;
}
@Override
public int getCount() {
return pageCount;
}
@Override
public Fragment getItem(int position) {
switch (position) {
case INFO_FRAGMENT:
return MangaInfoFragment.newInstance();
case CHAPTERS_FRAGMENT:
return ChaptersFragment.newInstance();
case MYANIMELIST_FRAGMENT:
return MyAnimeListFragment.newInstance();
default:
return null;
}
}
@Override
public CharSequence getPageTitle(int position) {
// Generate title based on item position
return tabTitles[position];
}
}
}

View File

@@ -0,0 +1,50 @@
package eu.kanade.tachiyomi.ui.manga;
import android.os.Bundle;
import javax.inject.Inject;
import de.greenrobot.event.EventBus;
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import icepick.State;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class MangaPresenter extends BasePresenter<MangaActivity> {
@Inject DatabaseHelper db;
@State long mangaId;
private static final int DB_MANGA = 1;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
restartableLatestCache(DB_MANGA, this::getDbMangaObservable, MangaActivity::setManga);
}
@Override
protected void onDestroy() {
super.onDestroy();
// Avoid new instances receiving wrong manga
EventBus.getDefault().removeStickyEvent(Manga.class);
}
private Observable<Manga> getDbMangaObservable() {
return db.getManga(mangaId).createObservable()
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(manga -> EventBus.getDefault().postSticky(manga));
}
public void queryManga(long mangaId) {
this.mangaId = mangaId;
start(DB_MANGA);
}
}

View File

@@ -0,0 +1,55 @@
package eu.kanade.tachiyomi.ui.manga.chapter;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.ArrayList;
import java.util.List;
import eu.davidea.flexibleadapter.FlexibleAdapter;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
public class ChaptersAdapter extends FlexibleAdapter<ChaptersHolder, Chapter> {
private ChaptersFragment fragment;
public ChaptersAdapter(ChaptersFragment fragment) {
this.fragment = fragment;
mItems = new ArrayList<>();
setHasStableIds(true);
}
@Override
public void updateDataSet(String param) {}
@Override
public ChaptersHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v = LayoutInflater.from(fragment.getActivity()).inflate(R.layout.item_chapter, parent, false);
return new ChaptersHolder(v, this, fragment);
}
@Override
public void onBindViewHolder(ChaptersHolder holder, int position) {
final Chapter chapter = getItem(position);
holder.onSetValues(fragment.getActivity(), chapter);
//When user scrolls this bind the correct selection status
holder.itemView.setActivated(isSelected(position));
}
@Override
public long getItemId(int position) {
return mItems.get(position).id;
}
public void setItems(List<Chapter> chapters) {
mItems = chapters;
notifyDataSetChanged();
}
public ChaptersFragment getFragment() {
return fragment;
}
}

View File

@@ -0,0 +1,350 @@
package eu.kanade.tachiyomi.ui.manga.chapter;
import android.content.Intent;
import android.os.Bundle;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.view.ActionMode;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.Menu;
import android.view.MenuItem;
import android.view.View;
import android.view.ViewGroup;
import android.widget.CheckBox;
import android.widget.ImageView;
import com.afollestad.materialdialogs.MaterialDialog;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.download.DownloadService;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
import eu.kanade.tachiyomi.ui.decoration.DividerItemDecoration;
import eu.kanade.tachiyomi.ui.manga.MangaActivity;
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
import eu.kanade.tachiyomi.util.ToastUtil;
import nucleus.factory.RequiresPresenter;
import rx.Observable;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
@RequiresPresenter(ChaptersPresenter.class)
public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implements
ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener {
@Bind(R.id.chapter_list) RecyclerView recyclerView;
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
@Bind(R.id.toolbar_bottom) ViewGroup toolbarBottom;
@Bind(R.id.action_sort) ImageView sortBtn;
@Bind(R.id.action_next_unread) ImageView nextUnreadBtn;
@Bind(R.id.action_show_unread) CheckBox readCb;
@Bind(R.id.action_show_downloaded) CheckBox downloadedCb;
private ChaptersAdapter adapter;
private LinearLayoutManager linearLayout;
private ActionMode actionMode;
private Subscription downloadProgressSubscription;
public static ChaptersFragment newInstance() {
return new ChaptersFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_manga_chapters, container, false);
ButterKnife.bind(this, view);
// Init RecyclerView and adapter
linearLayout = new LinearLayoutManager(getActivity());
recyclerView.setLayoutManager(linearLayout);
recyclerView.addItemDecoration(new DividerItemDecoration(ContextCompat.getDrawable(getContext(), R.drawable.line_divider)));
recyclerView.setHasFixedSize(true);
adapter = new ChaptersAdapter(this);
recyclerView.setAdapter(adapter);
// Set initial values
setReadFilter();
setDownloadedFilter();
setSortIcon();
// Init listeners
swipeRefresh.setOnRefreshListener(this::fetchChapters);
readCb.setOnCheckedChangeListener((arg, isChecked) ->
getPresenter().setReadFilter(isChecked));
downloadedCb.setOnCheckedChangeListener((v, isChecked) ->
getPresenter().setDownloadedFilter(isChecked));
sortBtn.setOnClickListener(v -> {
getPresenter().revertSortOrder();
setSortIcon();
});
nextUnreadBtn.setOnClickListener(v -> {
Chapter chapter = getPresenter().getNextUnreadChapter();
if (chapter != null) {
openChapter(chapter);
} else {
ToastUtil.showShort(getContext(), R.string.no_next_chapter);
}
});
return view;
}
@Override
public void onResume() {
super.onResume();
observeChapterDownloadProgress();
}
@Override
public void onPause() {
unsubscribeChapterDownloadProgress();
super.onPause();
}
public void onNextChapters(List<Chapter> chapters) {
// If the list is empty, fetch chapters from source if the conditions are met
// We use presenter chapters instead because they are always unfiltered
if (getPresenter().getChapters().isEmpty())
initialFetchChapters();
destroyActionModeIfNeeded();
adapter.setItems(chapters);
}
private void initialFetchChapters() {
// Only fetch if this view is from the catalog and it hasn't requested previously
if (isCatalogueManga() && !getPresenter().hasRequested()) {
fetchChapters();
}
}
public void fetchChapters() {
if (getPresenter().getManga() != null) {
swipeRefresh.setRefreshing(true);
getPresenter().fetchChaptersFromSource();
}
}
public void onFetchChaptersDone() {
swipeRefresh.setRefreshing(false);
}
public void onFetchChaptersError() {
swipeRefresh.setRefreshing(false);
ToastUtil.showShort(getContext(), R.string.fetch_chapters_error);
}
public boolean isCatalogueManga() {
return ((MangaActivity) getActivity()).isCatalogueManga();
}
protected void openChapter(Chapter chapter) {
getPresenter().onOpenChapter(chapter);
Intent intent = ReaderActivity.newIntent(getActivity());
startActivity(intent);
}
private void observeChapterDownloadProgress() {
downloadProgressSubscription = getPresenter().getDownloadProgressObs()
.subscribe(this::onDownloadProgressChange,
error -> { /* TODO getting a NPE sometimes on 'manga' from presenter */ });
}
private void unsubscribeChapterDownloadProgress() {
if (downloadProgressSubscription != null)
downloadProgressSubscription.unsubscribe();
}
private void onDownloadProgressChange(Download download) {
ChaptersHolder holder = getHolder(download.chapter);
if (holder != null)
holder.onProgressChange(getContext(), download.downloadedImages, download.pages.size());
}
public void onChapterStatusChange(Chapter chapter) {
ChaptersHolder holder = getHolder(chapter);
if (holder != null)
holder.onStatusChange(chapter.status);
}
@Nullable
private ChaptersHolder getHolder(Chapter chapter) {
return (ChaptersHolder) recyclerView.findViewHolderForItemId(chapter.id);
}
@Override
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
mode.getMenuInflater().inflate(R.menu.chapter_selection, menu);
adapter.setMode(ChaptersAdapter.MODE_MULTI);
return true;
}
@Override
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
return false;
}
@Override
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
switch (item.getItemId()) {
case R.id.action_select_all:
return onSelectAll();
case R.id.action_mark_as_read:
return onMarkAsRead(getSelectedChapters());
case R.id.action_mark_as_unread:
return onMarkAsUnread(getSelectedChapters());
case R.id.action_download:
return onDownload(getSelectedChapters());
case R.id.action_delete:
return onDelete(getSelectedChapters());
}
return false;
}
@Override
public void onDestroyActionMode(ActionMode mode) {
adapter.setMode(ChaptersAdapter.MODE_SINGLE);
adapter.clearSelection();
actionMode = null;
}
private Observable<Chapter> getSelectedChapters() {
// Create a blocking copy of the selected chapters.
// When the action mode is closed the list is cleared. If we use background
// threads with this observable, some emissions could be lost.
List<Chapter> chapters = Observable.from(adapter.getSelectedItems())
.map(adapter::getItem).toList().toBlocking().single();
return Observable.from(chapters);
}
public void destroyActionModeIfNeeded() {
if (actionMode != null) {
actionMode.finish();
}
}
protected boolean onSelectAll() {
adapter.selectAll();
setContextTitle(adapter.getSelectedItemCount());
return true;
}
protected boolean onMarkAsRead(Observable<Chapter> chapters) {
getPresenter().markChaptersRead(chapters, true);
return true;
}
protected boolean onMarkAsUnread(Observable<Chapter> chapters) {
getPresenter().markChaptersRead(chapters, false);
return true;
}
protected boolean onDownload(Observable<Chapter> chapters) {
DownloadService.start(getActivity());
Observable<Chapter> observable = chapters
.doOnCompleted(adapter::notifyDataSetChanged);
getPresenter().downloadChapters(observable);
destroyActionModeIfNeeded();
return true;
}
protected boolean onDelete(Observable<Chapter> chapters) {
int size = adapter.getSelectedItemCount();
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
.title(R.string.deleting)
.progress(false, size, true)
.cancelable(false)
.show();
Observable<Chapter> observable = chapters
.concatMap(chapter -> {
getPresenter().deleteChapter(chapter);
return Observable.just(chapter);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.doOnNext(chapter -> {
dialog.incrementProgress(1);
chapter.status = Download.NOT_DOWNLOADED;
})
.doOnCompleted(adapter::notifyDataSetChanged)
.finallyDo(dialog::dismiss);
getPresenter().deleteChapters(observable);
destroyActionModeIfNeeded();
return true;
}
@Override
public boolean onListItemClick(int position) {
if (actionMode != null && adapter.getMode() == ChaptersAdapter.MODE_MULTI) {
toggleSelection(position);
return true;
} else {
openChapter(adapter.getItem(position));
return false;
}
}
@Override
public void onListItemLongClick(int position) {
if (actionMode == null)
actionMode = getBaseActivity().startSupportActionMode(this);
toggleSelection(position);
}
private void toggleSelection(int position) {
adapter.toggleSelection(position, false);
int count = adapter.getSelectedItemCount();
if (count == 0) {
actionMode.finish();
} else {
setContextTitle(count);
actionMode.invalidate();
}
}
private void setContextTitle(int count) {
actionMode.setTitle(getString(R.string.label_selected, count));
}
public void setSortIcon() {
if (sortBtn != null) {
boolean aToZ = getPresenter().getSortOrder();
sortBtn.setImageResource(!aToZ ? R.drawable.ic_expand_less_white_36dp : R.drawable.ic_expand_more_white_36dp);
}
}
public void setReadFilter() {
if (readCb != null) {
readCb.setChecked(getPresenter().getReadFilter());
}
}
public void setDownloadedFilter() {
if (downloadedCb != null) {
downloadedCb.setChecked(getPresenter().getDownloadedFilter());
}
}
}

View File

@@ -0,0 +1,110 @@
package eu.kanade.tachiyomi.ui.manga.chapter;
import android.content.Context;
import android.support.v4.content.ContextCompat;
import android.view.View;
import android.widget.PopupMenu;
import android.widget.RelativeLayout;
import android.widget.TextView;
import java.text.SimpleDateFormat;
import java.util.Date;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.Chapter;
import eu.kanade.tachiyomi.data.download.model.Download;
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
import rx.Observable;
public class ChaptersHolder extends FlexibleViewHolder {
private final ChaptersAdapter adapter;
private Chapter item;
@Bind(R.id.chapter_title) TextView title;
@Bind(R.id.download_text) TextView downloadText;
@Bind(R.id.chapter_menu) RelativeLayout chapterMenu;
@Bind(R.id.chapter_pages) TextView pages;
@Bind(R.id.chapter_date) TextView date;
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
public ChaptersHolder(View view, ChaptersAdapter adapter, OnListItemClickListener listener) {
super(view, adapter, listener);
this.adapter = adapter;
ButterKnife.bind(this, view);
chapterMenu.setOnClickListener(v -> v.post(() -> showPopupMenu(v)));
}
public void onSetValues(Context context, Chapter chapter) {
this.item = chapter;
title.setText(chapter.name);
if (chapter.read) {
title.setTextColor(ContextCompat.getColor(context, R.color.hint_text));
} else {
title.setTextColor(ContextCompat.getColor(context, R.color.primary_text));
}
if (!chapter.read && chapter.last_page_read > 0) {
pages.setText(context.getString(R.string.chapter_progress, chapter.last_page_read + 1));
} else {
pages.setText("");
}
onStatusChange(chapter.status);
date.setText(sdf.format(new Date(chapter.date_upload)));
}
public void onStatusChange(int status) {
switch (status) {
case Download.QUEUE:
downloadText.setText(R.string.chapter_queued); break;
case Download.DOWNLOADING:
downloadText.setText(R.string.chapter_downloading); break;
case Download.DOWNLOADED:
downloadText.setText(R.string.chapter_downloaded); break;
case Download.ERROR:
downloadText.setText(R.string.chapter_error); break;
default:
downloadText.setText(""); break;
}
}
public void onProgressChange(Context context, int downloaded, int total) {
downloadText.setText(context.getString(
R.string.chapter_downloading_progress, downloaded, total));
}
private void showPopupMenu(View view) {
// Create a PopupMenu, giving it the clicked view for an anchor
PopupMenu popup = new PopupMenu(adapter.getFragment().getActivity(), view);
// Inflate our menu resource into the PopupMenu's Menu
popup.getMenuInflater().inflate(R.menu.chapter_single, popup.getMenu());
// Set a listener so we are notified if a menu item is clicked
popup.setOnMenuItemClickListener(menuItem -> {
Observable<Chapter> chapter = Observable.just(item);
switch (menuItem.getItemId()) {
case R.id.action_mark_as_read:
return adapter.getFragment().onMarkAsRead(chapter);
case R.id.action_mark_as_unread:
return adapter.getFragment().onMarkAsUnread(chapter);
case R.id.action_download:
return adapter.getFragment().onDownload(chapter);
case R.id.action_delete:
return adapter.getFragment().onDelete(chapter);
}
return false;
});
// Finally show the PopupMenu
popup.show();
}
}

View File

@@ -0,0 +1,275 @@
package eu.kanade.tachiyomi.ui.manga.chapter;
import android.os.Bundle;
import android.util.Pair;
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.download.DownloadManager;
import eu.kanade.tachiyomi.data.download.model.Download;
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.event.ChapterCountEvent;
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
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 ChaptersPresenter extends BasePresenter<ChaptersFragment> {
@Inject DatabaseHelper db;
@Inject SourceManager sourceManager;
@Inject PreferencesHelper preferences;
@Inject DownloadManager downloadManager;
private Manga manga;
private Source source;
private List<Chapter> chapters;
private boolean sortOrderAToZ = true;
private boolean onlyUnread = true;
private boolean onlyDownloaded;
@State boolean hasRequested;
private PublishSubject<List<Chapter>> chaptersSubject;
private static final int DB_CHAPTERS = 1;
private static final int FETCH_CHAPTERS = 2;
private static final int CHAPTER_STATUS_CHANGES = 3;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
if (savedState != null) {
onProcessRestart();
}
chaptersSubject = PublishSubject.create();
restartableLatestCache(DB_CHAPTERS,
this::getDbChaptersObs,
ChaptersFragment::onNextChapters);
restartableFirst(FETCH_CHAPTERS,
this::getOnlineChaptersObs,
(view, result) -> view.onFetchChaptersDone(),
(view, error) -> view.onFetchChaptersError());
restartableLatestCache(CHAPTER_STATUS_CHANGES,
this::getChapterStatusObs,
(view, download) -> view.onChapterStatusChange(download.chapter),
(view, error) -> Timber.e(error.getCause(), error.getMessage()));
registerForStickyEvents();
}
private void onProcessRestart() {
stop(DB_CHAPTERS);
stop(FETCH_CHAPTERS);
stop(CHAPTER_STATUS_CHANGES);
}
@Override
protected void onDestroy() {
unregisterForEvents();
EventBus.getDefault().removeStickyEvent(ChapterCountEvent.class);
super.onDestroy();
}
@EventBusHook
public void onEventMainThread(Manga manga) {
this.manga = manga;
if (!isSubscribed(DB_CHAPTERS)) {
source = sourceManager.get(manga.source);
start(DB_CHAPTERS);
add(db.getChapters(manga).createObservable()
.subscribeOn(Schedulers.io())
.doOnNext(chapters -> {
this.chapters = chapters;
EventBus.getDefault().postSticky(new ChapterCountEvent(chapters.size()));
for (Chapter chapter : chapters) {
setChapterStatus(chapter);
}
start(CHAPTER_STATUS_CHANGES);
})
.subscribe(chaptersSubject::onNext));
}
}
public void fetchChaptersFromSource() {
hasRequested = true;
start(FETCH_CHAPTERS);
}
private void refreshChapters() {
chaptersSubject.onNext(chapters);
}
private Observable<Pair<Integer, Integer>> getOnlineChaptersObs() {
return source.pullChaptersFromNetwork(manga.url)
.subscribeOn(Schedulers.io())
.flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters))
.observeOn(AndroidSchedulers.mainThread());
}
private Observable<List<Chapter>> getDbChaptersObs() {
return chaptersSubject.flatMap(this::applyChapterFilters)
.observeOn(AndroidSchedulers.mainThread());
}
private Observable<List<Chapter>> applyChapterFilters(List<Chapter> chapters) {
Observable<Chapter> observable = Observable.from(chapters)
.subscribeOn(Schedulers.io());
if (onlyUnread) {
observable = observable.filter(chapter -> !chapter.read);
}
if (onlyDownloaded) {
observable = observable.filter(chapter -> chapter.status == Download.DOWNLOADED);
}
return observable.toSortedList((chapter, chapter2) -> sortOrderAToZ ?
Float.compare(chapter2.chapter_number, chapter.chapter_number) :
Float.compare(chapter.chapter_number, chapter2.chapter_number));
}
private void setChapterStatus(Chapter chapter) {
for (Download download : downloadManager.getQueue()) {
if (chapter.id.equals(download.chapter.id)) {
chapter.status = download.getStatus();
return;
}
}
if (downloadManager.isChapterDownloaded(source, manga, chapter)) {
chapter.status = Download.DOWNLOADED;
} else {
chapter.status = Download.NOT_DOWNLOADED;
}
}
private Observable<Download> getChapterStatusObs() {
return downloadManager.getQueue().getStatusObservable()
.observeOn(AndroidSchedulers.mainThread())
.filter(download -> download.manga.id.equals(manga.id))
.doOnNext(this::updateChapterStatus);
}
public void updateChapterStatus(Download download) {
for (Chapter chapter : chapters) {
if (download.chapter.id.equals(chapter.id)) {
chapter.status = download.getStatus();
break;
}
}
if (onlyDownloaded && download.getStatus() == Download.DOWNLOADED)
refreshChapters();
}
public Observable<Download> getDownloadProgressObs() {
return downloadManager.getQueue().getProgressObservable()
.filter(download -> download.manga.id.equals(manga.id))
.observeOn(AndroidSchedulers.mainThread());
}
public void onOpenChapter(Chapter chapter) {
EventBus.getDefault().postSticky(new ReaderEvent(source, manga, chapter));
}
public Chapter getNextUnreadChapter() {
return db.getNextUnreadChapter(manga).executeAsBlocking();
}
public void markChaptersRead(Observable<Chapter> selectedChapters, boolean read) {
add(selectedChapters
.subscribeOn(Schedulers.io())
.map(chapter -> {
chapter.read = read;
if (!read) chapter.last_page_read = 0;
return chapter;
})
.toList()
.flatMap(chapters -> db.insertChapters(chapters).createObservable())
.observeOn(AndroidSchedulers.mainThread())
.subscribe());
}
public void downloadChapters(Observable<Chapter> selectedChapters) {
add(selectedChapters
.toList()
.subscribe(chapters -> {
EventBus.getDefault().postSticky(new DownloadChaptersEvent(manga, chapters));
}));
}
public void deleteChapters(Observable<Chapter> selectedChapters) {
add(selectedChapters
.subscribe(chapter -> {
downloadManager.getQueue().remove(chapter);
}, error -> {
Timber.e(error.getMessage());
}, () -> {
if (onlyDownloaded)
refreshChapters();
}));
}
public void deleteChapter(Chapter chapter) {
downloadManager.deleteChapter(source, manga, chapter);
}
public void revertSortOrder() {
//TODO manga.chapter_order
sortOrderAToZ = !sortOrderAToZ;
refreshChapters();
}
public void setReadFilter(boolean onlyUnread) {
//TODO do we need save filter for manga?
this.onlyUnread = onlyUnread;
refreshChapters();
}
public void setDownloadedFilter(boolean onlyDownloaded) {
this.onlyDownloaded = onlyDownloaded;
refreshChapters();
}
public boolean getSortOrder() {
return sortOrderAToZ;
}
public boolean getReadFilter() {
return onlyUnread;
}
public boolean getDownloadedFilter() {
return onlyDownloaded;
}
public Manga getManga() {
return manga;
}
public List<Chapter> getChapters() {
return chapters;
}
public boolean hasRequested() {
return hasRequested;
}
}

View File

@@ -0,0 +1,116 @@
package eu.kanade.tachiyomi.ui.manga.info;
import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.bumptech.glide.load.model.LazyHeaders;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.cache.CoverCache;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
import nucleus.factory.RequiresPresenter;
@RequiresPresenter(MangaInfoPresenter.class)
public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
@Bind(R.id.manga_artist) TextView artist;
@Bind(R.id.manga_author) TextView author;
@Bind(R.id.manga_chapters) TextView chapterCount;
@Bind(R.id.manga_genres) TextView genres;
@Bind(R.id.manga_status) TextView status;
@Bind(R.id.manga_summary) TextView description;
@Bind(R.id.manga_cover) ImageView cover;
@Bind(R.id.action_favorite) Button favoriteBtn;
public static MangaInfoFragment newInstance() {
return new MangaInfoFragment();
}
@Override
public void onCreate(Bundle savedState) {
super.onCreate(savedState);
setHasOptionsMenu(true);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
// Inflate the layout for this fragment
View view = inflater.inflate(R.layout.fragment_manga_info, container, false);
ButterKnife.bind(this, view);
favoriteBtn.setOnClickListener(v -> {
getPresenter().toggleFavorite();
});
swipeRefresh.setOnRefreshListener(this::fetchMangaFromSource);
return view;
}
public void onNextManga(Manga manga) {
if (manga.initialized) {
setMangaInfo(manga);
} else {
// Initialize manga
fetchMangaFromSource();
}
}
private void setMangaInfo(Manga manga) {
artist.setText(manga.artist);
author.setText(manga.author);
genres.setText(manga.genre);
status.setText(manga.getStatus(getActivity()));
description.setText(manga.description);
setFavoriteText(manga.favorite);
CoverCache coverCache = getPresenter().coverCache;
LazyHeaders headers = getPresenter().source.getGlideHeaders();
if (manga.thumbnail_url != null && cover.getDrawable() == null) {
if (manga.favorite) {
coverCache.saveAndLoadFromCache(cover, manga.thumbnail_url, headers);
} else {
coverCache.loadFromNetwork(cover, manga.thumbnail_url, headers);
}
}
}
public void setChapterCount(int count) {
chapterCount.setText(String.valueOf(count));
}
public void setFavoriteText(boolean isFavorite) {
favoriteBtn.setText(!isFavorite ? R.string.add_to_library : R.string.remove_from_library);
}
private void fetchMangaFromSource() {
setRefreshing(true);
getPresenter().fetchMangaFromSource();
}
public void onFetchMangaDone() {
setRefreshing(false);
}
public void onFetchMangaError() {
setRefreshing(false);
}
private void setRefreshing(boolean value) {
swipeRefresh.setRefreshing(value);
}
}

View File

@@ -0,0 +1,119 @@
package eu.kanade.tachiyomi.ui.manga.info;
import android.os.Bundle;
import javax.inject.Inject;
import eu.kanade.tachiyomi.data.cache.CoverCache;
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.event.ChapterCountEvent;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import eu.kanade.tachiyomi.util.EventBusHook;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
@Inject DatabaseHelper db;
@Inject SourceManager sourceManager;
@Inject CoverCache coverCache;
private Manga manga;
protected Source source;
private int count = -1;
private boolean isFetching;
private static final int GET_MANGA = 1;
private static final int GET_CHAPTER_COUNT = 2;
private static final int FETCH_MANGA_INFO = 3;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
if (savedState != null) {
onProcessRestart();
}
restartableLatestCache(GET_MANGA,
() -> Observable.just(manga),
MangaInfoFragment::onNextManga);
restartableLatestCache(GET_CHAPTER_COUNT,
() -> Observable.just(count),
MangaInfoFragment::setChapterCount);
restartableFirst(FETCH_MANGA_INFO,
this::fetchMangaObs,
(view, manga) -> view.onFetchMangaDone(),
(view, error) -> view.onFetchMangaError());
registerForStickyEvents();
}
private void onProcessRestart() {
stop(GET_MANGA);
stop(GET_CHAPTER_COUNT);
stop(FETCH_MANGA_INFO);
}
@Override
protected void onDestroy() {
unregisterForEvents();
super.onDestroy();
}
@EventBusHook
public void onEventMainThread(Manga manga) {
this.manga = manga;
source = sourceManager.get(manga.source);
start(GET_MANGA);
}
@EventBusHook
public void onEventMainThread(ChapterCountEvent event) {
if (count != event.getCount()) {
count = event.getCount();
start(GET_CHAPTER_COUNT);
}
}
public void fetchMangaFromSource() {
if (!isFetching) {
isFetching = true;
start(FETCH_MANGA_INFO);
}
}
private Observable<Manga> fetchMangaObs() {
return source.pullMangaFromNetwork(manga.url)
.flatMap(networkManga -> {
manga.copyFrom(networkManga);
db.insertManga(manga).executeAsBlocking();
return Observable.just(manga);
})
.finallyDo(() -> isFetching = false)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread());
}
public void toggleFavorite() {
manga.favorite = !manga.favorite;
onMangaFavoriteChange(manga.favorite);
db.insertManga(manga).executeAsBlocking();
}
private void onMangaFavoriteChange(boolean isFavorite) {
if (isFavorite) {
coverCache.save(manga.thumbnail_url, source.getGlideHeaders());
} else {
coverCache.delete(manga.thumbnail_url);
}
}
}

View File

@@ -0,0 +1,172 @@
package eu.kanade.tachiyomi.ui.manga.myanimelist;
import android.app.Dialog;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.DialogFragment;
import android.text.Editable;
import android.text.TextUtils;
import android.text.TextWatcher;
import android.view.View;
import android.widget.EditText;
import android.widget.ListView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.afollestad.materialdialogs.MaterialDialog;
import java.util.List;
import java.util.concurrent.TimeUnit;
import butterknife.Bind;
import butterknife.ButterKnife;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.MangaSync;
import rx.Subscription;
import rx.android.schedulers.AndroidSchedulers;
import rx.subjects.PublishSubject;
import uk.co.ribot.easyadapter.EasyAdapter;
import uk.co.ribot.easyadapter.ItemViewHolder;
import uk.co.ribot.easyadapter.PositionInfo;
import uk.co.ribot.easyadapter.annotations.LayoutId;
import uk.co.ribot.easyadapter.annotations.ViewId;
public class MyAnimeListDialogFragment extends DialogFragment {
@Bind(R.id.myanimelist_search_field) EditText searchText;
@Bind(R.id.myanimelist_search_results) ListView searchResults;
@Bind(R.id.progress) ProgressBar progressBar;
private EasyAdapter<MangaSync> adapter;
private MangaSync selectedItem;
private Subscription searchSubscription;
public static MyAnimeListDialogFragment newInstance() {
return new MyAnimeListDialogFragment();
}
@NonNull
@Override
public Dialog onCreateDialog(Bundle savedState) {
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
.customView(R.layout.dialog_myanimelist_search, false)
.positiveText(R.string.button_ok)
.negativeText(R.string.button_cancel)
.onPositive((dialog1, which) -> onPositiveButtonClick())
.build();
ButterKnife.bind(this, dialog.getView());
// Create adapter
adapter = new EasyAdapter<>(getActivity(), ResultViewHolder.class);
searchResults.setAdapter(adapter);
// Set listeners
searchResults.setOnItemClickListener((parent, viewList, position, id) ->
selectedItem = adapter.getItem(position));
// Do an initial search based on the manga's title
if (savedState == null) {
String title = getPresenter().manga.title;
searchText.append(title);
search(title);
}
return dialog;
}
@Override
public void onResume() {
super.onResume();
PublishSubject<String> querySubject = PublishSubject.create();
searchText.addTextChangedListener(new SimpleTextChangeListener() {
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
querySubject.onNext(s.toString());
}
});
// Listen to text changes
searchSubscription = querySubject.debounce(1, TimeUnit.SECONDS)
.observeOn(AndroidSchedulers.mainThread())
.subscribe(this::search);
}
@Override
public void onPause() {
if (searchSubscription != null) {
searchSubscription.unsubscribe();
}
super.onPause();
}
private void onPositiveButtonClick() {
if (adapter != null && selectedItem != null) {
getPresenter().registerManga(selectedItem);
}
}
private void search(String query) {
if (!TextUtils.isEmpty(query)) {
searchResults.setVisibility(View.GONE);
progressBar.setVisibility(View.VISIBLE);
getPresenter().searchManga(query);
}
}
public void onSearchResults(List<MangaSync> results) {
selectedItem = null;
progressBar.setVisibility(View.GONE);
searchResults.setVisibility(View.VISIBLE);
adapter.setItems(results);
}
public void onSearchResultsError() {
progressBar.setVisibility(View.GONE);
searchResults.setVisibility(View.VISIBLE);
adapter.getItems().clear();
}
public MyAnimeListFragment getMALFragment() {
return (MyAnimeListFragment) getParentFragment();
}
public MyAnimeListPresenter getPresenter() {
return getMALFragment().getPresenter();
}
@LayoutId(R.layout.dialog_myanimelist_search_item)
public static class ResultViewHolder extends ItemViewHolder<MangaSync> {
@ViewId(R.id.myanimelist_result_title) TextView title;
public ResultViewHolder(View view) {
super(view);
}
@Override
public void onSetValues(MangaSync chapter, PositionInfo positionInfo) {
title.setText(chapter.title);
}
}
private static class SimpleTextChangeListener implements TextWatcher {
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
}
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
}
@Override
public void afterTextChanged(Editable s) {
}
}
}

View File

@@ -0,0 +1,181 @@
package eu.kanade.tachiyomi.ui.manga.myanimelist;
import android.content.Context;
import android.os.Bundle;
import android.support.v4.widget.SwipeRefreshLayout;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.NumberPicker;
import android.widget.TextView;
import com.afollestad.materialdialogs.MaterialDialog;
import java.text.DecimalFormat;
import java.util.List;
import butterknife.Bind;
import butterknife.ButterKnife;
import butterknife.OnClick;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.models.MangaSync;
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
import nucleus.factory.RequiresPresenter;
@RequiresPresenter(MyAnimeListPresenter.class)
public class MyAnimeListFragment extends BaseRxFragment<MyAnimeListPresenter> {
@Bind(R.id.myanimelist_title) TextView title;
@Bind(R.id.myanimelist_chapters) TextView chapters;
@Bind(R.id.myanimelist_score) TextView score;
@Bind(R.id.myanimelist_status) TextView status;
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
private MyAnimeListDialogFragment dialog;
private DecimalFormat decimalFormat = new DecimalFormat("#.##");
private final static String SEARCH_FRAGMENT_TAG = "mal_search";
public static MyAnimeListFragment newInstance() {
return new MyAnimeListFragment();
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = inflater.inflate(R.layout.fragment_myanimelist, container, false);
ButterKnife.bind(this, view);
swipeRefresh.setEnabled(false);
swipeRefresh.setOnRefreshListener(() -> getPresenter().refresh());
return view;
}
public void setMangaSync(MangaSync mangaSync) {
swipeRefresh.setEnabled(mangaSync != null);
if (mangaSync != null) {
title.setText(mangaSync.title);
chapters.setText(mangaSync.last_chapter_read + "/" +
(mangaSync.total_chapters > 0 ? mangaSync.total_chapters : "-"));
score.setText(mangaSync.score == 0 ? "-" : decimalFormat.format(mangaSync.score));
status.setText(getPresenter().myAnimeList.getStatus(mangaSync.status));
}
}
public void onRefreshDone() {
swipeRefresh.setRefreshing(false);
}
public void onRefreshError() {
swipeRefresh.setRefreshing(false);
}
public void setSearchResults(List<MangaSync> results) {
findSearchFragmentIfNeeded();
if (dialog != null) {
dialog.onSearchResults(results);
}
}
public void setSearchResultsError() {
findSearchFragmentIfNeeded();
if (dialog != null) {
dialog.onSearchResultsError();
}
}
private void findSearchFragmentIfNeeded() {
if (dialog == null) {
dialog = (MyAnimeListDialogFragment) getChildFragmentManager()
.findFragmentByTag(SEARCH_FRAGMENT_TAG);
}
}
@OnClick(R.id.myanimelist_title_layout)
void onTitleClick() {
if (dialog == null)
dialog = MyAnimeListDialogFragment.newInstance();
getPresenter().restartSearch();
dialog.show(getChildFragmentManager(), SEARCH_FRAGMENT_TAG);
}
@OnClick(R.id.myanimelist_status_layout)
void onStatusClick() {
if (getPresenter().mangaSync == null)
return;
Context ctx = getActivity();
new MaterialDialog.Builder(ctx)
.title(R.string.status)
.items(getPresenter().getAllStatus(ctx))
.itemsCallbackSingleChoice(getPresenter().getIndexFromStatus(),
(materialDialog, view, i, charSequence) -> {
getPresenter().setStatus(i);
status.setText("...");
return true;
})
.show();
}
@OnClick(R.id.myanimelist_chapters_layout)
void onChaptersClick() {
if (getPresenter().mangaSync == null)
return;
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
.title(R.string.chapters)
.customView(R.layout.dialog_myanimelist_chapters, false)
.positiveText(R.string.button_ok)
.negativeText(R.string.button_cancel)
.onPositive((materialDialog, dialogAction) -> {
View view = materialDialog.getCustomView();
if (view != null) {
NumberPicker np = (NumberPicker) view.findViewById(R.id.chapters_picker);
getPresenter().setLastChapterRead(np.getValue());
chapters.setText("...");
}
})
.show();
View view = dialog.getCustomView();
if (view != null) {
NumberPicker np = (NumberPicker) view.findViewById(R.id.chapters_picker);
// Set initial value
np.setValue(getPresenter().mangaSync.last_chapter_read);
// Don't allow to go from 0 to 9999
np.setWrapSelectorWheel(false);
}
}
@OnClick(R.id.myanimelist_score_layout)
void onScoreClick() {
if (getPresenter().mangaSync == null)
return;
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
.title(R.string.score)
.customView(R.layout.dialog_myanimelist_score, false)
.positiveText(R.string.button_ok)
.negativeText(R.string.button_cancel)
.onPositive((materialDialog, dialogAction) -> {
View view = materialDialog.getCustomView();
if (view != null) {
NumberPicker np = (NumberPicker) view.findViewById(R.id.score_picker);
getPresenter().setScore(np.getValue());
score.setText("...");
}
})
.show();
View view = dialog.getCustomView();
if (view != null) {
NumberPicker np = (NumberPicker) view.findViewById(R.id.score_picker);
// Set initial value
np.setValue((int) getPresenter().mangaSync.score);
}
}
}

View File

@@ -0,0 +1,186 @@
package eu.kanade.tachiyomi.ui.manga.myanimelist;
import android.content.Context;
import android.os.Bundle;
import android.text.TextUtils;
import javax.inject.Inject;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.database.models.Manga;
import eu.kanade.tachiyomi.data.database.models.MangaSync;
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
import eu.kanade.tachiyomi.util.EventBusHook;
import eu.kanade.tachiyomi.util.ToastUtil;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import timber.log.Timber;
public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
@Inject DatabaseHelper db;
@Inject MangaSyncManager syncManager;
protected MyAnimeList myAnimeList;
protected Manga manga;
protected MangaSync mangaSync;
private String query;
private static final int GET_MANGA_SYNC = 1;
private static final int GET_SEARCH_RESULTS = 2;
private static final int REFRESH = 3;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
if (savedState != null) {
onProcessRestart();
}
myAnimeList = syncManager.getMyAnimeList();
restartableLatestCache(GET_MANGA_SYNC,
() -> db.getMangaSync(manga, myAnimeList).createObservable()
.doOnNext(mangaSync -> this.mangaSync = mangaSync)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()),
MyAnimeListFragment::setMangaSync);
restartableLatestCache(GET_SEARCH_RESULTS,
() -> myAnimeList.search(query)
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()),
(view, results) -> {
view.setSearchResults(results);
}, (view, error) -> {
Timber.e(error.getMessage());
view.setSearchResultsError();
});
restartableFirst(REFRESH,
() -> myAnimeList.getList()
.flatMap(myList -> {
for (MangaSync myManga : myList) {
if (myManga.remote_id == mangaSync.remote_id) {
mangaSync.copyPersonalFrom(myManga);
mangaSync.total_chapters = myManga.total_chapters;
return Observable.just(mangaSync);
}
}
return Observable.error(new Exception("Could not find manga"));
})
.flatMap(myManga -> db.insertMangaSync(myManga).createObservable())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread()),
(view, result) -> view.onRefreshDone(),
(view, error) -> view.onRefreshError());
}
private void onProcessRestart() {
stop(GET_MANGA_SYNC);
stop(GET_SEARCH_RESULTS);
stop(REFRESH);
}
@Override
protected void onTakeView(MyAnimeListFragment view) {
super.onTakeView(view);
registerForStickyEvents();
}
@Override
protected void onDropView() {
unregisterForEvents();
super.onDropView();
}
@EventBusHook
public void onEventMainThread(Manga manga) {
this.manga = manga;
start(GET_MANGA_SYNC);
}
private void updateRemote() {
add(myAnimeList.update(mangaSync)
.flatMap(response -> db.insertMangaSync(mangaSync).createObservable())
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(next -> {},
error -> {
Timber.e(error.getMessage());
// Restart on error to set old values
start(GET_MANGA_SYNC);
}
));
}
public void searchManga(String query) {
if (TextUtils.isEmpty(query) || query.equals(this.query))
return;
this.query = query;
start(GET_SEARCH_RESULTS);
}
public void restartSearch() {
this.query = null;
stop(GET_SEARCH_RESULTS);
}
public void registerManga(MangaSync manga) {
manga.manga_id = this.manga.id;
add(myAnimeList.bind(manga)
.flatMap(response -> {
if (response.isSuccessful()) {
return db.insertMangaSync(manga).createObservable();
}
return Observable.error(new Exception("Could not bind manga"));
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(manga2 -> {},
error -> ToastUtil.showShort(getContext(), error.getMessage())));
}
public String[] getAllStatus(Context context) {
return new String[] {
context.getString(R.string.reading),
context.getString(R.string.completed),
context.getString(R.string.on_hold),
context.getString(R.string.dropped),
context.getString(R.string.plan_to_read)
};
}
public int getIndexFromStatus() {
return mangaSync.status == 6 ? 4 : mangaSync.status - 1;
}
public void setStatus(int index) {
mangaSync.status = index == 4 ? 6 : index + 1;
updateRemote();
}
public void setScore(int score) {
mangaSync.score = score;
updateRemote();
}
public void setLastChapterRead(int chapterNumber) {
mangaSync.last_chapter_read = chapterNumber;
updateRemote();
}
public void refresh() {
if (mangaSync != null) {
start(REFRESH);
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,6 @@
package eu.kanade.tachiyomi.ui.reader.viewer.base;
public interface OnChapterBoundariesOutListener {
void onFirstPageOutEvent();
void onLastPageOutEvent();
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,57 @@
package eu.kanade.tachiyomi.ui.setting;
import android.os.Bundle;
import android.preference.Preference;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
import eu.kanade.tachiyomi.BuildConfig;
import eu.kanade.tachiyomi.R;
public class SettingsAboutFragment extends SettingsNestedFragment {
public static SettingsNestedFragment newInstance(int resourcePreference, int resourceTitle) {
SettingsNestedFragment fragment = new SettingsAboutFragment();
fragment.setArgs(resourcePreference, resourceTitle);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
Preference version = findPreference(getString(R.string.pref_version));
Preference buildTime = findPreference(getString(R.string.pref_build_time));
version.setSummary(BuildConfig.DEBUG ? "r" + BuildConfig.COMMIT_COUNT :
BuildConfig.VERSION_NAME);
buildTime.setSummary(getFormattedBuildTime());
return super.onCreateView(inflater, container, savedState);
}
private String getFormattedBuildTime() {
try {
DateFormat inputDf = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm'Z'");
inputDf.setTimeZone(TimeZone.getTimeZone("UTC"));
Date date = inputDf.parse(BuildConfig.BUILD_TIME);
DateFormat outputDf = DateFormat.getDateTimeInstance(
DateFormat.MEDIUM, DateFormat.SHORT, Locale.getDefault());
outputDf.setTimeZone(TimeZone.getDefault());
return outputDf.format(date);
} catch (ParseException e) {
// Do nothing
}
return "";
}
}

View File

@@ -0,0 +1,82 @@
package eu.kanade.tachiyomi.ui.setting;
import android.os.Bundle;
import android.preference.PreferenceCategory;
import android.preference.PreferenceScreen;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import java.util.List;
import javax.inject.Inject;
import eu.kanade.tachiyomi.App;
import eu.kanade.tachiyomi.data.mangasync.base.MangaSyncService;
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
import eu.kanade.tachiyomi.data.source.SourceManager;
import eu.kanade.tachiyomi.data.source.base.Source;
import eu.kanade.tachiyomi.widget.preference.MangaSyncLoginDialog;
import eu.kanade.tachiyomi.widget.preference.SourceLoginDialog;
import rx.Observable;
public class SettingsAccountsFragment extends SettingsNestedFragment {
@Inject SourceManager sourceManager;
@Inject MangaSyncManager syncManager;
public static SettingsNestedFragment newInstance(int resourcePreference, int resourceTitle) {
SettingsNestedFragment fragment = new SettingsAccountsFragment();
fragment.setArgs(resourcePreference, resourceTitle);
return fragment;
}
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
App.get(getActivity()).getComponent().inject(this);
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedSate) {
View view = super.onCreateView(inflater, container, savedSate);
PreferenceScreen screen = getPreferenceScreen();
List<Source> sourceAccounts = getSourcesWithLogin();
PreferenceCategory sourceCategory = new PreferenceCategory(screen.getContext());
sourceCategory.setTitle("Sources");
screen.addPreference(sourceCategory);
for (Source source : sourceAccounts) {
SourceLoginDialog dialog = new SourceLoginDialog(
screen.getContext(), preferences, source);
dialog.setTitle(source.getName());
sourceCategory.addPreference(dialog);
}
PreferenceCategory mangaSyncCategory = new PreferenceCategory(screen.getContext());
mangaSyncCategory.setTitle("Sync");
screen.addPreference(mangaSyncCategory);
for (MangaSyncService sync : syncManager.getSyncServices()) {
MangaSyncLoginDialog dialog = new MangaSyncLoginDialog(
screen.getContext(), preferences, sync);
dialog.setTitle(sync.getName());
mangaSyncCategory.addPreference(dialog);
}
return view;
}
private List<Source> getSourcesWithLogin() {
return Observable.from(sourceManager.getSources())
.filter(Source::isLoginRequired)
.toList()
.toBlocking()
.single();
}
}

View File

@@ -0,0 +1,96 @@
package eu.kanade.tachiyomi.ui.setting;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import android.support.v7.widget.Toolbar;
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.cache.ChapterCache;
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity;
public class SettingsActivity extends BaseActivity {
@Inject PreferencesHelper preferences;
@Inject ChapterCache chapterCache;
@Inject DatabaseHelper db;
@Bind(R.id.toolbar) Toolbar toolbar;
@Override
protected void onCreate(Bundle savedState) {
super.onCreate(savedState);
App.get(this).getComponent().inject(this);
setContentView(R.layout.activity_preferences);
ButterKnife.bind(this);
setupToolbar(toolbar);
if (savedState == null)
getFragmentManager().beginTransaction().replace(R.id.settings_content,
new SettingsMainFragment())
.commit();
}
@Override
public void onBackPressed() {
if( !getFragmentManager().popBackStackImmediate() ) super.onBackPressed();
}
public static class SettingsMainFragment extends PreferenceFragment {
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(R.xml.pref_main);
registerSubpreference(R.string.pref_category_general_key,
SettingsGeneralFragment.newInstance(
R.xml.pref_general, R.string.pref_category_general));
registerSubpreference(R.string.pref_category_reader_key,
SettingsNestedFragment.newInstance(
R.xml.pref_reader, R.string.pref_category_reader));
registerSubpreference(R.string.pref_category_downloads_key,
SettingsDownloadsFragment.newInstance(
R.xml.pref_downloads, R.string.pref_category_downloads));
registerSubpreference(R.string.pref_category_accounts_key,
SettingsAccountsFragment.newInstance(
R.xml.pref_accounts, R.string.pref_category_accounts));
registerSubpreference(R.string.pref_category_advanced_key,
SettingsAdvancedFragment.newInstance(
R.xml.pref_advanced, R.string.pref_category_advanced));
registerSubpreference(R.string.pref_category_about_key,
SettingsAboutFragment.newInstance(
R.xml.pref_about, R.string.pref_category_about));
}
@Override
public void onResume() {
super.onResume();
((BaseActivity) getActivity()).setToolbarTitle(getString(R.string.label_settings));
}
private void registerSubpreference(int preferenceResource, PreferenceFragment fragment) {
findPreference(getString(preferenceResource))
.setOnPreferenceClickListener(preference -> {
getFragmentManager().beginTransaction()
.replace(R.id.settings_content, fragment)
.addToBackStack(fragment.getClass().getSimpleName()).commit();
return true;
});
}
}
}

View File

@@ -0,0 +1,109 @@
package eu.kanade.tachiyomi.ui.setting;
import android.os.Bundle;
import android.preference.Preference;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.afollestad.materialdialogs.MaterialDialog;
import java.io.File;
import java.util.concurrent.atomic.AtomicInteger;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.cache.ChapterCache;
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
import eu.kanade.tachiyomi.util.ToastUtil;
import rx.Observable;
import rx.android.schedulers.AndroidSchedulers;
import rx.schedulers.Schedulers;
import rx.subscriptions.CompositeSubscription;
public class SettingsAdvancedFragment extends SettingsNestedFragment {
private CompositeSubscription subscriptions;
public static SettingsNestedFragment newInstance(int resourcePreference, int resourceTitle) {
SettingsNestedFragment fragment = new SettingsAdvancedFragment();
fragment.setArgs(resourcePreference, resourceTitle);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
View view = super.onCreateView(inflater, container, savedState);
subscriptions = new CompositeSubscription();
Preference clearCache = findPreference(getString(R.string.pref_clear_chapter_cache_key));
clearCache.setOnPreferenceClickListener(preference -> {
clearChapterCache(preference);
return true;
});
clearCache.setSummary(getString(R.string.used_cache, getChapterCache().getReadableSize()));
Preference clearDatabase = findPreference(getString(R.string.pref_clear_database_key));
clearDatabase.setOnPreferenceClickListener(preference -> {
clearDatabase();
return true;
});
return view;
}
@Override
public void onDestroyView() {
subscriptions.unsubscribe();
super.onDestroyView();
}
private void clearChapterCache(Preference preference) {
final ChapterCache chapterCache = getChapterCache();
final AtomicInteger deletedFiles = new AtomicInteger();
File[] files = chapterCache.getCacheDir().listFiles();
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
.title(R.string.deleting)
.progress(false, files.length, true)
.cancelable(false)
.show();
subscriptions.add(Observable.defer(() -> Observable.from(files))
.concatMap(file -> {
if (chapterCache.remove(file.getName())) {
deletedFiles.incrementAndGet();
}
return Observable.just(file);
})
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(file -> dialog.incrementProgress(1),
error -> {
dialog.dismiss();
ToastUtil.showShort(getActivity(), getString(R.string.cache_delete_error));
}, () -> {
dialog.dismiss();
ToastUtil.showShort(getActivity(), getString(R.string.cache_deleted, deletedFiles.get()));
preference.setSummary(getString(R.string.used_cache, chapterCache.getReadableSize()));
}));
}
private void clearDatabase() {
final DatabaseHelper db = getSettingsActivity().db;
new MaterialDialog.Builder(getActivity())
.content(R.string.clear_database_confirmation)
.positiveText(R.string.button_yes)
.negativeText(R.string.button_no)
.onPositive((dialog1, which) -> {
db.deleteMangasNotInLibrary().executeAsBlocking();
})
.show();
}
private ChapterCache getChapterCache() {
return getSettingsActivity().chapterCache;
}
}

View File

@@ -0,0 +1,93 @@
package eu.kanade.tachiyomi.ui.setting;
import android.app.Activity;
import android.content.Intent;
import android.net.Uri;
import android.os.Bundle;
import android.preference.Preference;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import com.nononsenseapps.filepicker.AbstractFilePickerFragment;
import com.nononsenseapps.filepicker.FilePickerActivity;
import com.nononsenseapps.filepicker.FilePickerFragment;
import com.nononsenseapps.filepicker.LogicHandler;
import java.io.File;
import eu.kanade.tachiyomi.R;
public class SettingsDownloadsFragment extends SettingsNestedFragment {
Preference downloadDirPref;
public static final int DOWNLOAD_DIR_CODE = 1;
public static SettingsNestedFragment newInstance(int resourcePreference, int resourceTitle) {
SettingsNestedFragment fragment = new SettingsDownloadsFragment();
fragment.setArgs(resourcePreference, resourceTitle);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
View view = super.onCreateView(inflater, container, savedInstanceState);
downloadDirPref = findPreference(getString(R.string.pref_download_directory_key));
downloadDirPref.setOnPreferenceClickListener(preference -> {
Intent i = new Intent(getActivity(), CustomLayoutPickerActivity.class);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false);
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true);
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR);
i.putExtra(FilePickerActivity.EXTRA_START_PATH, preferences.getDownloadsDirectory());
startActivityForResult(i, DOWNLOAD_DIR_CODE);
return true;
});
return view;
}
@Override
public void onResume() {
super.onResume();
downloadDirPref.setSummary(preferences.getDownloadsDirectory());
}
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
if (requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) {
Uri uri = data.getData();
preferences.setDownloadsDirectory(uri.getPath());
}
}
public static class CustomLayoutPickerActivity extends FilePickerActivity {
@Override
protected AbstractFilePickerFragment<File> getFragment(
String startPath, int mode, boolean allowMultiple, boolean allowCreateDir) {
AbstractFilePickerFragment<File> fragment = new CustomLayoutFilePickerFragment();
fragment.setArgs(startPath, mode, allowMultiple, allowCreateDir);
return fragment;
}
}
public static class CustomLayoutFilePickerFragment extends FilePickerFragment {
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
View v;
switch (viewType) {
case LogicHandler.VIEWTYPE_DIR:
v = LayoutInflater.from(getActivity()).inflate(R.layout.listitem_dir,
parent, false);
return new DirViewHolder(v);
default:
return super.onCreateViewHolder(parent, viewType);
}
}
}
}

View File

@@ -0,0 +1,44 @@
package eu.kanade.tachiyomi.ui.setting;
import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import eu.kanade.tachiyomi.R;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.data.sync.LibraryUpdateAlarm;
import eu.kanade.tachiyomi.widget.preference.IntListPreference;
import eu.kanade.tachiyomi.widget.preference.LibraryColumnsDialog;
public class SettingsGeneralFragment extends SettingsNestedFragment {
public static SettingsNestedFragment newInstance(int resourcePreference, int resourceTitle) {
SettingsNestedFragment fragment = new SettingsGeneralFragment();
fragment.setArgs(resourcePreference, resourceTitle);
return fragment;
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedState) {
View view = super.onCreateView(inflater, container, savedState);
PreferencesHelper preferences = getSettingsActivity().preferences;
LibraryColumnsDialog columnsDialog = (LibraryColumnsDialog) findPreference(
getString(R.string.pref_library_columns_dialog_key));
columnsDialog.setPreferencesHelper(preferences);
IntListPreference updateInterval = (IntListPreference) findPreference(
getString(R.string.pref_library_update_interval_key));
updateInterval.setOnPreferenceChangeListener((preference, newValue) -> {
LibraryUpdateAlarm.startAlarm(getActivity(), Integer.parseInt((String) newValue));
return true;
});
return view;
}
}

View File

@@ -0,0 +1,55 @@
package eu.kanade.tachiyomi.ui.setting;
import android.os.Bundle;
import android.preference.PreferenceFragment;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
import eu.kanade.tachiyomi.ui.base.activity.BaseActivity;
public class SettingsNestedFragment extends PreferenceFragment {
protected PreferencesHelper preferences;
private static final String RESOURCE_FILE = "resource_file";
private static final String TOOLBAR_TITLE = "toolbar_title";
public static SettingsNestedFragment newInstance(int resourcePreference, int resourceTitle) {
SettingsNestedFragment fragment = new SettingsNestedFragment();
fragment.setArgs(resourcePreference, resourceTitle);
return fragment;
}
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
addPreferencesFromResource(getArguments().getInt(RESOURCE_FILE));
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
preferences = getSettingsActivity().preferences;
return super.onCreateView(inflater, container, savedInstanceState);
}
@Override
public void onResume() {
super.onResume();
((BaseActivity) getActivity())
.setToolbarTitle(getString(getArguments().getInt(TOOLBAR_TITLE)));
}
public void setArgs(int resourcePreference, int resourceTitle) {
Bundle args = new Bundle();
args.putInt(RESOURCE_FILE, resourcePreference);
args.putInt(TOOLBAR_TITLE, resourceTitle);
setArguments(args);
}
public SettingsActivity getSettingsActivity() {
return (SettingsActivity) getActivity();
}
}