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 {
|
||||
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,8 +31,17 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
|
|||
}
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
downloadDirPref.setOnPreferenceClickListener { preference ->
|
||||
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)
|
||||
|
@ -38,15 +49,36 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
|
|||
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()
|
||||
|
||||
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 a new issue