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 { dependencies {
final SUPPORT_LIBRARY_VERSION = '23.1.1' final SUPPORT_LIBRARY_VERSION = '23.2.0'
final DAGGER_VERSION = '2.0.2' final DAGGER_VERSION = '2.0.2'
final OKHTTP_VERSION = '3.2.0' final OKHTTP_VERSION = '3.2.0'
final RETROFIT_VERSION = '2.0.0-beta4' final RETROFIT_VERSION = '2.0.0-beta4'

View file

@ -26,6 +26,7 @@ import eu.kanade.tachiyomi.data.source.model.Page;
import eu.kanade.tachiyomi.event.DownloadChaptersEvent; import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
import eu.kanade.tachiyomi.util.DiskUtils; import eu.kanade.tachiyomi.util.DiskUtils;
import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator; import eu.kanade.tachiyomi.util.DynamicConcurrentMergeOperator;
import eu.kanade.tachiyomi.util.ToastUtil;
import eu.kanade.tachiyomi.util.UrlUtil; import eu.kanade.tachiyomi.util.UrlUtil;
import rx.Observable; import rx.Observable;
import rx.Subscription; import rx.Subscription;
@ -84,7 +85,11 @@ public class DownloadManager {
if (finished) { if (finished) {
DownloadService.stop(context); DownloadService.stop(context);
} }
}, e -> DownloadService.stop(context)); }, e -> {
DownloadService.stop(context);
Timber.e(e, e.getMessage());
ToastUtil.showShort(context, e.getMessage());
});
if (!isRunning) { if (!isRunning) {
isRunning = true; isRunning = true;
@ -410,7 +415,7 @@ public class DownloadManager {
if (queue.isEmpty()) if (queue.isEmpty())
return false; return false;
if (downloadsSubscription == null) if (downloadsSubscription == null || downloadsSubscription.isUnsubscribed())
initializeSubscriptions(); initializeSubscriptions();
final List<Download> pending = new ArrayList<>(); final List<Download> pending = new ArrayList<>();

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>) { private fun setCategories(categories: List<Category>) {
adapter.categories = categories adapter.categories = categories
tabs.setTabsFromPagerAdapter(adapter) tabs.setupWithViewPager(view_pager)
tabs.visibility = if (categories.size <= 1) View.GONE else View.VISIBLE tabs.visibility = if (categories.size <= 1) View.GONE else View.VISIBLE
} }

View file

@ -3,9 +3,11 @@ package eu.kanade.tachiyomi.ui.setting
import android.app.Activity import android.app.Activity
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.os.Environment
import android.support.v7.widget.RecyclerView import android.support.v7.widget.RecyclerView
import android.view.View import android.view.View
import android.view.ViewGroup import android.view.ViewGroup
import com.afollestad.materialdialogs.MaterialDialog
import com.nononsenseapps.filepicker.AbstractFilePickerFragment import com.nononsenseapps.filepicker.AbstractFilePickerFragment
import com.nononsenseapps.filepicker.FilePickerActivity import com.nononsenseapps.filepicker.FilePickerActivity
import com.nononsenseapps.filepicker.FilePickerFragment import com.nononsenseapps.filepicker.FilePickerFragment
@ -29,8 +31,17 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
} }
} }
override fun onViewCreated(view: View, savedInstanceState: Bundle?) { override fun onViewCreated(view: View, savedState: Bundle?) {
downloadDirPref.setOnPreferenceClickListener { preference -> 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) val i = Intent(activity, CustomLayoutPickerActivity::class.java)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false) i.putExtra(FilePickerActivity.EXTRA_ALLOW_MULTIPLE, false)
i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true) i.putExtra(FilePickerActivity.EXTRA_ALLOW_CREATE_DIR, true)
@ -38,15 +49,36 @@ class SettingsDownloadsFragment : SettingsNestedFragment() {
i.putExtra(FilePickerActivity.EXTRA_START_PATH, preferences.downloadsDirectory) i.putExtra(FilePickerActivity.EXTRA_START_PATH, preferences.downloadsDirectory)
startActivityForResult(i, DOWNLOAD_DIR_CODE) startActivityForResult(i, DOWNLOAD_DIR_CODE)
} else {
// One of the predefined folders was selected
preferences.downloadsDirectory = text.toString()
updateDownloadsDir()
}
true
})
.show()
true true
} }
} }
override fun onResume() { override fun onResume() {
super.onResume() super.onResume()
updateDownloadsDir()
}
fun updateDownloadsDir() {
downloadDirPref.summary = preferences.downloadsDirectory downloadDirPref.summary = preferences.downloadsDirectory
} }
fun getExternalFilesDirs(): List<File> {
val defaultDir = Environment.getExternalStorageDirectory().absolutePath +
File.separator + getString(R.string.app_name) +
File.separator + "downloads"
return mutableListOf(File(defaultDir)) + context.getExternalFilesDirs("")
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) { override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
if (data != null && requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) { if (data != null && requestCode == DOWNLOAD_DIR_CODE && resultCode == Activity.RESULT_OK) {
preferences.downloadsDirectory = data.data.path preferences.downloadsDirectory = data.data.path

View file

@ -124,6 +124,7 @@
<string name="pref_download_directory">Downloads directory</string> <string name="pref_download_directory">Downloads directory</string>
<string name="pref_download_slots">Simultaneous downloads</string> <string name="pref_download_slots">Simultaneous downloads</string>
<string name="pref_download_only_over_wifi">Only download over Wi-Fi</string> <string name="pref_download_only_over_wifi">Only download over Wi-Fi</string>
<string name="custom_dir">Custom directory</string>
<!-- Advanced section --> <!-- Advanced section -->
<string name="pref_clear_chapter_cache">Clear chapter cache</string> <string name="pref_clear_chapter_cache">Clear chapter cache</string>

View file

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