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:
len 2016-03-01 23:29:07 +01:00
parent fabdba4452
commit ee4bf163ef
18 changed files with 1046 additions and 747 deletions

View file

@ -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'

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,2 +1,2 @@
include ':app', ':SubsamplingScaleImageView', ':ReactiveNetwork'
include ':app', ':SubsamplingScaleImageView'
project(':SubsamplingScaleImageView').projectDir = new File('libs/SubsamplingScaleImageView')