Catalogue in Kotlin. Support library upgraded to 23.2.0. Downloads directory now shows a list of folders, it should fix #141.
This commit is contained in:
parent
fabdba4452
commit
ee4bf163ef
18 changed files with 1046 additions and 747 deletions
|
@ -98,7 +98,7 @@ apt {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
final SUPPORT_LIBRARY_VERSION = '23.1.1'
|
final SUPPORT_LIBRARY_VERSION = '23.2.0'
|
||||||
final DAGGER_VERSION = '2.0.2'
|
final DAGGER_VERSION = '2.0.2'
|
||||||
final OKHTTP_VERSION = '3.2.0'
|
final OKHTTP_VERSION = '3.2.0'
|
||||||
final RETROFIT_VERSION = '2.0.0-beta4'
|
final RETROFIT_VERSION = '2.0.0-beta4'
|
||||||
|
|
|
@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.data.source.model.Page;
|
||||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
|
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
|
||||||
import eu.kanade.tachiyomi.util.DiskUtils;
|
import eu.kanade.tachiyomi.util.DiskUtils;
|
||||||
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator;
|
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator;
|
||||||
|
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||||
import eu.kanade.tachiyomi.util.UrlUtil;
|
import eu.kanade.tachiyomi.util.UrlUtil;
|
||||||
import rx.Observable;
|
import rx.Observable;
|
||||||
import rx.Subscription;
|
import rx.Subscription;
|
||||||
|
@ -84,7 +85,11 @@ public class DownloadManager {
|
||||||
if (finished) {
|
if (finished) {
|
||||||
DownloadService.stop(context);
|
DownloadService.stop(context);
|
||||||
}
|
}
|
||||||
}, e -> DownloadService.stop(context));
|
}, e -> {
|
||||||
|
DownloadService.stop(context);
|
||||||
|
Timber.e(e, e.getMessage());
|
||||||
|
ToastUtil.showShort(context, e.getMessage());
|
||||||
|
});
|
||||||
|
|
||||||
if (!isRunning) {
|
if (!isRunning) {
|
||||||
isRunning = true;
|
isRunning = true;
|
||||||
|
@ -410,7 +415,7 @@ public class DownloadManager {
|
||||||
if (queue.isEmpty())
|
if (queue.isEmpty())
|
||||||
return false;
|
return false;
|
||||||
|
|
||||||
if (downloadsSubscription == null)
|
if (downloadsSubscription == null || downloadsSubscription.isUnsubscribed())
|
||||||
initializeSubscriptions();
|
initializeSubscriptions();
|
||||||
|
|
||||||
final List<Download> pending = new ArrayList<>();
|
final List<Download> pending = new ArrayList<>();
|
||||||
|
|
|
@ -1,69 +0,0 @@
|
||||||
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();
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Manga> getItems() {
|
|
||||||
return mItems;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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();
|
|
||||||
if (parent.getId() == R.id.catalogue_grid) {
|
|
||||||
View v = inflater.inflate(R.layout.item_catalogue_grid, parent, false);
|
|
||||||
return new CatalogueGridHolder(v, this, fragment);
|
|
||||||
} else {
|
|
||||||
View v = inflater.inflate(R.layout.item_catalogue_list, parent, false);
|
|
||||||
return new CatalogueListHolder(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));
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,89 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.catalogue
|
||||||
|
|
||||||
|
import android.view.ViewGroup
|
||||||
|
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.util.inflate
|
||||||
|
import java.util.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter storing a list of manga from the catalogue.
|
||||||
|
*
|
||||||
|
* @param fragment the fragment containing this adapter.
|
||||||
|
*/
|
||||||
|
class CatalogueAdapter(private val fragment: CatalogueFragment) : FlexibleAdapter<CatalogueHolder, Manga>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property to get the list of manga in the adapter.
|
||||||
|
*/
|
||||||
|
val items: List<Manga>
|
||||||
|
get() = mItems
|
||||||
|
|
||||||
|
init {
|
||||||
|
mItems = ArrayList<Manga>()
|
||||||
|
setHasStableIds(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds a list of manga to the adapter.
|
||||||
|
*
|
||||||
|
* @param list the list to add.
|
||||||
|
*/
|
||||||
|
fun addItems(list: List<Manga>) {
|
||||||
|
mItems.addAll(list)
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clears the list of manga from the adapter.
|
||||||
|
*/
|
||||||
|
fun clear() {
|
||||||
|
mItems.clear()
|
||||||
|
notifyDataSetChanged()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the identifier for a manga.
|
||||||
|
*
|
||||||
|
* @param position the position in the adapter.
|
||||||
|
* @return an identifier for the item.
|
||||||
|
*/
|
||||||
|
override fun getItemId(position: Int): Long {
|
||||||
|
return mItems[position].id
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to filter the list. Required but not used.
|
||||||
|
*/
|
||||||
|
override fun updateDataSet(param: String) {}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new view holder.
|
||||||
|
*
|
||||||
|
* @param parent the parent view.
|
||||||
|
* @param viewType the type of the holder.
|
||||||
|
* @return a new view holder for a manga.
|
||||||
|
*/
|
||||||
|
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): CatalogueHolder {
|
||||||
|
if (parent.id == R.id.catalogue_grid) {
|
||||||
|
val v = parent.inflate(R.layout.item_catalogue_grid)
|
||||||
|
return CatalogueGridHolder(v, this, fragment)
|
||||||
|
} else {
|
||||||
|
val v = parent.inflate(R.layout.item_catalogue_list)
|
||||||
|
return CatalogueListHolder(v, this, fragment)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Binds a holder with a new position.
|
||||||
|
*
|
||||||
|
* @param holder the holder to bind.
|
||||||
|
* @param position the position to bind.
|
||||||
|
*/
|
||||||
|
override fun onBindViewHolder(holder: CatalogueHolder, position: Int) {
|
||||||
|
val manga = getItem(position)
|
||||||
|
holder.onSetValues(manga, fragment.presenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,354 +0,0 @@
|
||||||
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.v4.content.ContextCompat;
|
|
||||||
import android.support.v7.widget.GridLayoutManager;
|
|
||||||
import android.support.v7.widget.LinearLayoutManager;
|
|
||||||
import android.support.v7.widget.RecyclerView;
|
|
||||||
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.view.animation.Animation;
|
|
||||||
import android.view.animation.AnimationUtils;
|
|
||||||
import android.widget.AdapterView;
|
|
||||||
import android.widget.ArrayAdapter;
|
|
||||||
import android.widget.ProgressBar;
|
|
||||||
import android.widget.Spinner;
|
|
||||||
import android.widget.ViewSwitcher;
|
|
||||||
|
|
||||||
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.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.decoration.DividerItemDecoration;
|
|
||||||
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.EndlessGridScrollListener;
|
|
||||||
import eu.kanade.tachiyomi.widget.EndlessListScrollListener;
|
|
||||||
import icepick.State;
|
|
||||||
import nucleus.factory.RequiresPresenter;
|
|
||||||
import rx.Subscription;
|
|
||||||
import rx.android.schedulers.AndroidSchedulers;
|
|
||||||
import rx.subjects.PublishSubject;
|
|
||||||
import timber.log.Timber;
|
|
||||||
|
|
||||||
@RequiresPresenter(CataloguePresenter.class)
|
|
||||||
public class CatalogueFragment extends BaseRxFragment<CataloguePresenter>
|
|
||||||
implements FlexibleViewHolder.OnListItemClickListener {
|
|
||||||
|
|
||||||
@Bind(R.id.switcher) ViewSwitcher switcher;
|
|
||||||
@Bind(R.id.catalogue_grid) AutofitRecyclerView catalogueGrid;
|
|
||||||
@Bind(R.id.catalogue_list) RecyclerView catalogueList;
|
|
||||||
@Bind(R.id.progress) ProgressBar progress;
|
|
||||||
@Bind(R.id.progress_grid) ProgressBar progressGrid;
|
|
||||||
|
|
||||||
private Toolbar toolbar;
|
|
||||||
private Spinner spinner;
|
|
||||||
private CatalogueAdapter adapter;
|
|
||||||
private EndlessGridScrollListener gridScrollListener;
|
|
||||||
private EndlessListScrollListener listScrollListener;
|
|
||||||
|
|
||||||
@State String query = "";
|
|
||||||
@State int selectedIndex;
|
|
||||||
private final int SEARCH_TIMEOUT = 1000;
|
|
||||||
|
|
||||||
private PublishSubject<String> queryDebouncerSubject;
|
|
||||||
private Subscription queryDebouncerSubscription;
|
|
||||||
|
|
||||||
private MenuItem displayMode;
|
|
||||||
private MenuItem searchItem;
|
|
||||||
|
|
||||||
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, scroll listener and recycler views
|
|
||||||
adapter = new CatalogueAdapter(this);
|
|
||||||
|
|
||||||
GridLayoutManager glm = (GridLayoutManager) catalogueGrid.getLayoutManager();
|
|
||||||
gridScrollListener = new EndlessGridScrollListener(glm, this::requestNextPage);
|
|
||||||
catalogueGrid.setHasFixedSize(true);
|
|
||||||
catalogueGrid.setAdapter(adapter);
|
|
||||||
catalogueGrid.addOnScrollListener(gridScrollListener);
|
|
||||||
|
|
||||||
LinearLayoutManager llm = new LinearLayoutManager(getActivity());
|
|
||||||
listScrollListener = new EndlessListScrollListener(llm, this::requestNextPage);
|
|
||||||
catalogueList.setHasFixedSize(true);
|
|
||||||
catalogueList.setAdapter(adapter);
|
|
||||||
catalogueList.setLayoutManager(llm);
|
|
||||||
catalogueList.addOnScrollListener(listScrollListener);
|
|
||||||
catalogueList.addItemDecoration(new DividerItemDecoration(
|
|
||||||
ContextCompat.getDrawable(getContext(), R.drawable.line_divider)));
|
|
||||||
|
|
||||||
if (getPresenter().isListMode()) {
|
|
||||||
switcher.showNext();
|
|
||||||
}
|
|
||||||
|
|
||||||
Animation inAnim = AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_in);
|
|
||||||
Animation outAnim = AnimationUtils.loadAnimation(getActivity(), android.R.anim.fade_out);
|
|
||||||
switcher.setInAnimation(inAnim);
|
|
||||||
switcher.setOutAnimation(outAnim);
|
|
||||||
|
|
||||||
// Create toolbar spinner
|
|
||||||
Context themedContext = getBaseActivity().getSupportActionBar() != null ?
|
|
||||||
getBaseActivity().getSupportActionBar().getThemedContext() : getActivity();
|
|
||||||
spinner = new Spinner(themedContext);
|
|
||||||
ArrayAdapter<Source> spinnerAdapter = new ArrayAdapter<>(themedContext,
|
|
||||||
android.R.layout.simple_spinner_item, getPresenter().getEnabledSources());
|
|
||||||
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item);
|
|
||||||
|
|
||||||
if (savedState == null) {
|
|
||||||
selectedIndex = getPresenter().getLastUsedSourceIndex();
|
|
||||||
}
|
|
||||||
spinner.setAdapter(spinnerAdapter);
|
|
||||||
spinner.setSelection(selectedIndex);
|
|
||||||
spinner.setOnItemSelectedListener(new AdapterView.OnItemSelectedListener() {
|
|
||||||
@Override
|
|
||||||
public void onItemSelected(AdapterView<?> parent, View view, int position, long id) {
|
|
||||||
Source source = spinnerAdapter.getItem(position);
|
|
||||||
if (selectedIndex != position || adapter.isEmpty()) {
|
|
||||||
// Set previous selection if it's not a valid source and notify the user
|
|
||||||
if (!getPresenter().isValidSource(source)) {
|
|
||||||
spinner.setSelection(getPresenter().findFirstValidSource());
|
|
||||||
ToastUtil.showShort(getActivity(), R.string.source_requires_login);
|
|
||||||
} else {
|
|
||||||
selectedIndex = position;
|
|
||||||
getPresenter().setEnabledSource(selectedIndex);
|
|
||||||
showProgressBar();
|
|
||||||
glm.scrollToPositionWithOffset(0, 0);
|
|
||||||
llm.scrollToPositionWithOffset(0, 0);
|
|
||||||
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
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Show next display mode
|
|
||||||
displayMode = menu.findItem(R.id.action_display_mode);
|
|
||||||
int icon = getPresenter().isListMode() ?
|
|
||||||
R.drawable.ic_view_module_white_24dp : R.drawable.ic_view_list_white_24dp;
|
|
||||||
displayMode.setIcon(icon);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
|
||||||
switch (item.getItemId()) {
|
|
||||||
case R.id.action_display_mode:
|
|
||||||
swapDisplayMode();
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
return super.onOptionsItemSelected(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStart() {
|
|
||||||
super.onStart();
|
|
||||||
initializeSearchSubscription();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onStop() {
|
|
||||||
destroySearchSubscription();
|
|
||||||
super.onStop();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onDestroyView() {
|
|
||||||
if (searchItem != null && searchItem.isActionViewExpanded()) {
|
|
||||||
searchItem.collapseActionView();
|
|
||||||
}
|
|
||||||
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) || getPresenter().getSource() == null)
|
|
||||||
return;
|
|
||||||
|
|
||||||
query = newQuery;
|
|
||||||
showProgressBar();
|
|
||||||
catalogueGrid.getLayoutManager().scrollToPosition(0);
|
|
||||||
catalogueList.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 == 0) {
|
|
||||||
adapter.clear();
|
|
||||||
gridScrollListener.resetScroll();
|
|
||||||
listScrollListener.resetScroll();
|
|
||||||
}
|
|
||||||
adapter.addItems(mangas);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onAddPageError(Throwable error) {
|
|
||||||
hideProgressBar();
|
|
||||||
ToastUtil.showShort(getContext(), error.getMessage());
|
|
||||||
Timber.e(error, error.getMessage());
|
|
||||||
}
|
|
||||||
|
|
||||||
public void updateImage(Manga manga) {
|
|
||||||
CatalogueGridHolder holder = getHolder(manga);
|
|
||||||
if (holder != null) {
|
|
||||||
holder.setImage(manga, getPresenter());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void swapDisplayMode() {
|
|
||||||
getPresenter().swapDisplayMode();
|
|
||||||
boolean isListMode = getPresenter().isListMode();
|
|
||||||
int icon = isListMode ?
|
|
||||||
R.drawable.ic_view_module_white_24dp : R.drawable.ic_view_list_white_24dp;
|
|
||||||
displayMode.setIcon(icon);
|
|
||||||
switcher.showNext();
|
|
||||||
if (!isListMode) {
|
|
||||||
// Initialize mangas if going to grid view
|
|
||||||
getPresenter().initializeMangas(adapter.getItems());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Nullable
|
|
||||||
private CatalogueGridHolder getHolder(Manga manga) {
|
|
||||||
return (CatalogueGridHolder) catalogueGrid.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) {
|
|
||||||
final Manga selectedManga = adapter.getItem(position);
|
|
||||||
|
|
||||||
int textRes = selectedManga.favorite ? R.string.remove_from_library : R.string.add_to_library;
|
|
||||||
|
|
||||||
new MaterialDialog.Builder(getActivity())
|
|
||||||
.items(getString(textRes))
|
|
||||||
.itemsCallback((dialog, itemView, which, text) -> {
|
|
||||||
switch (which) {
|
|
||||||
case 0:
|
|
||||||
getPresenter().changeMangaFavorite(selectedManga);
|
|
||||||
adapter.notifyItemChanged(position);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.show();
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,456 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.catalogue
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
import android.support.v4.content.ContextCompat
|
||||||
|
import android.support.v7.widget.GridLayoutManager
|
||||||
|
import android.support.v7.widget.LinearLayoutManager
|
||||||
|
import android.support.v7.widget.SearchView
|
||||||
|
import android.support.v7.widget.Toolbar
|
||||||
|
import android.view.*
|
||||||
|
import android.view.animation.AnimationUtils
|
||||||
|
import android.widget.AdapterView
|
||||||
|
import android.widget.ArrayAdapter
|
||||||
|
import android.widget.ProgressBar
|
||||||
|
import android.widget.Spinner
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
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.main.MainActivity
|
||||||
|
import eu.kanade.tachiyomi.ui.manga.MangaActivity
|
||||||
|
import eu.kanade.tachiyomi.util.ToastUtil
|
||||||
|
import eu.kanade.tachiyomi.widget.EndlessGridScrollListener
|
||||||
|
import eu.kanade.tachiyomi.widget.EndlessListScrollListener
|
||||||
|
import kotlinx.android.synthetic.main.fragment_catalogue.*
|
||||||
|
import nucleus.factory.RequiresPresenter
|
||||||
|
import rx.Subscription
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
import timber.log.Timber
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fragment that shows the manga from the catalogue.
|
||||||
|
* Uses R.layout.fragment_catalogue.
|
||||||
|
*/
|
||||||
|
@RequiresPresenter(CataloguePresenter::class)
|
||||||
|
class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHolder.OnListItemClickListener {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Spinner shown in the toolbar to change the selected source.
|
||||||
|
*/
|
||||||
|
private lateinit var spinner: Spinner
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adapter containing the list of manga from the catalogue.
|
||||||
|
*/
|
||||||
|
private lateinit var adapter: CatalogueAdapter
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll listener for grid mode. It loads next pages when the end of the list is reached.
|
||||||
|
*/
|
||||||
|
private lateinit var gridScrollListener: EndlessGridScrollListener
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Scroll listener for list mode. It loads next pages when the end of the list is reached.
|
||||||
|
*/
|
||||||
|
private lateinit var listScrollListener: EndlessListScrollListener
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query of the search box.
|
||||||
|
*/
|
||||||
|
private var query = ""
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selected index of the spinner (selected source).
|
||||||
|
*/
|
||||||
|
private var selectedIndex: Int = 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Time in milliseconds to wait for input events in the search query before doing network calls.
|
||||||
|
*/
|
||||||
|
private val SEARCH_TIMEOUT = 1000L
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subject to debounce the query.
|
||||||
|
*/
|
||||||
|
private val queryDebouncerSubject = PublishSubject.create<String>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subscription of the debouncer subject.
|
||||||
|
*/
|
||||||
|
private var queryDebouncerSubscription: Subscription? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display mode of the catalogue (list or grid mode).
|
||||||
|
*/
|
||||||
|
private var displayMode: MenuItem? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search item.
|
||||||
|
*/
|
||||||
|
private var searchItem: MenuItem? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Property to get the toolbar from the containing activity.
|
||||||
|
*/
|
||||||
|
private val toolbar: Toolbar
|
||||||
|
get() = (activity as MainActivity).toolbar
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key to save and restore [query] from a [Bundle].
|
||||||
|
*/
|
||||||
|
const val QUERY_KEY = "query_key"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key to save and restore [selectedIndex] from a [Bundle].
|
||||||
|
*/
|
||||||
|
const val SELECTED_INDEX_KEY = "selected_index_key"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of this fragment.
|
||||||
|
*
|
||||||
|
* @return a new instance of [CatalogueFragment].
|
||||||
|
*/
|
||||||
|
@JvmStatic
|
||||||
|
fun newInstance(): CatalogueFragment {
|
||||||
|
return CatalogueFragment()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
setHasOptionsMenu(true)
|
||||||
|
|
||||||
|
if (savedState != null) {
|
||||||
|
selectedIndex = savedState.getInt(SELECTED_INDEX_KEY)
|
||||||
|
query = savedState.getString(QUERY_KEY)
|
||||||
|
} else {
|
||||||
|
selectedIndex = presenter.getLastUsedSourceIndex()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? {
|
||||||
|
return inflater.inflate(R.layout.fragment_catalogue, container, false)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
|
// Initialize adapter, scroll listener and recycler views
|
||||||
|
adapter = CatalogueAdapter(this)
|
||||||
|
|
||||||
|
val glm = catalogue_grid.layoutManager as GridLayoutManager
|
||||||
|
gridScrollListener = EndlessGridScrollListener(glm, { requestNextPage() })
|
||||||
|
catalogue_grid.setHasFixedSize(true)
|
||||||
|
catalogue_grid.adapter = adapter
|
||||||
|
catalogue_grid.addOnScrollListener(gridScrollListener)
|
||||||
|
|
||||||
|
val llm = LinearLayoutManager(activity)
|
||||||
|
listScrollListener = EndlessListScrollListener(llm, { requestNextPage() })
|
||||||
|
catalogue_list.setHasFixedSize(true)
|
||||||
|
catalogue_list.adapter = adapter
|
||||||
|
catalogue_list.layoutManager = llm
|
||||||
|
catalogue_list.addOnScrollListener(listScrollListener)
|
||||||
|
catalogue_list.addItemDecoration(DividerItemDecoration(
|
||||||
|
ContextCompat.getDrawable(context, R.drawable.line_divider)))
|
||||||
|
|
||||||
|
if (presenter.isListMode) {
|
||||||
|
switcher.showNext()
|
||||||
|
}
|
||||||
|
|
||||||
|
switcher.inAnimation = AnimationUtils.loadAnimation(activity, android.R.anim.fade_in)
|
||||||
|
switcher.outAnimation = AnimationUtils.loadAnimation(activity, android.R.anim.fade_out)
|
||||||
|
|
||||||
|
// Create toolbar spinner
|
||||||
|
val themedContext = baseActivity.supportActionBar?.themedContext ?: activity
|
||||||
|
|
||||||
|
val spinnerAdapter = ArrayAdapter(themedContext,
|
||||||
|
android.R.layout.simple_spinner_item, presenter.getEnabledSources())
|
||||||
|
spinnerAdapter.setDropDownViewResource(android.R.layout.simple_spinner_dropdown_item)
|
||||||
|
|
||||||
|
val onItemSelected = object : AdapterView.OnItemSelectedListener {
|
||||||
|
override fun onItemSelected(parent: AdapterView<*>, view: View, position: Int, id: Long) {
|
||||||
|
val source = spinnerAdapter.getItem(position)
|
||||||
|
if (selectedIndex != position || adapter.isEmpty) {
|
||||||
|
// Set previous selection if it's not a valid source and notify the user
|
||||||
|
if (!presenter.isValidSource(source)) {
|
||||||
|
spinner.setSelection(presenter.findFirstValidSource())
|
||||||
|
ToastUtil.showShort(activity, R.string.source_requires_login)
|
||||||
|
} else {
|
||||||
|
selectedIndex = position
|
||||||
|
presenter.setEnabledSource(selectedIndex)
|
||||||
|
showProgressBar()
|
||||||
|
glm.scrollToPositionWithOffset(0, 0)
|
||||||
|
llm.scrollToPositionWithOffset(0, 0)
|
||||||
|
presenter.startRequesting(source)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNothingSelected(parent: AdapterView<*>) {
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
spinner = Spinner(themedContext).apply {
|
||||||
|
adapter = spinnerAdapter
|
||||||
|
setSelection(selectedIndex)
|
||||||
|
onItemSelectedListener = onItemSelected
|
||||||
|
}
|
||||||
|
|
||||||
|
setToolbarTitle("")
|
||||||
|
toolbar.addView(spinner)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSaveInstanceState(bundle: Bundle) {
|
||||||
|
bundle.putInt(SELECTED_INDEX_KEY, selectedIndex)
|
||||||
|
bundle.putString(QUERY_KEY, query)
|
||||||
|
super.onSaveInstanceState(bundle)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||||
|
inflater.inflate(R.menu.catalogue_list, menu)
|
||||||
|
|
||||||
|
// Initialize search menu
|
||||||
|
searchItem = menu.findItem(R.id.action_search).apply {
|
||||||
|
val searchView = actionView as SearchView
|
||||||
|
|
||||||
|
if (!query.isNullOrEmpty()) {
|
||||||
|
expandActionView()
|
||||||
|
searchView.setQuery(query, true)
|
||||||
|
searchView.clearFocus()
|
||||||
|
}
|
||||||
|
searchView.setOnQueryTextListener(object : SearchView.OnQueryTextListener {
|
||||||
|
override fun onQueryTextSubmit(query: String): Boolean {
|
||||||
|
onSearchEvent(query, true)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onQueryTextChange(newText: String): Boolean {
|
||||||
|
onSearchEvent(newText, false)
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Show next display mode
|
||||||
|
displayMode = menu.findItem(R.id.action_display_mode).apply {
|
||||||
|
val icon = if (presenter.isListMode)
|
||||||
|
R.drawable.ic_view_module_white_24dp
|
||||||
|
else
|
||||||
|
R.drawable.ic_view_list_white_24dp
|
||||||
|
setIcon(icon)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||||
|
when (item.itemId) {
|
||||||
|
R.id.action_display_mode -> swapDisplayMode()
|
||||||
|
else -> return super.onOptionsItemSelected(item)
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStart() {
|
||||||
|
super.onStart()
|
||||||
|
initializeSearchSubscription()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onStop() {
|
||||||
|
destroySearchSubscription()
|
||||||
|
super.onStop()
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onDestroyView() {
|
||||||
|
searchItem?.let {
|
||||||
|
if (it.isActionViewExpanded) it.collapseActionView()
|
||||||
|
}
|
||||||
|
toolbar.removeView(spinner)
|
||||||
|
super.onDestroyView()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen for query events on the debouncer.
|
||||||
|
*/
|
||||||
|
private fun initializeSearchSubscription() {
|
||||||
|
queryDebouncerSubscription = queryDebouncerSubject.debounce(SEARCH_TIMEOUT, TimeUnit.MILLISECONDS)
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { restartRequest(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unsubscribe from the query debouncer.
|
||||||
|
*/
|
||||||
|
private fun destroySearchSubscription() {
|
||||||
|
queryDebouncerSubscription?.unsubscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the input text changes or is submitted
|
||||||
|
*
|
||||||
|
* @param query the new query.
|
||||||
|
* @param now whether to send the network call now or debounce it by [SEARCH_TIMEOUT].
|
||||||
|
*/
|
||||||
|
private fun onSearchEvent(query: String, now: Boolean) {
|
||||||
|
if (now) {
|
||||||
|
restartRequest(query)
|
||||||
|
} else {
|
||||||
|
queryDebouncerSubject.onNext(query)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restarts the request.
|
||||||
|
*
|
||||||
|
* @param newQuery the new query.
|
||||||
|
*/
|
||||||
|
private fun restartRequest(newQuery: String) {
|
||||||
|
// If text didn't change, do nothing
|
||||||
|
if (query == newQuery || presenter.source == null)
|
||||||
|
return
|
||||||
|
|
||||||
|
query = newQuery
|
||||||
|
showProgressBar()
|
||||||
|
catalogue_grid.layoutManager.scrollToPosition(0)
|
||||||
|
catalogue_list.layoutManager.scrollToPosition(0)
|
||||||
|
|
||||||
|
presenter.restartRequest(query)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the next page (if available). Called from scroll listeners when they reach the end.
|
||||||
|
*/
|
||||||
|
private fun requestNextPage() {
|
||||||
|
if (presenter.hasNextPage()) {
|
||||||
|
showGridProgressBar()
|
||||||
|
presenter.requestNext()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from the presenter when the network request is received.
|
||||||
|
*
|
||||||
|
* @param page the current page.
|
||||||
|
* @param mangas the list of manga of the page.
|
||||||
|
*/
|
||||||
|
fun onAddPage(page: Int, mangas: List<Manga>) {
|
||||||
|
hideProgressBar()
|
||||||
|
if (page == 0) {
|
||||||
|
adapter.clear()
|
||||||
|
gridScrollListener.resetScroll()
|
||||||
|
listScrollListener.resetScroll()
|
||||||
|
}
|
||||||
|
adapter.addItems(mangas)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from the presenter when the network request fails.
|
||||||
|
*
|
||||||
|
* @param error the error received.
|
||||||
|
*/
|
||||||
|
fun onAddPageError(error: Throwable) {
|
||||||
|
hideProgressBar()
|
||||||
|
ToastUtil.showShort(context, error.message)
|
||||||
|
Timber.e(error, error.message)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called from the presenter when a manga is initialized.
|
||||||
|
*
|
||||||
|
* @param manga the manga initialized
|
||||||
|
*/
|
||||||
|
fun onMangaInitialized(manga: Manga) {
|
||||||
|
getHolder(manga)?.setImage(manga, presenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Swaps the current display mode.
|
||||||
|
*/
|
||||||
|
fun swapDisplayMode() {
|
||||||
|
presenter.swapDisplayMode()
|
||||||
|
val isListMode = presenter.isListMode
|
||||||
|
val icon = if (isListMode)
|
||||||
|
R.drawable.ic_view_module_white_24dp
|
||||||
|
else
|
||||||
|
R.drawable.ic_view_list_white_24dp
|
||||||
|
displayMode?.setIcon(icon)
|
||||||
|
switcher.showNext()
|
||||||
|
if (!isListMode) {
|
||||||
|
// Initialize mangas if going to grid view
|
||||||
|
presenter.initializeMangas(adapter.items)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the view holder for the given manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga to find.
|
||||||
|
* @return the holder of the manga or null if it's not bound.
|
||||||
|
*/
|
||||||
|
private fun getHolder(manga: Manga): CatalogueGridHolder? {
|
||||||
|
return catalogue_grid.findViewHolderForItemId(manga.id) as? CatalogueGridHolder
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the progress bar.
|
||||||
|
*/
|
||||||
|
private fun showProgressBar() {
|
||||||
|
progress.visibility = ProgressBar.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Shows the progress bar at the end of the screen.
|
||||||
|
*/
|
||||||
|
private fun showGridProgressBar() {
|
||||||
|
progress_grid.visibility = ProgressBar.VISIBLE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hides active progress bars.
|
||||||
|
*/
|
||||||
|
private fun hideProgressBar() {
|
||||||
|
progress.visibility = ProgressBar.GONE
|
||||||
|
progress_grid.visibility = ProgressBar.GONE
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a manga is clicked.
|
||||||
|
*
|
||||||
|
* @param position the position of the element clicked.
|
||||||
|
* @return true if the item should be selected, false otherwise.
|
||||||
|
*/
|
||||||
|
override fun onListItemClick(position: Int): Boolean {
|
||||||
|
val selectedManga = adapter.getItem(position)
|
||||||
|
|
||||||
|
val intent = MangaActivity.newIntent(activity, selectedManga)
|
||||||
|
intent.putExtra(MangaActivity.MANGA_ONLINE, true)
|
||||||
|
startActivity(intent)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when a manga is long clicked.
|
||||||
|
*
|
||||||
|
* @param position the position of the element clicked.
|
||||||
|
*/
|
||||||
|
override fun onListItemLongClick(position: Int) {
|
||||||
|
val selectedManga = adapter.getItem(position)
|
||||||
|
|
||||||
|
val textRes = if (selectedManga.favorite) R.string.remove_from_library else R.string.add_to_library
|
||||||
|
|
||||||
|
MaterialDialog.Builder(activity)
|
||||||
|
.items(getString(textRes))
|
||||||
|
.itemsCallback { dialog, itemView, which, text ->
|
||||||
|
when (which) {
|
||||||
|
0 -> {
|
||||||
|
presenter.changeMangaFavorite(selectedManga)
|
||||||
|
adapter.notifyItemChanged(position)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}.show()
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -1,43 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.ui.catalogue;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.ImageView;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import com.mikepenz.iconics.view.IconicsImageView;
|
|
||||||
|
|
||||||
import butterknife.Bind;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import eu.kanade.tachiyomi.R;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
|
|
||||||
public class CatalogueGridHolder extends CatalogueHolder {
|
|
||||||
|
|
||||||
@Bind(R.id.title) TextView title;
|
|
||||||
@Bind(R.id.thumbnail) ImageView thumbnail;
|
|
||||||
@Bind(R.id.favorite_sticker) IconicsImageView favoriteSticker;
|
|
||||||
|
|
||||||
public CatalogueGridHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) {
|
|
||||||
super(view, adapter, listener);
|
|
||||||
ButterKnife.bind(this, view);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSetValues(Manga manga, CataloguePresenter presenter) {
|
|
||||||
title.setText(manga.title);
|
|
||||||
// Set visibility of in library icon.
|
|
||||||
favoriteSticker.setVisibility(manga.favorite ? View.VISIBLE : View.GONE);
|
|
||||||
// Set alpha of thumbnail.
|
|
||||||
thumbnail.setAlpha(manga.favorite ? 0.3f : 1.0f);
|
|
||||||
setImage(manga, presenter);
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,54 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.catalogue
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import kotlinx.android.synthetic.main.item_catalogue_grid.view.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
||||||
|
* All the elements from the layout file "item_catalogue_grid" are available in this class.
|
||||||
|
*
|
||||||
|
* @param view the inflated view for this holder.
|
||||||
|
* @param adapter the adapter handling this holder.
|
||||||
|
* @param listener a listener to react to single tap and long tap events.
|
||||||
|
* @constructor creates a new catalogue holder.
|
||||||
|
*/
|
||||||
|
class CatalogueGridHolder(private val view: View, adapter: CatalogueAdapter, listener: OnListItemClickListener) :
|
||||||
|
CatalogueHolder(view, adapter, listener) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
|
||||||
|
* holder with the given manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga to bind.
|
||||||
|
* @param presenter the catalogue presenter.
|
||||||
|
*/
|
||||||
|
override fun onSetValues(manga: Manga, presenter: CataloguePresenter) {
|
||||||
|
// Set manga title
|
||||||
|
view.title.text = manga.title
|
||||||
|
|
||||||
|
// Set visibility of in library icon.
|
||||||
|
view.favorite_sticker.visibility = if (manga.favorite) View.VISIBLE else View.GONE
|
||||||
|
|
||||||
|
// Set alpha of thumbnail.
|
||||||
|
view.thumbnail.alpha = if (manga.favorite) 0.3f else 1.0f
|
||||||
|
|
||||||
|
setImage(manga, presenter)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Updates the image for this holder. Useful to update the image when the manga is initialized
|
||||||
|
* and the url is now known.
|
||||||
|
*
|
||||||
|
* @param manga the manga to bind.
|
||||||
|
* @param presenter the catalogue presenter.
|
||||||
|
*/
|
||||||
|
fun setImage(manga: Manga, presenter: CataloguePresenter) {
|
||||||
|
if (manga.thumbnail_url != null) {
|
||||||
|
presenter.coverCache.loadFromNetwork(view.thumbnail, manga.thumbnail_url,
|
||||||
|
presenter.source.glideHeaders)
|
||||||
|
} else {
|
||||||
|
view.thumbnail.setImageResource(android.R.color.transparent)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,15 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.ui.catalogue;
|
|
||||||
|
|
||||||
import android.view.View;
|
|
||||||
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
|
||||||
|
|
||||||
public abstract class CatalogueHolder extends FlexibleViewHolder {
|
|
||||||
|
|
||||||
public CatalogueHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) {
|
|
||||||
super(view, adapter, listener);
|
|
||||||
}
|
|
||||||
|
|
||||||
abstract void onSetValues(Manga manga, CataloguePresenter presenter);
|
|
||||||
}
|
|
|
@ -0,0 +1,25 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.catalogue
|
||||||
|
|
||||||
|
import android.view.View
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generic class used to hold the displayed data of a manga in the catalogue.
|
||||||
|
*
|
||||||
|
* @param view the inflated view for this holder.
|
||||||
|
* @param adapter the adapter handling this holder.
|
||||||
|
* @param listener a listener to react to single tap and long tap events.
|
||||||
|
*/
|
||||||
|
abstract class CatalogueHolder(view: View, adapter: CatalogueAdapter, listener: OnListItemClickListener) :
|
||||||
|
FlexibleViewHolder(view, adapter, listener) {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
|
||||||
|
* holder with the given manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga to bind.
|
||||||
|
* @param presenter the catalogue presenter.
|
||||||
|
*/
|
||||||
|
abstract fun onSetValues(manga: Manga, presenter: CataloguePresenter)
|
||||||
|
}
|
|
@ -1,32 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.ui.catalogue;
|
|
||||||
|
|
||||||
import android.support.v4.content.ContextCompat;
|
|
||||||
import android.view.View;
|
|
||||||
import android.widget.TextView;
|
|
||||||
|
|
||||||
import butterknife.Bind;
|
|
||||||
import butterknife.ButterKnife;
|
|
||||||
import eu.kanade.tachiyomi.R;
|
|
||||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
|
||||||
|
|
||||||
public class CatalogueListHolder extends CatalogueHolder {
|
|
||||||
|
|
||||||
@Bind(R.id.title) TextView title;
|
|
||||||
|
|
||||||
private final int favoriteColor;
|
|
||||||
private final int unfavoriteColor;
|
|
||||||
|
|
||||||
public CatalogueListHolder(View view, CatalogueAdapter adapter, OnListItemClickListener listener) {
|
|
||||||
super(view, adapter, listener);
|
|
||||||
ButterKnife.bind(this, view);
|
|
||||||
|
|
||||||
favoriteColor = ContextCompat.getColor(view.getContext(), R.color.hint_text);
|
|
||||||
unfavoriteColor = ContextCompat.getColor(view.getContext(), R.color.primary_text);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onSetValues(Manga manga, CataloguePresenter presenter) {
|
|
||||||
title.setText(manga.title);
|
|
||||||
title.setTextColor(manga.favorite ? favoriteColor : unfavoriteColor);
|
|
||||||
}
|
|
||||||
}
|
|
|
@ -0,0 +1,35 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.catalogue
|
||||||
|
|
||||||
|
import android.support.v4.content.ContextCompat
|
||||||
|
import android.view.View
|
||||||
|
import eu.kanade.tachiyomi.R
|
||||||
|
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||||
|
import kotlinx.android.synthetic.main.item_catalogue_list.view.*
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to hold the displayed data of a manga in the catalogue, like the cover or the title.
|
||||||
|
* All the elements from the layout file "item_catalogue_list" are available in this class.
|
||||||
|
*
|
||||||
|
* @param view the inflated view for this holder.
|
||||||
|
* @param adapter the adapter handling this holder.
|
||||||
|
* @param listener a listener to react to single tap and long tap events.
|
||||||
|
* @constructor creates a new catalogue holder.
|
||||||
|
*/
|
||||||
|
class CatalogueListHolder(private val view: View, adapter: CatalogueAdapter, listener: OnListItemClickListener) :
|
||||||
|
CatalogueHolder(view, adapter, listener) {
|
||||||
|
|
||||||
|
private val favoriteColor = ContextCompat.getColor(view.context, R.color.hint_text)
|
||||||
|
private val unfavoriteColor = ContextCompat.getColor(view.context, R.color.primary_text)
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method called from [CatalogueAdapter.onBindViewHolder]. It updates the data for this
|
||||||
|
* holder with the given manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga to bind.
|
||||||
|
* @param presenter the catalogue presenter.
|
||||||
|
*/
|
||||||
|
override fun onSetValues(manga: Manga, presenter: CataloguePresenter) {
|
||||||
|
view.title.text = manga.title
|
||||||
|
view.title.setTextColor(if (manga.favorite) favoriteColor else unfavoriteColor)
|
||||||
|
}
|
||||||
|
}
|
|
@ -1,221 +0,0 @@
|
||||||
package eu.kanade.tachiyomi.ui.catalogue;
|
|
||||||
|
|
||||||
import android.os.Bundle;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
|
|
||||||
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 List<Source> sources;
|
|
||||||
private Source source;
|
|
||||||
@State int sourceId;
|
|
||||||
|
|
||||||
private String query;
|
|
||||||
|
|
||||||
private RxPager<Manga> pager;
|
|
||||||
private MangasPage lastMangasPage;
|
|
||||||
|
|
||||||
private PublishSubject<List<Manga>> mangaDetailSubject;
|
|
||||||
|
|
||||||
private boolean isListMode;
|
|
||||||
|
|
||||||
private static final int GET_MANGA_LIST = 1;
|
|
||||||
private static final int GET_MANGA_DETAIL = 2;
|
|
||||||
private static final int GET_MANGA_PAGE = 3;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onCreate(Bundle savedState) {
|
|
||||||
super.onCreate(savedState);
|
|
||||||
|
|
||||||
if (savedState != null) {
|
|
||||||
source = sourceManager.get(sourceId);
|
|
||||||
}
|
|
||||||
|
|
||||||
sources = sourceManager.getSources();
|
|
||||||
|
|
||||||
mangaDetailSubject = PublishSubject.create();
|
|
||||||
|
|
||||||
pager = new RxPager<>();
|
|
||||||
|
|
||||||
startableReplay(GET_MANGA_LIST,
|
|
||||||
pager::results,
|
|
||||||
(view, pair) -> view.onAddPage(pair.first, pair.second));
|
|
||||||
|
|
||||||
startableFirst(GET_MANGA_PAGE,
|
|
||||||
() -> pager.request(page -> getMangasPageObservable(page + 1)),
|
|
||||||
(view, next) -> {},
|
|
||||||
(view, error) -> view.onAddPageError(error));
|
|
||||||
|
|
||||||
startableLatestCache(GET_MANGA_DETAIL,
|
|
||||||
() -> mangaDetailSubject
|
|
||||||
.observeOn(Schedulers.io())
|
|
||||||
.flatMap(Observable::from)
|
|
||||||
.filter(manga -> !manga.initialized)
|
|
||||||
.concatMap(this::getMangaDetails)
|
|
||||||
.onBackpressureBuffer()
|
|
||||||
.observeOn(AndroidSchedulers.mainThread()),
|
|
||||||
CatalogueFragment::updateImage,
|
|
||||||
(view, error) -> Timber.e(error.getMessage()));
|
|
||||||
|
|
||||||
add(prefs.catalogueAsList().asObservable()
|
|
||||||
.subscribe(this::setDisplayMode));
|
|
||||||
}
|
|
||||||
|
|
||||||
private void setDisplayMode(boolean asList) {
|
|
||||||
this.isListMode = asList;
|
|
||||||
if (asList) {
|
|
||||||
stop(GET_MANGA_DETAIL);
|
|
||||||
} else {
|
|
||||||
start(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_PAGE);
|
|
||||||
lastMangasPage = null;
|
|
||||||
|
|
||||||
if (!isListMode) {
|
|
||||||
start(GET_MANGA_DETAIL);
|
|
||||||
}
|
|
||||||
start(GET_MANGA_LIST);
|
|
||||||
start(GET_MANGA_PAGE);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void requestNext() {
|
|
||||||
if (hasNextPage()) {
|
|
||||||
start(GET_MANGA_PAGE);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private Observable<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()
|
|
||||||
.doOnNext(this::initializeMangas)
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void initializeMangas(List<Manga> mangas) {
|
|
||||||
mangaDetailSubject.onNext(mangas);
|
|
||||||
}
|
|
||||||
|
|
||||||
private Observable<Manga> getMangaDetails(final Manga manga) {
|
|
||||||
return source.pullMangaFromNetwork(manga.url)
|
|
||||||
.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 int getLastUsedSourceIndex() {
|
|
||||||
int index = prefs.lastUsedCatalogueSource().get();
|
|
||||||
if (index < 0 || index >= sources.size() || !isValidSource(sources.get(index))) {
|
|
||||||
return findFirstValidSource();
|
|
||||||
}
|
|
||||||
return index;
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isValidSource(Source source) {
|
|
||||||
if (!source.isLoginRequired() || source.isLogged())
|
|
||||||
return true;
|
|
||||||
|
|
||||||
return !(prefs.getSourceUsername(source).equals("")
|
|
||||||
|| prefs.getSourcePassword(source).equals(""));
|
|
||||||
}
|
|
||||||
|
|
||||||
public int findFirstValidSource() {
|
|
||||||
for (int i = 0; i < sources.size(); i++) {
|
|
||||||
if (isValidSource(sources.get(i))) {
|
|
||||||
return i;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setEnabledSource(int index) {
|
|
||||||
prefs.lastUsedCatalogueSource().set(index);
|
|
||||||
}
|
|
||||||
|
|
||||||
public List<Source> getEnabledSources() {
|
|
||||||
// TODO filter by enabled source
|
|
||||||
return sourceManager.getSources();
|
|
||||||
}
|
|
||||||
|
|
||||||
public void changeMangaFavorite(Manga manga) {
|
|
||||||
manga.favorite = !manga.favorite;
|
|
||||||
db.insertManga(manga).executeAsBlocking();
|
|
||||||
}
|
|
||||||
|
|
||||||
public boolean isListMode() {
|
|
||||||
return isListMode;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void swapDisplayMode() {
|
|
||||||
prefs.catalogueAsList().set(!isListMode);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
|
@ -0,0 +1,336 @@
|
||||||
|
package eu.kanade.tachiyomi.ui.catalogue
|
||||||
|
|
||||||
|
import android.os.Bundle
|
||||||
|
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 rx.Observable
|
||||||
|
import rx.android.schedulers.AndroidSchedulers
|
||||||
|
import rx.schedulers.Schedulers
|
||||||
|
import rx.subjects.PublishSubject
|
||||||
|
import timber.log.Timber
|
||||||
|
import javax.inject.Inject
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Presenter of [CatalogueFragment].
|
||||||
|
*/
|
||||||
|
class CataloguePresenter : BasePresenter<CatalogueFragment>() {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Source manager.
|
||||||
|
*/
|
||||||
|
@Inject lateinit var sourceManager: SourceManager
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Database.
|
||||||
|
*/
|
||||||
|
@Inject lateinit var db: DatabaseHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cover cache.
|
||||||
|
*/
|
||||||
|
@Inject lateinit var coverCache: CoverCache
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Preferences.
|
||||||
|
*/
|
||||||
|
@Inject lateinit var prefs: PreferencesHelper
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enabled sources.
|
||||||
|
*/
|
||||||
|
private val sources by lazy { sourceManager.sources }
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Active source.
|
||||||
|
*/
|
||||||
|
lateinit var source: Source
|
||||||
|
private set
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Query from the view.
|
||||||
|
*/
|
||||||
|
private var query: String? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pager containing a list of manga results.
|
||||||
|
*/
|
||||||
|
private lateinit var pager: RxPager<Manga>
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Last fetched page from network.
|
||||||
|
*/
|
||||||
|
private var lastMangasPage: MangasPage? = null
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Subject that initializes a list of manga.
|
||||||
|
*/
|
||||||
|
private val mangaDetailSubject = PublishSubject.create<List<Manga>>()
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the view is in list mode or not.
|
||||||
|
*/
|
||||||
|
var isListMode: Boolean = false
|
||||||
|
private set
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
/**
|
||||||
|
* Id of the restartable that delivers a list of manga from network.
|
||||||
|
*/
|
||||||
|
const val GET_MANGA_LIST = 1
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of the restartable that requests the list of manga from network.
|
||||||
|
*/
|
||||||
|
const val GET_MANGA_PAGE = 2
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Id of the restartable that initializes the details of a manga.
|
||||||
|
*/
|
||||||
|
const val GET_MANGA_DETAIL = 3
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Key to save and restore [source] from a [Bundle].
|
||||||
|
*/
|
||||||
|
const val ACTIVE_SOURCE_KEY = "active_source"
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onCreate(savedState: Bundle?) {
|
||||||
|
super.onCreate(savedState)
|
||||||
|
|
||||||
|
if (savedState != null) {
|
||||||
|
source = sourceManager.get(savedState.getInt(ACTIVE_SOURCE_KEY))!!
|
||||||
|
}
|
||||||
|
|
||||||
|
pager = RxPager()
|
||||||
|
|
||||||
|
startableReplay(GET_MANGA_LIST,
|
||||||
|
{ pager.results() },
|
||||||
|
{ view, pair -> view.onAddPage(pair.first, pair.second) })
|
||||||
|
|
||||||
|
startableFirst(GET_MANGA_PAGE,
|
||||||
|
{ pager.request { page -> getMangasPageObservable(page + 1) } },
|
||||||
|
{ view, next -> },
|
||||||
|
{ view, error -> view.onAddPageError(error) })
|
||||||
|
|
||||||
|
startableLatestCache(GET_MANGA_DETAIL,
|
||||||
|
{ mangaDetailSubject.observeOn(Schedulers.io())
|
||||||
|
.flatMap { Observable.from(it) }
|
||||||
|
.filter { !it.initialized }
|
||||||
|
.concatMap { getMangaDetailsObservable(it) }
|
||||||
|
.onBackpressureBuffer()
|
||||||
|
.observeOn(AndroidSchedulers.mainThread()) },
|
||||||
|
{ view, manga -> view.onMangaInitialized(manga) },
|
||||||
|
{ view, error -> Timber.e(error.message) })
|
||||||
|
|
||||||
|
add(prefs.catalogueAsList().asObservable()
|
||||||
|
.subscribe { setDisplayMode(it) })
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onSave(state: Bundle) {
|
||||||
|
state.putInt(ACTIVE_SOURCE_KEY, source.id)
|
||||||
|
super.onSave(state)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the display mode.
|
||||||
|
*
|
||||||
|
* @param asList whether the current mode is in list or not.
|
||||||
|
*/
|
||||||
|
private fun setDisplayMode(asList: Boolean) {
|
||||||
|
isListMode = asList
|
||||||
|
if (asList) {
|
||||||
|
stop(GET_MANGA_DETAIL)
|
||||||
|
} else {
|
||||||
|
start(GET_MANGA_DETAIL)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Starts the request with the given source.
|
||||||
|
*
|
||||||
|
* @param source the active source.
|
||||||
|
*/
|
||||||
|
fun startRequesting(source: Source) {
|
||||||
|
this.source = source
|
||||||
|
restartRequest(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Restarts the request for the active source with a query.
|
||||||
|
*
|
||||||
|
* @param query a query, or null if searching popular manga.
|
||||||
|
*/
|
||||||
|
fun restartRequest(query: String?) {
|
||||||
|
this.query = query
|
||||||
|
stop(GET_MANGA_PAGE)
|
||||||
|
lastMangasPage = null
|
||||||
|
|
||||||
|
if (!isListMode) {
|
||||||
|
start(GET_MANGA_DETAIL)
|
||||||
|
}
|
||||||
|
start(GET_MANGA_LIST)
|
||||||
|
start(GET_MANGA_PAGE)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Requests the next page for the active pager.
|
||||||
|
*/
|
||||||
|
fun requestNext() {
|
||||||
|
if (hasNextPage()) {
|
||||||
|
start(GET_MANGA_PAGE)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the observable of the network request for a page.
|
||||||
|
*
|
||||||
|
* @param page the page number to request.
|
||||||
|
* @return an observable of the network request.
|
||||||
|
*/
|
||||||
|
private fun getMangasPageObservable(page: Int): Observable<List<Manga>> {
|
||||||
|
val nextMangasPage = MangasPage(page)
|
||||||
|
if (page != 1) {
|
||||||
|
nextMangasPage.url = lastMangasPage!!.nextPageUrl
|
||||||
|
}
|
||||||
|
|
||||||
|
val obs = if (query.isNullOrEmpty())
|
||||||
|
source.pullPopularMangasFromNetwork(nextMangasPage)
|
||||||
|
else
|
||||||
|
source.searchMangasFromNetwork(nextMangasPage, query)
|
||||||
|
|
||||||
|
return obs.subscribeOn(Schedulers.io())
|
||||||
|
.doOnNext { lastMangasPage = it }
|
||||||
|
.flatMap { Observable.from(it.mangas) }
|
||||||
|
.map { networkToLocalManga(it) }
|
||||||
|
.toList()
|
||||||
|
.doOnNext { initializeMangas(it) }
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a manga from the database for the given manga from network. It creates a new entry
|
||||||
|
* if the manga is not yet in the database.
|
||||||
|
*
|
||||||
|
* @param networkManga the manga from network.
|
||||||
|
* @return a manga from the database.
|
||||||
|
*/
|
||||||
|
private fun networkToLocalManga(networkManga: Manga): Manga {
|
||||||
|
var localManga = db.getManga(networkManga.url, source.id).executeAsBlocking()
|
||||||
|
if (localManga == null) {
|
||||||
|
val result = db.insertManga(networkManga).executeAsBlocking()
|
||||||
|
networkManga.id = result.insertedId()
|
||||||
|
localManga = networkManga
|
||||||
|
}
|
||||||
|
return localManga
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize a list of manga.
|
||||||
|
*
|
||||||
|
* @param mangas the list of manga to initialize.
|
||||||
|
*/
|
||||||
|
fun initializeMangas(mangas: List<Manga>) {
|
||||||
|
mangaDetailSubject.onNext(mangas)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns an observable of manga that initializes the given manga.
|
||||||
|
*
|
||||||
|
* @param manga the manga to initialize.
|
||||||
|
* @return an observable of the manga to initialize
|
||||||
|
*/
|
||||||
|
private fun getMangaDetailsObservable(manga: Manga): Observable<Manga> {
|
||||||
|
return source.pullMangaFromNetwork(manga.url)
|
||||||
|
.flatMap { networkManga ->
|
||||||
|
manga.copyFrom(networkManga)
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
Observable.just(manga)
|
||||||
|
}
|
||||||
|
.onErrorResumeNext { Observable.just(manga) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the last fetched page has a next page.
|
||||||
|
*/
|
||||||
|
fun hasNextPage(): Boolean {
|
||||||
|
return lastMangasPage?.nextPageUrl != null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the last used source from preferences, or the first valid source.
|
||||||
|
*
|
||||||
|
* @return the index of the last used source.
|
||||||
|
*/
|
||||||
|
fun getLastUsedSourceIndex(): Int {
|
||||||
|
val index = prefs.lastUsedCatalogueSource().get() ?: -1
|
||||||
|
if (index < 0 || index >= sources.size || !isValidSource(sources[index])) {
|
||||||
|
return findFirstValidSource()
|
||||||
|
}
|
||||||
|
return index
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the given source is valid.
|
||||||
|
*
|
||||||
|
* @param source the source to check.
|
||||||
|
* @return true if the source is valid, false otherwise.
|
||||||
|
*/
|
||||||
|
fun isValidSource(source: Source): Boolean = with(source) {
|
||||||
|
if (!isLoginRequired || isLogged)
|
||||||
|
return true
|
||||||
|
|
||||||
|
prefs.getSourceUsername(this) != "" && prefs.getSourcePassword(this) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds the first valid source.
|
||||||
|
*
|
||||||
|
* @return the index of the first valid source.
|
||||||
|
*/
|
||||||
|
fun findFirstValidSource(): Int {
|
||||||
|
return sources.indexOfFirst { isValidSource(it) }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sets the enabled source.
|
||||||
|
*
|
||||||
|
* @param index the index of the source in [sources].
|
||||||
|
*/
|
||||||
|
fun setEnabledSource(index: Int) {
|
||||||
|
prefs.lastUsedCatalogueSource().set(index)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a list of enabled sources.
|
||||||
|
*
|
||||||
|
* TODO filter by enabled sources.
|
||||||
|
*/
|
||||||
|
fun getEnabledSources(): List<Source> {
|
||||||
|
return sourceManager.sources
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or removes a manga from the library.
|
||||||
|
*
|
||||||
|
* @param manga the manga to update.
|
||||||
|
*/
|
||||||
|
fun changeMangaFavorite(manga: Manga) {
|
||||||
|
manga.favorite = !manga.favorite
|
||||||
|
db.insertManga(manga).executeAsBlocking()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Changes the active display mode.
|
||||||
|
*/
|
||||||
|
fun swapDisplayMode() {
|
||||||
|
prefs.catalogueAsList().set(!isListMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -225,7 +225,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
|
||||||
*/
|
*/
|
||||||
private fun setCategories(categories: List<Category>) {
|
private fun setCategories(categories: List<Category>) {
|
||||||
adapter.categories = categories
|
adapter.categories = categories
|
||||||
tabs.setTabsFromPagerAdapter(adapter)
|
tabs.setupWithViewPager(view_pager)
|
||||||
tabs.visibility = if (categories.size <= 1) View.GONE else View.VISIBLE
|
tabs.visibility = if (categories.size <= 1) View.GONE else View.VISIBLE
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -3,9 +3,11 @@ package eu.kanade.tachiyomi.ui.setting
|
||||||
import android.app.Activity
|
import android.app.Activity
|
||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
|
import android.os.Environment
|
||||||
import android.support.v7.widget.RecyclerView
|
import android.support.v7.widget.RecyclerView
|
||||||
import android.view.View
|
import android.view.View
|
||||||
import android.view.ViewGroup
|
import android.view.ViewGroup
|
||||||
|
import com.afollestad.materialdialogs.MaterialDialog
|
||||||
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
|
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
|
||||||
import com.nononsenseapps.filepicker.FilePickerActivity
|
import com.nononsenseapps.filepicker.FilePickerActivity
|
||||||
import com.nononsenseapps.filepicker.FilePickerFragment
|
import com.nononsenseapps.filepicker.FilePickerFragment
|
||||||
|
@ -29,24 +31,54 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedState: Bundle?) {
|
||||||
downloadDirPref.setOnPreferenceClickListener { preference ->
|
downloadDirPref.setOnPreferenceClickListener {
|
||||||
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
|
val externalDirs = getExternalFilesDirs()
|
||||||
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
|
val selectedIndex = externalDirs.indexOf(File(preferences.downloadsDirectory))
|
||||||
i.putExtra(FilePickerActivity.EXTRA_MODE, FilePickerActivity.MODE_DIR)
|
|
||||||
i.putExtra(FilePickerActivity.EXTRA_START_PATH, preferences.downloadsDirectory)
|
MaterialDialog.Builder(activity)
|
||||||
|
.items(externalDirs + getString(R.string.custom_dir))
|
||||||
|
.itemsCallbackSingleChoice(selectedIndex, { dialog, view, which, text ->
|
||||||
|
if (which == externalDirs.size) {
|
||||||
|
// Custom dir selected, open directory selector
|
||||||
|
val i = Intent(activity, CustomLayoutPickerActivity::class.java)
|
||||||
|
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.downloadsDirectory)
|
||||||
|
|
||||||
|
startActivityForResult(i, DOWNLOAD_DIR_CODE)
|
||||||
|
} else {
|
||||||
|
// One of the predefined folders was selected
|
||||||
|
preferences.downloadsDirectory = text.toString()
|
||||||
|
updateDownloadsDir()
|
||||||
|
}
|
||||||
|
true
|
||||||
|
})
|
||||||
|
.show()
|
||||||
|
|
||||||
startActivityForResult(i, DOWNLOAD_DIR_CODE)
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onResume() {
|
override fun onResume() {
|
||||||
super.onResume()
|
super.onResume()
|
||||||
|
updateDownloadsDir()
|
||||||
|
}
|
||||||
|
|
||||||
|
fun updateDownloadsDir() {
|
||||||
downloadDirPref.summary = preferences.downloadsDirectory
|
downloadDirPref.summary = preferences.downloadsDirectory
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun getExternalFilesDirs(): List<File> {
|
||||||
|
val defaultDir = Environment.getExternalStorageDirectory().absolutePath +
|
||||||
|
File.separator + getString(R.string.app_name) +
|
||||||
|
File.separator + "downloads"
|
||||||
|
|
||||||
|
return mutableListOf(File(defaultDir)) + context.getExternalFilesDirs("")
|
||||||
|
}
|
||||||
|
|
||||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||||
if (data != null && requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) {
|
if (data != null && requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) {
|
||||||
preferences.downloadsDirectory = data.data.path
|
preferences.downloadsDirectory = data.data.path
|
||||||
|
|
|
@ -124,6 +124,7 @@
|
||||||
<string name="pref_download_directory">Downloads directory</string>
|
<string name="pref_download_directory">Downloads directory</string>
|
||||||
<string name="pref_download_slots">Simultaneous downloads</string>
|
<string name="pref_download_slots">Simultaneous downloads</string>
|
||||||
<string name="pref_download_only_over_wifi">Only download over Wi-Fi</string>
|
<string name="pref_download_only_over_wifi">Only download over Wi-Fi</string>
|
||||||
|
<string name="custom_dir">Custom directory</string>
|
||||||
|
|
||||||
<!-- Advanced section -->
|
<!-- Advanced section -->
|
||||||
<string name="pref_clear_chapter_cache">Clear chapter cache</string>
|
<string name="pref_clear_chapter_cache">Clear chapter cache</string>
|
||||||
|
|
|
@ -1,2 +1,2 @@
|
||||||
include ':app', ':SubsamplingScaleImageView', ':ReactiveNetwork'
|
include ':app', ':SubsamplingScaleImageView'
|
||||||
project(':SubsamplingScaleImageView').projectDir = new File('libs/SubsamplingScaleImageView')
|
project(':SubsamplingScaleImageView').projectDir = new File('libs/SubsamplingScaleImageView')
|
||||||
|
|
Reference in a new issue