mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	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:
		@@ -98,7 +98,7 @@ apt {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
dependencies {
 | 
			
		||||
    final SUPPORT_LIBRARY_VERSION = '23.1.1'
 | 
			
		||||
    final SUPPORT_LIBRARY_VERSION = '23.2.0'
 | 
			
		||||
    final DAGGER_VERSION = '2.0.2'
 | 
			
		||||
    final OKHTTP_VERSION = '3.2.0'
 | 
			
		||||
    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.util.DiskUtils;
 | 
			
		||||
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator;
 | 
			
		||||
import eu.kanade.tachiyomi.util.ToastUtil;
 | 
			
		||||
import eu.kanade.tachiyomi.util.UrlUtil;
 | 
			
		||||
import rx.Observable;
 | 
			
		||||
import rx.Subscription;
 | 
			
		||||
@@ -84,7 +85,11 @@ public class DownloadManager {
 | 
			
		||||
                    if (finished) {
 | 
			
		||||
                        DownloadService.stop(context);
 | 
			
		||||
                    }
 | 
			
		||||
                }, e -> DownloadService.stop(context));
 | 
			
		||||
                }, e -> {
 | 
			
		||||
                    DownloadService.stop(context);
 | 
			
		||||
                    Timber.e(e, e.getMessage());
 | 
			
		||||
                    ToastUtil.showShort(context, e.getMessage());
 | 
			
		||||
                });
 | 
			
		||||
 | 
			
		||||
        if (!isRunning) {
 | 
			
		||||
            isRunning = true;
 | 
			
		||||
@@ -410,7 +415,7 @@ public class DownloadManager {
 | 
			
		||||
        if (queue.isEmpty())
 | 
			
		||||
            return false;
 | 
			
		||||
 | 
			
		||||
        if (downloadsSubscription == null)
 | 
			
		||||
        if (downloadsSubscription == null || downloadsSubscription.isUnsubscribed())
 | 
			
		||||
            initializeSubscriptions();
 | 
			
		||||
 | 
			
		||||
        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>) {
 | 
			
		||||
        adapter.categories = categories
 | 
			
		||||
        tabs.setTabsFromPagerAdapter(adapter)
 | 
			
		||||
        tabs.setupWithViewPager(view_pager)
 | 
			
		||||
        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.content.Intent
 | 
			
		||||
import android.os.Bundle
 | 
			
		||||
import android.os.Environment
 | 
			
		||||
import android.support.v7.widget.RecyclerView
 | 
			
		||||
import android.view.View
 | 
			
		||||
import android.view.ViewGroup
 | 
			
		||||
import com.afollestad.materialdialogs.MaterialDialog
 | 
			
		||||
import com.nononsenseapps.filepicker.AbstractFilePickerFragment
 | 
			
		||||
import com.nononsenseapps.filepicker.FilePickerActivity
 | 
			
		||||
import com.nononsenseapps.filepicker.FilePickerFragment
 | 
			
		||||
@@ -29,24 +31,54 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
 | 
			
		||||
        downloadDirPref.setOnPreferenceClickListener { preference ->
 | 
			
		||||
            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)
 | 
			
		||||
    override fun onViewCreated(view: View, savedState: Bundle?) {
 | 
			
		||||
        downloadDirPref.setOnPreferenceClickListener {
 | 
			
		||||
 | 
			
		||||
            val externalDirs = getExternalFilesDirs()
 | 
			
		||||
            val selectedIndex = externalDirs.indexOf(File(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
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override fun onResume() {
 | 
			
		||||
        super.onResume()
 | 
			
		||||
        updateDownloadsDir()
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    fun updateDownloadsDir() {
 | 
			
		||||
        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?) {
 | 
			
		||||
        if (data != null && requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) {
 | 
			
		||||
            preferences.downloadsDirectory = data.data.path
 | 
			
		||||
 
 | 
			
		||||
@@ -124,6 +124,7 @@
 | 
			
		||||
    <string name="pref_download_directory">Downloads directory</string>
 | 
			
		||||
    <string name="pref_download_slots">Simultaneous downloads</string>
 | 
			
		||||
    <string name="pref_download_only_over_wifi">Only download over Wi-Fi</string>
 | 
			
		||||
    <string name="custom_dir">Custom directory</string>
 | 
			
		||||
 | 
			
		||||
      <!-- Advanced section -->
 | 
			
		||||
    <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')
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user