Rename project
This commit is contained in:
@@ -0,0 +1,137 @@
|
||||
package eu.kanade.tachiyomi.ui.manga;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.design.widget.TabLayout;
|
||||
import android.support.v4.app.Fragment;
|
||||
import android.support.v4.app.FragmentManager;
|
||||
import android.support.v4.app.FragmentPagerAdapter;
|
||||
import android.support.v4.view.ViewPager;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.App;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper;
|
||||
import eu.kanade.tachiyomi.ui.base.activity.BaseRxActivity;
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersFragment;
|
||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoFragment;
|
||||
import eu.kanade.tachiyomi.ui.manga.myanimelist.MyAnimeListFragment;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
|
||||
@RequiresPresenter(MangaPresenter.class)
|
||||
public class MangaActivity extends BaseRxActivity<MangaPresenter> {
|
||||
|
||||
@Bind(R.id.toolbar) Toolbar toolbar;
|
||||
@Bind(R.id.tabs) TabLayout tabs;
|
||||
@Bind(R.id.view_pager) ViewPager view_pager;
|
||||
|
||||
@Inject PreferencesHelper preferences;
|
||||
@Inject MangaSyncManager mangaSyncManager;
|
||||
|
||||
private MangaDetailAdapter adapter;
|
||||
private long manga_id;
|
||||
private boolean is_online;
|
||||
|
||||
public final static String MANGA_ID = "manga_id";
|
||||
public final static String MANGA_ONLINE = "manga_online";
|
||||
|
||||
public static Intent newIntent(Context context, Manga manga) {
|
||||
Intent intent = new Intent(context, MangaActivity.class);
|
||||
intent.putExtra(MANGA_ID, manga.id);
|
||||
return intent;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
App.get(this).getComponent().inject(this);
|
||||
setContentView(R.layout.activity_manga);
|
||||
ButterKnife.bind(this);
|
||||
|
||||
setupToolbar(toolbar);
|
||||
|
||||
Intent intent = getIntent();
|
||||
|
||||
manga_id = intent.getLongExtra(MANGA_ID, -1);
|
||||
is_online = intent.getBooleanExtra(MANGA_ONLINE, false);
|
||||
|
||||
setupViewPager();
|
||||
|
||||
if (savedState == null)
|
||||
getPresenter().queryManga(manga_id);
|
||||
}
|
||||
|
||||
private void setupViewPager() {
|
||||
adapter = new MangaDetailAdapter(getSupportFragmentManager(), this);
|
||||
|
||||
view_pager.setAdapter(adapter);
|
||||
tabs.setupWithViewPager(view_pager);
|
||||
|
||||
if (!is_online)
|
||||
view_pager.setCurrentItem(MangaDetailAdapter.CHAPTERS_FRAGMENT);
|
||||
}
|
||||
|
||||
public void setManga(Manga manga) {
|
||||
setToolbarTitle(manga.title);
|
||||
}
|
||||
|
||||
public boolean isCatalogueManga() {
|
||||
return is_online;
|
||||
}
|
||||
|
||||
class MangaDetailAdapter extends FragmentPagerAdapter {
|
||||
|
||||
private int pageCount;
|
||||
private String tabTitles[];
|
||||
|
||||
final static int INFO_FRAGMENT = 0;
|
||||
final static int CHAPTERS_FRAGMENT = 1;
|
||||
final static int MYANIMELIST_FRAGMENT = 2;
|
||||
|
||||
public MangaDetailAdapter(FragmentManager fm, Context context) {
|
||||
super(fm);
|
||||
tabTitles = new String[]{
|
||||
context.getString(R.string.manga_detail_tab),
|
||||
context.getString(R.string.manga_chapters_tab),
|
||||
"MAL"
|
||||
};
|
||||
|
||||
pageCount = 2;
|
||||
if (!is_online && mangaSyncManager.getMyAnimeList().isLogged())
|
||||
pageCount++;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return pageCount;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
switch (position) {
|
||||
case INFO_FRAGMENT:
|
||||
return MangaInfoFragment.newInstance();
|
||||
case CHAPTERS_FRAGMENT:
|
||||
return ChaptersFragment.newInstance();
|
||||
case MYANIMELIST_FRAGMENT:
|
||||
return MyAnimeListFragment.newInstance();
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public CharSequence getPageTitle(int position) {
|
||||
// Generate title based on item position
|
||||
return tabTitles[position];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package eu.kanade.tachiyomi.ui.manga;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import icepick.State;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
public class MangaPresenter extends BasePresenter<MangaActivity> {
|
||||
|
||||
@Inject DatabaseHelper db;
|
||||
|
||||
@State long mangaId;
|
||||
|
||||
private static final int DB_MANGA = 1;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
restartableLatestCache(DB_MANGA, this::getDbMangaObservable, MangaActivity::setManga);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
// Avoid new instances receiving wrong manga
|
||||
EventBus.getDefault().removeStickyEvent(Manga.class);
|
||||
}
|
||||
|
||||
private Observable<Manga> getDbMangaObservable() {
|
||||
return db.getManga(mangaId).createObservable()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(manga -> EventBus.getDefault().postSticky(manga));
|
||||
}
|
||||
|
||||
public void queryManga(long mangaId) {
|
||||
this.mangaId = mangaId;
|
||||
start(DB_MANGA);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter;
|
||||
|
||||
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.Chapter;
|
||||
|
||||
public class ChaptersAdapter extends FlexibleAdapter<ChaptersHolder, Chapter> {
|
||||
|
||||
private ChaptersFragment fragment;
|
||||
|
||||
public ChaptersAdapter(ChaptersFragment fragment) {
|
||||
this.fragment = fragment;
|
||||
mItems = new ArrayList<>();
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDataSet(String param) {}
|
||||
|
||||
@Override
|
||||
public ChaptersHolder onCreateViewHolder(ViewGroup parent, int viewType) {
|
||||
View v = LayoutInflater.from(fragment.getActivity()).inflate(R.layout.item_chapter, parent, false);
|
||||
return new ChaptersHolder(v, this, fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ChaptersHolder holder, int position) {
|
||||
final Chapter chapter = getItem(position);
|
||||
holder.onSetValues(fragment.getActivity(), chapter);
|
||||
|
||||
//When user scrolls this bind the correct selection status
|
||||
holder.itemView.setActivated(isSelected(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
return mItems.get(position).id;
|
||||
}
|
||||
|
||||
public void setItems(List<Chapter> chapters) {
|
||||
mItems = chapters;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
public ChaptersFragment getFragment() {
|
||||
return fragment;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,350 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter;
|
||||
|
||||
import android.content.Intent;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.Nullable;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.support.v7.view.ActionMode;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.CheckBox;
|
||||
import android.widget.ImageView;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.download.DownloadService;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
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.manga.MangaActivity;
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
import rx.Observable;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
@RequiresPresenter(ChaptersPresenter.class)
|
||||
public class ChaptersFragment extends BaseRxFragment<ChaptersPresenter> implements
|
||||
ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener {
|
||||
|
||||
@Bind(R.id.chapter_list) RecyclerView recyclerView;
|
||||
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
|
||||
@Bind(R.id.toolbar_bottom) ViewGroup toolbarBottom;
|
||||
|
||||
@Bind(R.id.action_sort) ImageView sortBtn;
|
||||
@Bind(R.id.action_next_unread) ImageView nextUnreadBtn;
|
||||
@Bind(R.id.action_show_unread) CheckBox readCb;
|
||||
@Bind(R.id.action_show_downloaded) CheckBox downloadedCb;
|
||||
|
||||
private ChaptersAdapter adapter;
|
||||
private LinearLayoutManager linearLayout;
|
||||
private ActionMode actionMode;
|
||||
|
||||
private Subscription downloadProgressSubscription;
|
||||
|
||||
public static ChaptersFragment newInstance() {
|
||||
return new ChaptersFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
// Inflate the layout for this fragment
|
||||
View view = inflater.inflate(R.layout.fragment_manga_chapters, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
// Init RecyclerView and adapter
|
||||
linearLayout = new LinearLayoutManager(getActivity());
|
||||
recyclerView.setLayoutManager(linearLayout);
|
||||
recyclerView.addItemDecoration(new DividerItemDecoration(ContextCompat.getDrawable(getContext(), R.drawable.line_divider)));
|
||||
recyclerView.setHasFixedSize(true);
|
||||
adapter = new ChaptersAdapter(this);
|
||||
recyclerView.setAdapter(adapter);
|
||||
|
||||
// Set initial values
|
||||
setReadFilter();
|
||||
setDownloadedFilter();
|
||||
setSortIcon();
|
||||
|
||||
// Init listeners
|
||||
swipeRefresh.setOnRefreshListener(this::fetchChapters);
|
||||
readCb.setOnCheckedChangeListener((arg, isChecked) ->
|
||||
getPresenter().setReadFilter(isChecked));
|
||||
downloadedCb.setOnCheckedChangeListener((v, isChecked) ->
|
||||
getPresenter().setDownloadedFilter(isChecked));
|
||||
sortBtn.setOnClickListener(v -> {
|
||||
getPresenter().revertSortOrder();
|
||||
setSortIcon();
|
||||
});
|
||||
nextUnreadBtn.setOnClickListener(v -> {
|
||||
Chapter chapter = getPresenter().getNextUnreadChapter();
|
||||
if (chapter != null) {
|
||||
openChapter(chapter);
|
||||
} else {
|
||||
ToastUtil.showShort(getContext(), R.string.no_next_chapter);
|
||||
}
|
||||
});
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
observeChapterDownloadProgress();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
unsubscribeChapterDownloadProgress();
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
public void onNextChapters(List<Chapter> chapters) {
|
||||
// If the list is empty, fetch chapters from source if the conditions are met
|
||||
// We use presenter chapters instead because they are always unfiltered
|
||||
if (getPresenter().getChapters().isEmpty())
|
||||
initialFetchChapters();
|
||||
|
||||
destroyActionModeIfNeeded();
|
||||
adapter.setItems(chapters);
|
||||
}
|
||||
|
||||
private void initialFetchChapters() {
|
||||
// Only fetch if this view is from the catalog and it hasn't requested previously
|
||||
if (isCatalogueManga() && !getPresenter().hasRequested()) {
|
||||
fetchChapters();
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchChapters() {
|
||||
if (getPresenter().getManga() != null) {
|
||||
swipeRefresh.setRefreshing(true);
|
||||
getPresenter().fetchChaptersFromSource();
|
||||
}
|
||||
}
|
||||
|
||||
public void onFetchChaptersDone() {
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchChaptersError() {
|
||||
swipeRefresh.setRefreshing(false);
|
||||
ToastUtil.showShort(getContext(), R.string.fetch_chapters_error);
|
||||
}
|
||||
|
||||
public boolean isCatalogueManga() {
|
||||
return ((MangaActivity) getActivity()).isCatalogueManga();
|
||||
}
|
||||
|
||||
protected void openChapter(Chapter chapter) {
|
||||
getPresenter().onOpenChapter(chapter);
|
||||
Intent intent = ReaderActivity.newIntent(getActivity());
|
||||
startActivity(intent);
|
||||
}
|
||||
|
||||
private void observeChapterDownloadProgress() {
|
||||
downloadProgressSubscription = getPresenter().getDownloadProgressObs()
|
||||
.subscribe(this::onDownloadProgressChange,
|
||||
error -> { /* TODO getting a NPE sometimes on 'manga' from presenter */ });
|
||||
}
|
||||
|
||||
private void unsubscribeChapterDownloadProgress() {
|
||||
if (downloadProgressSubscription != null)
|
||||
downloadProgressSubscription.unsubscribe();
|
||||
}
|
||||
|
||||
private void onDownloadProgressChange(Download download) {
|
||||
ChaptersHolder holder = getHolder(download.chapter);
|
||||
if (holder != null)
|
||||
holder.onProgressChange(getContext(), download.downloadedImages, download.pages.size());
|
||||
}
|
||||
|
||||
public void onChapterStatusChange(Chapter chapter) {
|
||||
ChaptersHolder holder = getHolder(chapter);
|
||||
if (holder != null)
|
||||
holder.onStatusChange(chapter.status);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
private ChaptersHolder getHolder(Chapter chapter) {
|
||||
return (ChaptersHolder) recyclerView.findViewHolderForItemId(chapter.id);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.chapter_selection, menu);
|
||||
adapter.setMode(ChaptersAdapter.MODE_MULTI);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.action_select_all:
|
||||
return onSelectAll();
|
||||
case R.id.action_mark_as_read:
|
||||
return onMarkAsRead(getSelectedChapters());
|
||||
case R.id.action_mark_as_unread:
|
||||
return onMarkAsUnread(getSelectedChapters());
|
||||
case R.id.action_download:
|
||||
return onDownload(getSelectedChapters());
|
||||
case R.id.action_delete:
|
||||
return onDelete(getSelectedChapters());
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
adapter.setMode(ChaptersAdapter.MODE_SINGLE);
|
||||
adapter.clearSelection();
|
||||
actionMode = null;
|
||||
}
|
||||
|
||||
private Observable<Chapter> getSelectedChapters() {
|
||||
// Create a blocking copy of the selected chapters.
|
||||
// When the action mode is closed the list is cleared. If we use background
|
||||
// threads with this observable, some emissions could be lost.
|
||||
List<Chapter> chapters = Observable.from(adapter.getSelectedItems())
|
||||
.map(adapter::getItem).toList().toBlocking().single();
|
||||
|
||||
return Observable.from(chapters);
|
||||
}
|
||||
|
||||
public void destroyActionModeIfNeeded() {
|
||||
if (actionMode != null) {
|
||||
actionMode.finish();
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean onSelectAll() {
|
||||
adapter.selectAll();
|
||||
setContextTitle(adapter.getSelectedItemCount());
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean onMarkAsRead(Observable<Chapter> chapters) {
|
||||
getPresenter().markChaptersRead(chapters, true);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean onMarkAsUnread(Observable<Chapter> chapters) {
|
||||
getPresenter().markChaptersRead(chapters, false);
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean onDownload(Observable<Chapter> chapters) {
|
||||
DownloadService.start(getActivity());
|
||||
|
||||
Observable<Chapter> observable = chapters
|
||||
.doOnCompleted(adapter::notifyDataSetChanged);
|
||||
|
||||
getPresenter().downloadChapters(observable);
|
||||
destroyActionModeIfNeeded();
|
||||
return true;
|
||||
}
|
||||
|
||||
protected boolean onDelete(Observable<Chapter> chapters) {
|
||||
int size = adapter.getSelectedItemCount();
|
||||
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.deleting)
|
||||
.progress(false, size, true)
|
||||
.cancelable(false)
|
||||
.show();
|
||||
|
||||
Observable<Chapter> observable = chapters
|
||||
.concatMap(chapter -> {
|
||||
getPresenter().deleteChapter(chapter);
|
||||
return Observable.just(chapter);
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext(chapter -> {
|
||||
dialog.incrementProgress(1);
|
||||
chapter.status = Download.NOT_DOWNLOADED;
|
||||
})
|
||||
.doOnCompleted(adapter::notifyDataSetChanged)
|
||||
.finallyDo(dialog::dismiss);
|
||||
|
||||
getPresenter().deleteChapters(observable);
|
||||
destroyActionModeIfNeeded();
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onListItemClick(int position) {
|
||||
if (actionMode != null && adapter.getMode() == ChaptersAdapter.MODE_MULTI) {
|
||||
toggleSelection(position);
|
||||
return true;
|
||||
} else {
|
||||
openChapter(adapter.getItem(position));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onListItemLongClick(int position) {
|
||||
if (actionMode == null)
|
||||
actionMode = getBaseActivity().startSupportActionMode(this);
|
||||
|
||||
toggleSelection(position);
|
||||
}
|
||||
|
||||
private void toggleSelection(int position) {
|
||||
adapter.toggleSelection(position, false);
|
||||
|
||||
int count = adapter.getSelectedItemCount();
|
||||
if (count == 0) {
|
||||
actionMode.finish();
|
||||
} else {
|
||||
setContextTitle(count);
|
||||
actionMode.invalidate();
|
||||
}
|
||||
}
|
||||
|
||||
private void setContextTitle(int count) {
|
||||
actionMode.setTitle(getString(R.string.label_selected, count));
|
||||
}
|
||||
|
||||
public void setSortIcon() {
|
||||
if (sortBtn != null) {
|
||||
boolean aToZ = getPresenter().getSortOrder();
|
||||
sortBtn.setImageResource(!aToZ ? R.drawable.ic_expand_less_white_36dp : R.drawable.ic_expand_more_white_36dp);
|
||||
}
|
||||
}
|
||||
|
||||
public void setReadFilter() {
|
||||
if (readCb != null) {
|
||||
readCb.setChecked(getPresenter().getReadFilter());
|
||||
}
|
||||
}
|
||||
|
||||
public void setDownloadedFilter() {
|
||||
if (downloadedCb != null) {
|
||||
downloadedCb.setChecked(getPresenter().getDownloadedFilter());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter;
|
||||
|
||||
import android.content.Context;
|
||||
import android.support.v4.content.ContextCompat;
|
||||
import android.view.View;
|
||||
import android.widget.PopupMenu;
|
||||
import android.widget.RelativeLayout;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder;
|
||||
import rx.Observable;
|
||||
|
||||
public class ChaptersHolder extends FlexibleViewHolder {
|
||||
|
||||
private final ChaptersAdapter adapter;
|
||||
private Chapter item;
|
||||
|
||||
@Bind(R.id.chapter_title) TextView title;
|
||||
@Bind(R.id.download_text) TextView downloadText;
|
||||
@Bind(R.id.chapter_menu) RelativeLayout chapterMenu;
|
||||
@Bind(R.id.chapter_pages) TextView pages;
|
||||
@Bind(R.id.chapter_date) TextView date;
|
||||
|
||||
SimpleDateFormat sdf = new SimpleDateFormat("dd/MM/yyyy");
|
||||
|
||||
public ChaptersHolder(View view, ChaptersAdapter adapter, OnListItemClickListener listener) {
|
||||
super(view, adapter, listener);
|
||||
this.adapter = adapter;
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
chapterMenu.setOnClickListener(v -> v.post(() -> showPopupMenu(v)));
|
||||
}
|
||||
|
||||
public void onSetValues(Context context, Chapter chapter) {
|
||||
this.item = chapter;
|
||||
title.setText(chapter.name);
|
||||
|
||||
if (chapter.read) {
|
||||
title.setTextColor(ContextCompat.getColor(context, R.color.hint_text));
|
||||
} else {
|
||||
title.setTextColor(ContextCompat.getColor(context, R.color.primary_text));
|
||||
}
|
||||
|
||||
if (!chapter.read && chapter.last_page_read > 0) {
|
||||
pages.setText(context.getString(R.string.chapter_progress, chapter.last_page_read + 1));
|
||||
} else {
|
||||
pages.setText("");
|
||||
}
|
||||
|
||||
onStatusChange(chapter.status);
|
||||
date.setText(sdf.format(new Date(chapter.date_upload)));
|
||||
}
|
||||
|
||||
public void onStatusChange(int status) {
|
||||
switch (status) {
|
||||
case Download.QUEUE:
|
||||
downloadText.setText(R.string.chapter_queued); break;
|
||||
case Download.DOWNLOADING:
|
||||
downloadText.setText(R.string.chapter_downloading); break;
|
||||
case Download.DOWNLOADED:
|
||||
downloadText.setText(R.string.chapter_downloaded); break;
|
||||
case Download.ERROR:
|
||||
downloadText.setText(R.string.chapter_error); break;
|
||||
default:
|
||||
downloadText.setText(""); break;
|
||||
}
|
||||
}
|
||||
|
||||
public void onProgressChange(Context context, int downloaded, int total) {
|
||||
downloadText.setText(context.getString(
|
||||
R.string.chapter_downloading_progress, downloaded, total));
|
||||
}
|
||||
|
||||
private void showPopupMenu(View view) {
|
||||
// Create a PopupMenu, giving it the clicked view for an anchor
|
||||
PopupMenu popup = new PopupMenu(adapter.getFragment().getActivity(), view);
|
||||
|
||||
// Inflate our menu resource into the PopupMenu's Menu
|
||||
popup.getMenuInflater().inflate(R.menu.chapter_single, popup.getMenu());
|
||||
|
||||
// Set a listener so we are notified if a menu item is clicked
|
||||
popup.setOnMenuItemClickListener(menuItem -> {
|
||||
Observable<Chapter> chapter = Observable.just(item);
|
||||
|
||||
switch (menuItem.getItemId()) {
|
||||
case R.id.action_mark_as_read:
|
||||
return adapter.getFragment().onMarkAsRead(chapter);
|
||||
case R.id.action_mark_as_unread:
|
||||
return adapter.getFragment().onMarkAsUnread(chapter);
|
||||
case R.id.action_download:
|
||||
return adapter.getFragment().onDownload(chapter);
|
||||
case R.id.action_delete:
|
||||
return adapter.getFragment().onDelete(chapter);
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
||||
// Finally show the PopupMenu
|
||||
popup.show();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,275 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.util.Pair;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import de.greenrobot.event.EventBus;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Chapter;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager;
|
||||
import eu.kanade.tachiyomi.data.download.model.Download;
|
||||
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.event.ChapterCountEvent;
|
||||
import eu.kanade.tachiyomi.event.DownloadChaptersEvent;
|
||||
import eu.kanade.tachiyomi.event.ReaderEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
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 ChaptersPresenter extends BasePresenter<ChaptersFragment> {
|
||||
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject SourceManager sourceManager;
|
||||
@Inject PreferencesHelper preferences;
|
||||
@Inject DownloadManager downloadManager;
|
||||
|
||||
private Manga manga;
|
||||
private Source source;
|
||||
private List<Chapter> chapters;
|
||||
private boolean sortOrderAToZ = true;
|
||||
private boolean onlyUnread = true;
|
||||
private boolean onlyDownloaded;
|
||||
@State boolean hasRequested;
|
||||
|
||||
private PublishSubject<List<Chapter>> chaptersSubject;
|
||||
|
||||
private static final int DB_CHAPTERS = 1;
|
||||
private static final int FETCH_CHAPTERS = 2;
|
||||
private static final int CHAPTER_STATUS_CHANGES = 3;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
}
|
||||
|
||||
chaptersSubject = PublishSubject.create();
|
||||
|
||||
restartableLatestCache(DB_CHAPTERS,
|
||||
this::getDbChaptersObs,
|
||||
ChaptersFragment::onNextChapters);
|
||||
|
||||
restartableFirst(FETCH_CHAPTERS,
|
||||
this::getOnlineChaptersObs,
|
||||
(view, result) -> view.onFetchChaptersDone(),
|
||||
(view, error) -> view.onFetchChaptersError());
|
||||
|
||||
restartableLatestCache(CHAPTER_STATUS_CHANGES,
|
||||
this::getChapterStatusObs,
|
||||
(view, download) -> view.onChapterStatusChange(download.chapter),
|
||||
(view, error) -> Timber.e(error.getCause(), error.getMessage()));
|
||||
|
||||
registerForStickyEvents();
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
stop(DB_CHAPTERS);
|
||||
stop(FETCH_CHAPTERS);
|
||||
stop(CHAPTER_STATUS_CHANGES);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterForEvents();
|
||||
EventBus.getDefault().removeStickyEvent(ChapterCountEvent.class);
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(Manga manga) {
|
||||
this.manga = manga;
|
||||
|
||||
if (!isSubscribed(DB_CHAPTERS)) {
|
||||
source = sourceManager.get(manga.source);
|
||||
start(DB_CHAPTERS);
|
||||
|
||||
add(db.getChapters(manga).createObservable()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.doOnNext(chapters -> {
|
||||
this.chapters = chapters;
|
||||
EventBus.getDefault().postSticky(new ChapterCountEvent(chapters.size()));
|
||||
for (Chapter chapter : chapters) {
|
||||
setChapterStatus(chapter);
|
||||
}
|
||||
start(CHAPTER_STATUS_CHANGES);
|
||||
})
|
||||
.subscribe(chaptersSubject::onNext));
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchChaptersFromSource() {
|
||||
hasRequested = true;
|
||||
start(FETCH_CHAPTERS);
|
||||
}
|
||||
|
||||
private void refreshChapters() {
|
||||
chaptersSubject.onNext(chapters);
|
||||
}
|
||||
|
||||
private Observable<Pair<Integer, Integer>> getOnlineChaptersObs() {
|
||||
return source.pullChaptersFromNetwork(manga.url)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters))
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
private Observable<List<Chapter>> getDbChaptersObs() {
|
||||
return chaptersSubject.flatMap(this::applyChapterFilters)
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
private Observable<List<Chapter>> applyChapterFilters(List<Chapter> chapters) {
|
||||
Observable<Chapter> observable = Observable.from(chapters)
|
||||
.subscribeOn(Schedulers.io());
|
||||
if (onlyUnread) {
|
||||
observable = observable.filter(chapter -> !chapter.read);
|
||||
}
|
||||
if (onlyDownloaded) {
|
||||
observable = observable.filter(chapter -> chapter.status == Download.DOWNLOADED);
|
||||
}
|
||||
return observable.toSortedList((chapter, chapter2) -> sortOrderAToZ ?
|
||||
Float.compare(chapter2.chapter_number, chapter.chapter_number) :
|
||||
Float.compare(chapter.chapter_number, chapter2.chapter_number));
|
||||
}
|
||||
|
||||
private void setChapterStatus(Chapter chapter) {
|
||||
for (Download download : downloadManager.getQueue()) {
|
||||
if (chapter.id.equals(download.chapter.id)) {
|
||||
chapter.status = download.getStatus();
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (downloadManager.isChapterDownloaded(source, manga, chapter)) {
|
||||
chapter.status = Download.DOWNLOADED;
|
||||
} else {
|
||||
chapter.status = Download.NOT_DOWNLOADED;
|
||||
}
|
||||
}
|
||||
|
||||
private Observable<Download> getChapterStatusObs() {
|
||||
return downloadManager.getQueue().getStatusObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter(download -> download.manga.id.equals(manga.id))
|
||||
.doOnNext(this::updateChapterStatus);
|
||||
}
|
||||
|
||||
public void updateChapterStatus(Download download) {
|
||||
for (Chapter chapter : chapters) {
|
||||
if (download.chapter.id.equals(chapter.id)) {
|
||||
chapter.status = download.getStatus();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (onlyDownloaded && download.getStatus() == Download.DOWNLOADED)
|
||||
refreshChapters();
|
||||
}
|
||||
|
||||
public Observable<Download> getDownloadProgressObs() {
|
||||
return downloadManager.getQueue().getProgressObservable()
|
||||
.filter(download -> download.manga.id.equals(manga.id))
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public void onOpenChapter(Chapter chapter) {
|
||||
EventBus.getDefault().postSticky(new ReaderEvent(source, manga, chapter));
|
||||
}
|
||||
|
||||
public Chapter getNextUnreadChapter() {
|
||||
return db.getNextUnreadChapter(manga).executeAsBlocking();
|
||||
}
|
||||
|
||||
public void markChaptersRead(Observable<Chapter> selectedChapters, boolean read) {
|
||||
add(selectedChapters
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map(chapter -> {
|
||||
chapter.read = read;
|
||||
if (!read) chapter.last_page_read = 0;
|
||||
return chapter;
|
||||
})
|
||||
.toList()
|
||||
.flatMap(chapters -> db.insertChapters(chapters).createObservable())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe());
|
||||
}
|
||||
|
||||
public void downloadChapters(Observable<Chapter> selectedChapters) {
|
||||
add(selectedChapters
|
||||
.toList()
|
||||
.subscribe(chapters -> {
|
||||
EventBus.getDefault().postSticky(new DownloadChaptersEvent(manga, chapters));
|
||||
}));
|
||||
}
|
||||
|
||||
public void deleteChapters(Observable<Chapter> selectedChapters) {
|
||||
add(selectedChapters
|
||||
.subscribe(chapter -> {
|
||||
downloadManager.getQueue().remove(chapter);
|
||||
}, error -> {
|
||||
Timber.e(error.getMessage());
|
||||
}, () -> {
|
||||
if (onlyDownloaded)
|
||||
refreshChapters();
|
||||
}));
|
||||
}
|
||||
|
||||
public void deleteChapter(Chapter chapter) {
|
||||
downloadManager.deleteChapter(source, manga, chapter);
|
||||
}
|
||||
|
||||
public void revertSortOrder() {
|
||||
//TODO manga.chapter_order
|
||||
sortOrderAToZ = !sortOrderAToZ;
|
||||
refreshChapters();
|
||||
}
|
||||
|
||||
public void setReadFilter(boolean onlyUnread) {
|
||||
//TODO do we need save filter for manga?
|
||||
this.onlyUnread = onlyUnread;
|
||||
refreshChapters();
|
||||
}
|
||||
|
||||
public void setDownloadedFilter(boolean onlyDownloaded) {
|
||||
this.onlyDownloaded = onlyDownloaded;
|
||||
refreshChapters();
|
||||
}
|
||||
|
||||
public boolean getSortOrder() {
|
||||
return sortOrderAToZ;
|
||||
}
|
||||
|
||||
public boolean getReadFilter() {
|
||||
return onlyUnread;
|
||||
}
|
||||
|
||||
public boolean getDownloadedFilter() {
|
||||
return onlyDownloaded;
|
||||
}
|
||||
|
||||
public Manga getManga() {
|
||||
return manga;
|
||||
}
|
||||
|
||||
public List<Chapter> getChapters() {
|
||||
return chapters;
|
||||
}
|
||||
|
||||
public boolean hasRequested() {
|
||||
return hasRequested;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.info;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.Button;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.bumptech.glide.load.model.LazyHeaders;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
|
||||
@RequiresPresenter(MangaInfoPresenter.class)
|
||||
public class MangaInfoFragment extends BaseRxFragment<MangaInfoPresenter> {
|
||||
|
||||
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
|
||||
|
||||
@Bind(R.id.manga_artist) TextView artist;
|
||||
@Bind(R.id.manga_author) TextView author;
|
||||
@Bind(R.id.manga_chapters) TextView chapterCount;
|
||||
@Bind(R.id.manga_genres) TextView genres;
|
||||
@Bind(R.id.manga_status) TextView status;
|
||||
@Bind(R.id.manga_summary) TextView description;
|
||||
@Bind(R.id.manga_cover) ImageView cover;
|
||||
|
||||
@Bind(R.id.action_favorite) Button favoriteBtn;
|
||||
|
||||
|
||||
public static MangaInfoFragment newInstance() {
|
||||
return new MangaInfoFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container,
|
||||
Bundle savedInstanceState) {
|
||||
// Inflate the layout for this fragment
|
||||
View view = inflater.inflate(R.layout.fragment_manga_info, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
favoriteBtn.setOnClickListener(v -> {
|
||||
getPresenter().toggleFavorite();
|
||||
});
|
||||
swipeRefresh.setOnRefreshListener(this::fetchMangaFromSource);
|
||||
|
||||
return view;
|
||||
}
|
||||
|
||||
public void onNextManga(Manga manga) {
|
||||
if (manga.initialized) {
|
||||
setMangaInfo(manga);
|
||||
} else {
|
||||
// Initialize manga
|
||||
fetchMangaFromSource();
|
||||
}
|
||||
}
|
||||
|
||||
private void setMangaInfo(Manga manga) {
|
||||
artist.setText(manga.artist);
|
||||
author.setText(manga.author);
|
||||
genres.setText(manga.genre);
|
||||
status.setText(manga.getStatus(getActivity()));
|
||||
description.setText(manga.description);
|
||||
|
||||
setFavoriteText(manga.favorite);
|
||||
|
||||
CoverCache coverCache = getPresenter().coverCache;
|
||||
LazyHeaders headers = getPresenter().source.getGlideHeaders();
|
||||
if (manga.thumbnail_url != null && cover.getDrawable() == null) {
|
||||
if (manga.favorite) {
|
||||
coverCache.saveAndLoadFromCache(cover, manga.thumbnail_url, headers);
|
||||
} else {
|
||||
coverCache.loadFromNetwork(cover, manga.thumbnail_url, headers);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void setChapterCount(int count) {
|
||||
chapterCount.setText(String.valueOf(count));
|
||||
}
|
||||
|
||||
public void setFavoriteText(boolean isFavorite) {
|
||||
favoriteBtn.setText(!isFavorite ? R.string.add_to_library : R.string.remove_from_library);
|
||||
}
|
||||
|
||||
private void fetchMangaFromSource() {
|
||||
setRefreshing(true);
|
||||
getPresenter().fetchMangaFromSource();
|
||||
}
|
||||
|
||||
public void onFetchMangaDone() {
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onFetchMangaError() {
|
||||
setRefreshing(false);
|
||||
}
|
||||
|
||||
private void setRefreshing(boolean value) {
|
||||
swipeRefresh.setRefreshing(value);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,119 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.info;
|
||||
|
||||
import android.os.Bundle;
|
||||
|
||||
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.source.SourceManager;
|
||||
import eu.kanade.tachiyomi.data.source.base.Source;
|
||||
import eu.kanade.tachiyomi.event.ChapterCountEvent;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
|
||||
public class MangaInfoPresenter extends BasePresenter<MangaInfoFragment> {
|
||||
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject SourceManager sourceManager;
|
||||
@Inject CoverCache coverCache;
|
||||
|
||||
private Manga manga;
|
||||
protected Source source;
|
||||
private int count = -1;
|
||||
|
||||
private boolean isFetching;
|
||||
|
||||
private static final int GET_MANGA = 1;
|
||||
private static final int GET_CHAPTER_COUNT = 2;
|
||||
private static final int FETCH_MANGA_INFO = 3;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
}
|
||||
|
||||
restartableLatestCache(GET_MANGA,
|
||||
() -> Observable.just(manga),
|
||||
MangaInfoFragment::onNextManga);
|
||||
|
||||
restartableLatestCache(GET_CHAPTER_COUNT,
|
||||
() -> Observable.just(count),
|
||||
MangaInfoFragment::setChapterCount);
|
||||
|
||||
restartableFirst(FETCH_MANGA_INFO,
|
||||
this::fetchMangaObs,
|
||||
(view, manga) -> view.onFetchMangaDone(),
|
||||
(view, error) -> view.onFetchMangaError());
|
||||
|
||||
registerForStickyEvents();
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
stop(GET_MANGA);
|
||||
stop(GET_CHAPTER_COUNT);
|
||||
stop(FETCH_MANGA_INFO);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
unregisterForEvents();
|
||||
super.onDestroy();
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(Manga manga) {
|
||||
this.manga = manga;
|
||||
source = sourceManager.get(manga.source);
|
||||
start(GET_MANGA);
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(ChapterCountEvent event) {
|
||||
if (count != event.getCount()) {
|
||||
count = event.getCount();
|
||||
start(GET_CHAPTER_COUNT);
|
||||
}
|
||||
}
|
||||
|
||||
public void fetchMangaFromSource() {
|
||||
if (!isFetching) {
|
||||
isFetching = true;
|
||||
start(FETCH_MANGA_INFO);
|
||||
}
|
||||
}
|
||||
|
||||
private Observable<Manga> fetchMangaObs() {
|
||||
return source.pullMangaFromNetwork(manga.url)
|
||||
.flatMap(networkManga -> {
|
||||
manga.copyFrom(networkManga);
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
return Observable.just(manga);
|
||||
})
|
||||
.finallyDo(() -> isFetching = false)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread());
|
||||
}
|
||||
|
||||
public void toggleFavorite() {
|
||||
manga.favorite = !manga.favorite;
|
||||
onMangaFavoriteChange(manga.favorite);
|
||||
db.insertManga(manga).executeAsBlocking();
|
||||
}
|
||||
|
||||
private void onMangaFavoriteChange(boolean isFavorite) {
|
||||
if (isFavorite) {
|
||||
coverCache.save(manga.thumbnail_url, source.getGlideHeaders());
|
||||
} else {
|
||||
coverCache.delete(manga.thumbnail_url);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+172
@@ -0,0 +1,172 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.myanimelist;
|
||||
|
||||
import android.app.Dialog;
|
||||
import android.os.Bundle;
|
||||
import android.support.annotation.NonNull;
|
||||
import android.support.v4.app.DialogFragment;
|
||||
import android.text.Editable;
|
||||
import android.text.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
import android.view.View;
|
||||
import android.widget.EditText;
|
||||
import android.widget.ListView;
|
||||
import android.widget.ProgressBar;
|
||||
import android.widget.TextView;
|
||||
|
||||
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.MangaSync;
|
||||
import rx.Subscription;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.subjects.PublishSubject;
|
||||
import uk.co.ribot.easyadapter.EasyAdapter;
|
||||
import uk.co.ribot.easyadapter.ItemViewHolder;
|
||||
import uk.co.ribot.easyadapter.PositionInfo;
|
||||
import uk.co.ribot.easyadapter.annotations.LayoutId;
|
||||
import uk.co.ribot.easyadapter.annotations.ViewId;
|
||||
|
||||
public class MyAnimeListDialogFragment extends DialogFragment {
|
||||
|
||||
@Bind(R.id.myanimelist_search_field) EditText searchText;
|
||||
@Bind(R.id.myanimelist_search_results) ListView searchResults;
|
||||
@Bind(R.id.progress) ProgressBar progressBar;
|
||||
|
||||
private EasyAdapter<MangaSync> adapter;
|
||||
private MangaSync selectedItem;
|
||||
|
||||
private Subscription searchSubscription;
|
||||
|
||||
public static MyAnimeListDialogFragment newInstance() {
|
||||
return new MyAnimeListDialogFragment();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedState) {
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
|
||||
.customView(R.layout.dialog_myanimelist_search, false)
|
||||
.positiveText(R.string.button_ok)
|
||||
.negativeText(R.string.button_cancel)
|
||||
.onPositive((dialog1, which) -> onPositiveButtonClick())
|
||||
.build();
|
||||
|
||||
ButterKnife.bind(this, dialog.getView());
|
||||
|
||||
// Create adapter
|
||||
adapter = new EasyAdapter<>(getActivity(), ResultViewHolder.class);
|
||||
searchResults.setAdapter(adapter);
|
||||
|
||||
// Set listeners
|
||||
searchResults.setOnItemClickListener((parent, viewList, position, id) ->
|
||||
selectedItem = adapter.getItem(position));
|
||||
|
||||
// Do an initial search based on the manga's title
|
||||
if (savedState == null) {
|
||||
String title = getPresenter().manga.title;
|
||||
searchText.append(title);
|
||||
search(title);
|
||||
}
|
||||
|
||||
return dialog;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onResume() {
|
||||
super.onResume();
|
||||
PublishSubject<String> querySubject = PublishSubject.create();
|
||||
searchText.addTextChangedListener(new SimpleTextChangeListener() {
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
querySubject.onNext(s.toString());
|
||||
}
|
||||
});
|
||||
|
||||
// Listen to text changes
|
||||
searchSubscription = querySubject.debounce(1, TimeUnit.SECONDS)
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(this::search);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPause() {
|
||||
if (searchSubscription != null) {
|
||||
searchSubscription.unsubscribe();
|
||||
}
|
||||
super.onPause();
|
||||
}
|
||||
|
||||
private void onPositiveButtonClick() {
|
||||
if (adapter != null && selectedItem != null) {
|
||||
getPresenter().registerManga(selectedItem);
|
||||
}
|
||||
}
|
||||
|
||||
private void search(String query) {
|
||||
if (!TextUtils.isEmpty(query)) {
|
||||
searchResults.setVisibility(View.GONE);
|
||||
progressBar.setVisibility(View.VISIBLE);
|
||||
getPresenter().searchManga(query);
|
||||
}
|
||||
}
|
||||
|
||||
public void onSearchResults(List<MangaSync> results) {
|
||||
selectedItem = null;
|
||||
progressBar.setVisibility(View.GONE);
|
||||
searchResults.setVisibility(View.VISIBLE);
|
||||
adapter.setItems(results);
|
||||
}
|
||||
|
||||
public void onSearchResultsError() {
|
||||
progressBar.setVisibility(View.GONE);
|
||||
searchResults.setVisibility(View.VISIBLE);
|
||||
adapter.getItems().clear();
|
||||
}
|
||||
|
||||
public MyAnimeListFragment getMALFragment() {
|
||||
return (MyAnimeListFragment) getParentFragment();
|
||||
}
|
||||
|
||||
public MyAnimeListPresenter getPresenter() {
|
||||
return getMALFragment().getPresenter();
|
||||
}
|
||||
|
||||
@LayoutId(R.layout.dialog_myanimelist_search_item)
|
||||
public static class ResultViewHolder extends ItemViewHolder<MangaSync> {
|
||||
|
||||
@ViewId(R.id.myanimelist_result_title) TextView title;
|
||||
|
||||
public ResultViewHolder(View view) {
|
||||
super(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSetValues(MangaSync chapter, PositionInfo positionInfo) {
|
||||
title.setText(chapter.title);
|
||||
}
|
||||
}
|
||||
|
||||
private static class SimpleTextChangeListener implements TextWatcher {
|
||||
|
||||
@Override
|
||||
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onTextChanged(CharSequence s, int start, int before, int count) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void afterTextChanged(Editable s) {
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,181 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.myanimelist;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.widget.SwipeRefreshLayout;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.NumberPicker;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.afollestad.materialdialogs.MaterialDialog;
|
||||
|
||||
import java.text.DecimalFormat;
|
||||
import java.util.List;
|
||||
|
||||
import butterknife.Bind;
|
||||
import butterknife.ButterKnife;
|
||||
import butterknife.OnClick;
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment;
|
||||
import nucleus.factory.RequiresPresenter;
|
||||
|
||||
@RequiresPresenter(MyAnimeListPresenter.class)
|
||||
public class MyAnimeListFragment extends BaseRxFragment<MyAnimeListPresenter> {
|
||||
|
||||
@Bind(R.id.myanimelist_title) TextView title;
|
||||
@Bind(R.id.myanimelist_chapters) TextView chapters;
|
||||
@Bind(R.id.myanimelist_score) TextView score;
|
||||
@Bind(R.id.myanimelist_status) TextView status;
|
||||
@Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh;
|
||||
|
||||
private MyAnimeListDialogFragment dialog;
|
||||
|
||||
private DecimalFormat decimalFormat = new DecimalFormat("#.##");
|
||||
|
||||
private final static String SEARCH_FRAGMENT_TAG = "mal_search";
|
||||
|
||||
public static MyAnimeListFragment newInstance() {
|
||||
return new MyAnimeListFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
|
||||
View view = inflater.inflate(R.layout.fragment_myanimelist, container, false);
|
||||
ButterKnife.bind(this, view);
|
||||
|
||||
swipeRefresh.setEnabled(false);
|
||||
swipeRefresh.setOnRefreshListener(() -> getPresenter().refresh());
|
||||
return view;
|
||||
}
|
||||
|
||||
public void setMangaSync(MangaSync mangaSync) {
|
||||
swipeRefresh.setEnabled(mangaSync != null);
|
||||
if (mangaSync != null) {
|
||||
title.setText(mangaSync.title);
|
||||
chapters.setText(mangaSync.last_chapter_read + "/" +
|
||||
(mangaSync.total_chapters > 0 ? mangaSync.total_chapters : "-"));
|
||||
score.setText(mangaSync.score == 0 ? "-" : decimalFormat.format(mangaSync.score));
|
||||
status.setText(getPresenter().myAnimeList.getStatus(mangaSync.status));
|
||||
}
|
||||
}
|
||||
|
||||
public void onRefreshDone() {
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void onRefreshError() {
|
||||
swipeRefresh.setRefreshing(false);
|
||||
}
|
||||
|
||||
public void setSearchResults(List<MangaSync> results) {
|
||||
findSearchFragmentIfNeeded();
|
||||
|
||||
if (dialog != null) {
|
||||
dialog.onSearchResults(results);
|
||||
}
|
||||
}
|
||||
|
||||
public void setSearchResultsError() {
|
||||
findSearchFragmentIfNeeded();
|
||||
|
||||
if (dialog != null) {
|
||||
dialog.onSearchResultsError();
|
||||
}
|
||||
}
|
||||
|
||||
private void findSearchFragmentIfNeeded() {
|
||||
if (dialog == null) {
|
||||
dialog = (MyAnimeListDialogFragment) getChildFragmentManager()
|
||||
.findFragmentByTag(SEARCH_FRAGMENT_TAG);
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.myanimelist_title_layout)
|
||||
void onTitleClick() {
|
||||
if (dialog == null)
|
||||
dialog = MyAnimeListDialogFragment.newInstance();
|
||||
|
||||
getPresenter().restartSearch();
|
||||
dialog.show(getChildFragmentManager(), SEARCH_FRAGMENT_TAG);
|
||||
}
|
||||
|
||||
@OnClick(R.id.myanimelist_status_layout)
|
||||
void onStatusClick() {
|
||||
if (getPresenter().mangaSync == null)
|
||||
return;
|
||||
|
||||
Context ctx = getActivity();
|
||||
new MaterialDialog.Builder(ctx)
|
||||
.title(R.string.status)
|
||||
.items(getPresenter().getAllStatus(ctx))
|
||||
.itemsCallbackSingleChoice(getPresenter().getIndexFromStatus(),
|
||||
(materialDialog, view, i, charSequence) -> {
|
||||
getPresenter().setStatus(i);
|
||||
status.setText("...");
|
||||
return true;
|
||||
})
|
||||
.show();
|
||||
}
|
||||
|
||||
@OnClick(R.id.myanimelist_chapters_layout)
|
||||
void onChaptersClick() {
|
||||
if (getPresenter().mangaSync == null)
|
||||
return;
|
||||
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.chapters)
|
||||
.customView(R.layout.dialog_myanimelist_chapters, false)
|
||||
.positiveText(R.string.button_ok)
|
||||
.negativeText(R.string.button_cancel)
|
||||
.onPositive((materialDialog, dialogAction) -> {
|
||||
View view = materialDialog.getCustomView();
|
||||
if (view != null) {
|
||||
NumberPicker np = (NumberPicker) view.findViewById(R.id.chapters_picker);
|
||||
getPresenter().setLastChapterRead(np.getValue());
|
||||
chapters.setText("...");
|
||||
}
|
||||
})
|
||||
.show();
|
||||
|
||||
View view = dialog.getCustomView();
|
||||
if (view != null) {
|
||||
NumberPicker np = (NumberPicker) view.findViewById(R.id.chapters_picker);
|
||||
// Set initial value
|
||||
np.setValue(getPresenter().mangaSync.last_chapter_read);
|
||||
// Don't allow to go from 0 to 9999
|
||||
np.setWrapSelectorWheel(false);
|
||||
}
|
||||
}
|
||||
|
||||
@OnClick(R.id.myanimelist_score_layout)
|
||||
void onScoreClick() {
|
||||
if (getPresenter().mangaSync == null)
|
||||
return;
|
||||
|
||||
MaterialDialog dialog = new MaterialDialog.Builder(getActivity())
|
||||
.title(R.string.score)
|
||||
.customView(R.layout.dialog_myanimelist_score, false)
|
||||
.positiveText(R.string.button_ok)
|
||||
.negativeText(R.string.button_cancel)
|
||||
.onPositive((materialDialog, dialogAction) -> {
|
||||
View view = materialDialog.getCustomView();
|
||||
if (view != null) {
|
||||
NumberPicker np = (NumberPicker) view.findViewById(R.id.score_picker);
|
||||
getPresenter().setScore(np.getValue());
|
||||
score.setText("...");
|
||||
}
|
||||
})
|
||||
.show();
|
||||
|
||||
View view = dialog.getCustomView();
|
||||
if (view != null) {
|
||||
NumberPicker np = (NumberPicker) view.findViewById(R.id.score_picker);
|
||||
// Set initial value
|
||||
np.setValue((int) getPresenter().mangaSync.score);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.myanimelist;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Bundle;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import javax.inject.Inject;
|
||||
|
||||
import eu.kanade.tachiyomi.R;
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper;
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga;
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaSync;
|
||||
import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager;
|
||||
import eu.kanade.tachiyomi.data.mangasync.services.MyAnimeList;
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter;
|
||||
import eu.kanade.tachiyomi.util.EventBusHook;
|
||||
import eu.kanade.tachiyomi.util.ToastUtil;
|
||||
import rx.Observable;
|
||||
import rx.android.schedulers.AndroidSchedulers;
|
||||
import rx.schedulers.Schedulers;
|
||||
import timber.log.Timber;
|
||||
|
||||
public class MyAnimeListPresenter extends BasePresenter<MyAnimeListFragment> {
|
||||
|
||||
@Inject DatabaseHelper db;
|
||||
@Inject MangaSyncManager syncManager;
|
||||
|
||||
protected MyAnimeList myAnimeList;
|
||||
protected Manga manga;
|
||||
protected MangaSync mangaSync;
|
||||
|
||||
private String query;
|
||||
|
||||
private static final int GET_MANGA_SYNC = 1;
|
||||
private static final int GET_SEARCH_RESULTS = 2;
|
||||
private static final int REFRESH = 3;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedState) {
|
||||
super.onCreate(savedState);
|
||||
|
||||
if (savedState != null) {
|
||||
onProcessRestart();
|
||||
}
|
||||
|
||||
myAnimeList = syncManager.getMyAnimeList();
|
||||
|
||||
restartableLatestCache(GET_MANGA_SYNC,
|
||||
() -> db.getMangaSync(manga, myAnimeList).createObservable()
|
||||
.doOnNext(mangaSync -> this.mangaSync = mangaSync)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread()),
|
||||
MyAnimeListFragment::setMangaSync);
|
||||
|
||||
restartableLatestCache(GET_SEARCH_RESULTS,
|
||||
() -> myAnimeList.search(query)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread()),
|
||||
(view, results) -> {
|
||||
view.setSearchResults(results);
|
||||
}, (view, error) -> {
|
||||
Timber.e(error.getMessage());
|
||||
view.setSearchResultsError();
|
||||
});
|
||||
|
||||
restartableFirst(REFRESH,
|
||||
() -> myAnimeList.getList()
|
||||
.flatMap(myList -> {
|
||||
for (MangaSync myManga : myList) {
|
||||
if (myManga.remote_id == mangaSync.remote_id) {
|
||||
mangaSync.copyPersonalFrom(myManga);
|
||||
mangaSync.total_chapters = myManga.total_chapters;
|
||||
return Observable.just(mangaSync);
|
||||
}
|
||||
}
|
||||
return Observable.error(new Exception("Could not find manga"));
|
||||
})
|
||||
.flatMap(myManga -> db.insertMangaSync(myManga).createObservable())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread()),
|
||||
(view, result) -> view.onRefreshDone(),
|
||||
(view, error) -> view.onRefreshError());
|
||||
|
||||
}
|
||||
|
||||
private void onProcessRestart() {
|
||||
stop(GET_MANGA_SYNC);
|
||||
stop(GET_SEARCH_RESULTS);
|
||||
stop(REFRESH);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onTakeView(MyAnimeListFragment view) {
|
||||
super.onTakeView(view);
|
||||
registerForStickyEvents();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDropView() {
|
||||
unregisterForEvents();
|
||||
super.onDropView();
|
||||
}
|
||||
|
||||
@EventBusHook
|
||||
public void onEventMainThread(Manga manga) {
|
||||
this.manga = manga;
|
||||
start(GET_MANGA_SYNC);
|
||||
}
|
||||
|
||||
private void updateRemote() {
|
||||
add(myAnimeList.update(mangaSync)
|
||||
.flatMap(response -> db.insertMangaSync(mangaSync).createObservable())
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(next -> {},
|
||||
error -> {
|
||||
Timber.e(error.getMessage());
|
||||
// Restart on error to set old values
|
||||
start(GET_MANGA_SYNC);
|
||||
}
|
||||
));
|
||||
}
|
||||
|
||||
public void searchManga(String query) {
|
||||
if (TextUtils.isEmpty(query) || query.equals(this.query))
|
||||
return;
|
||||
|
||||
this.query = query;
|
||||
start(GET_SEARCH_RESULTS);
|
||||
}
|
||||
|
||||
public void restartSearch() {
|
||||
this.query = null;
|
||||
stop(GET_SEARCH_RESULTS);
|
||||
}
|
||||
|
||||
public void registerManga(MangaSync manga) {
|
||||
manga.manga_id = this.manga.id;
|
||||
add(myAnimeList.bind(manga)
|
||||
.flatMap(response -> {
|
||||
if (response.isSuccessful()) {
|
||||
return db.insertMangaSync(manga).createObservable();
|
||||
}
|
||||
return Observable.error(new Exception("Could not bind manga"));
|
||||
})
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe(manga2 -> {},
|
||||
error -> ToastUtil.showShort(getContext(), error.getMessage())));
|
||||
}
|
||||
|
||||
public String[] getAllStatus(Context context) {
|
||||
return new String[] {
|
||||
context.getString(R.string.reading),
|
||||
context.getString(R.string.completed),
|
||||
context.getString(R.string.on_hold),
|
||||
context.getString(R.string.dropped),
|
||||
context.getString(R.string.plan_to_read)
|
||||
};
|
||||
}
|
||||
|
||||
public int getIndexFromStatus() {
|
||||
return mangaSync.status == 6 ? 4 : mangaSync.status - 1;
|
||||
}
|
||||
|
||||
public void setStatus(int index) {
|
||||
mangaSync.status = index == 4 ? 6 : index + 1;
|
||||
updateRemote();
|
||||
}
|
||||
|
||||
public void setScore(int score) {
|
||||
mangaSync.score = score;
|
||||
updateRemote();
|
||||
}
|
||||
|
||||
public void setLastChapterRead(int chapterNumber) {
|
||||
mangaSync.last_chapter_read = chapterNumber;
|
||||
updateRemote();
|
||||
}
|
||||
|
||||
public void refresh() {
|
||||
if (mangaSync != null) {
|
||||
start(REFRESH);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user