diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersAdapter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersAdapter.java index 3ff8d1c792..2f2fae2998 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersAdapter.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersAdapter.java @@ -16,13 +16,33 @@ import eu.davidea.flexibleadapter.FlexibleAdapter; import eu.kanade.tachiyomi.R; import eu.kanade.tachiyomi.data.database.models.MangaChapter; +/** + * Adapter of RecentChaptersHolder. + * Connection between Fragment and Holder + * Holder updates should be called from here. + */ public class RecentChaptersAdapter extends FlexibleAdapter { - private RecentChaptersFragment fragment; + /** + * Fragment of RecentChaptersFragment + */ + private final RecentChaptersFragment fragment; + /** + * The id of the view type + */ private static final int VIEW_TYPE_CHAPTER = 0; + + /** + * The id of the view type + */ private static final int VIEW_TYPE_SECTION = 1; + /** + * Constructor + * + * @param fragment fragment + */ public RecentChaptersAdapter(RecentChaptersFragment fragment) { this.fragment = fragment; setHasStableIds(true); @@ -37,6 +57,11 @@ public class RecentChaptersAdapter extends FlexibleAdapter items) { mItems = items; notifyDataSetChanged(); @@ -56,6 +81,8 @@ public class RecentChaptersAdapter extends FlexibleAdapter implements FlexibleViewHolder.OnListItemClickListener { @@ -50,14 +65,21 @@ public class RecentChaptersFragment extends BaseRxFragment chapters) { adapter.setItems(chapters); } @Override public boolean onListItemClick(int position) { + // Get item from position Object item = adapter.getItem(position); if (item instanceof MangaChapter) { + // Open chapter in reader openChapter((MangaChapter) item); } return false; @@ -65,11 +87,114 @@ public class RecentChaptersFragment extends BaseRxFragment chapters, Manga manga) { + // Start the download service. + DownloadService.start(getActivity()); + + // Refresh data on download competition. + Observable observable = chapters + .doOnCompleted(adapter::notifyDataSetChanged); + + // Download chapter. + getPresenter().downloadChapter(observable, manga); + return true; + } + + /** + * Start deleting chapter + * @param chapters selected chapters + * @param manga manga that belongs to chapter + * @return success of deletion. + */ + protected boolean onDelete(Observable chapters, Manga manga) { + int size = adapter.getSelectedItemCount(); + + MaterialDialog dialog = new MaterialDialog.Builder(getActivity()) + .title(R.string.deleting) + .progress(false, size, true) + .cancelable(false) + .show(); + + Observable observable = chapters + .concatMap(chapter -> { + getPresenter().deleteChapter(chapter, manga); + 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); + + return true; + } + + /** + * Mark chapter as read + * + * @param chapters selected chapter + * @return true + */ + @SuppressWarnings("SameReturnValue") + protected boolean onMarkAsRead(Observable chapters) { + getPresenter().markChaptersRead(chapters, true); + return true; + } + + /** + * Mark chapter as unread + * + * @param chapters selected chapter + * @return true + */ + @SuppressWarnings("SameReturnValue") + protected boolean onMarkAsUnread(Observable chapters) { + getPresenter().markChaptersRead(chapters, false); + return true; + } + + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersHolder.java b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersHolder.java index 93927e9fc0..7aa99ae0b1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersHolder.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersHolder.java @@ -1,35 +1,102 @@ package eu.kanade.tachiyomi.ui.recent; import android.support.v4.content.ContextCompat; +import android.view.Menu; import android.view.View; +import android.widget.PopupMenu; +import android.widget.RelativeLayout; import android.widget.TextView; import butterknife.Bind; import butterknife.ButterKnife; import eu.kanade.tachiyomi.R; +import eu.kanade.tachiyomi.data.database.models.Chapter; import eu.kanade.tachiyomi.data.database.models.MangaChapter; +import eu.kanade.tachiyomi.data.download.model.Download; import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder; +import rx.Observable; +/** + * Holder that contains chapter item + * Uses R.layout.item_recent_chapter. + * UI related actions should be called from here. + */ public class RecentChaptersHolder extends FlexibleViewHolder { + /** + * Adapter for recent chapters + */ + private final RecentChaptersAdapter adapter; + + /** + * TextView containing chapter title + */ @Bind(R.id.chapter_title) TextView chapterTitle; + + /** + * TextView containing manga name + */ @Bind(R.id.manga_title) TextView mangaTitle; + /** + * TextView containing download status + */ + @Bind(R.id.download_text) TextView downloadText; + + /** + * RelativeLayout containing popup menu with download options + */ + @Bind(R.id.chapter_menu) RelativeLayout chapterMenu; + + /** + * Color of read chapter + */ private final int readColor; + + /** + * Color of unread chapter + */ private final int unreadColor; + /** + * Object containing chapter information + */ + private MangaChapter mangaChapter; + + /** + * Constructor of RecentChaptersHolder + * @param view view of ChapterHolder + * @param adapter adapter of ChapterHolder + * @param onListItemClickListener ClickListener + */ public RecentChaptersHolder(View view, RecentChaptersAdapter adapter, OnListItemClickListener onListItemClickListener) { super(view, adapter, onListItemClickListener); + this.adapter = adapter; ButterKnife.bind(this, view); + // Set colors. readColor = ContextCompat.getColor(view.getContext(), R.color.hint_text); unreadColor = ContextCompat.getColor(view.getContext(), R.color.primary_text); + + //Set OnClickListener for download menu + chapterMenu.setOnClickListener(v -> v.post(() -> showPopupMenu(v))); } + /** + * Set values of view + * + * @param item item containing chapter information + */ public void onSetValues(MangaChapter item) { + this.mangaChapter = item; + + // Set chapter title chapterTitle.setText(item.chapter.name); + + // Set manga title mangaTitle.setText(item.manga.title); + // Check if chapter is read and set correct color if (item.chapter.read) { chapterTitle.setTextColor(readColor); mangaTitle.setTextColor(readColor); @@ -37,6 +104,84 @@ public class RecentChaptersHolder extends FlexibleViewHolder { chapterTitle.setTextColor(unreadColor); mangaTitle.setTextColor(unreadColor); } + + // Set chapter status + onStatusChange(item.chapter.status); } + /** + * Updates chapter status in view. + * + * @param status download status + */ + 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; + } + } + + /** + * Show pop up menu + * @param view view containing popup menu. + */ + 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_recent, popup.getMenu()); + + // Hide download and show delete if the chapter is downloaded and + if (mangaChapter.chapter.isDownloaded()) { + Menu menu = popup.getMenu(); + menu.findItem(R.id.action_download).setVisible(false); + menu.findItem(R.id.action_delete).setVisible(true); + } + + // Hide mark as unread when the chapter is unread + if (!mangaChapter.chapter.read /*&& mangaChapter.chapter.last_page_read == 0*/) { + popup.getMenu().findItem(R.id.action_mark_as_unread).setVisible(false); + } + + // Hide mark as read when the chapter is read + if (mangaChapter.chapter.read) { + popup.getMenu().findItem(R.id.action_mark_as_read).setVisible(false); + } + + // Set a listener so we are notified if a menu item is clicked + popup.setOnMenuItemClickListener(menuItem -> { + Observable chapterObservable = Observable.just(mangaChapter.chapter); + + switch (menuItem.getItemId()) { + case R.id.action_download: + return adapter.getFragment().onDownload(chapterObservable, mangaChapter.manga); + case R.id.action_delete: + return adapter.getFragment().onDelete(chapterObservable, mangaChapter.manga); + case R.id.action_mark_as_read: + return adapter.getFragment().onMarkAsRead(chapterObservable); + case R.id.action_mark_as_unread: + return adapter.getFragment().onMarkAsUnread(chapterObservable); + } + return false; + }); + + // Finally show the PopupMenu + popup.show(); + } + + } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersPresenter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersPresenter.java index f991ee93b9..898744cca5 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersPresenter.java +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/recent/RecentChaptersPresenter.java @@ -15,46 +15,184 @@ import java.util.TreeMap; import javax.inject.Inject; 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.database.models.MangaChapter; +import eu.kanade.tachiyomi.data.download.DownloadManager; +import eu.kanade.tachiyomi.data.download.model.Download; import eu.kanade.tachiyomi.data.source.SourceManager; import eu.kanade.tachiyomi.data.source.base.Source; +import eu.kanade.tachiyomi.event.DownloadChaptersEvent; import eu.kanade.tachiyomi.event.ReaderEvent; import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; import rx.Observable; import rx.android.schedulers.AndroidSchedulers; +import rx.schedulers.Schedulers; +import timber.log.Timber; +/** + * Presenter of RecentChaptersFragment. + * Contains information and data for fragment. + * Observable updates should be called from here. + */ public class RecentChaptersPresenter extends BasePresenter { + /** + * The id of the restartable. + */ + private static final int GET_RECENT_CHAPTERS = 1; + + /** + * The id of the restartable. + */ + private static final int CHAPTER_STATUS_CHANGES = 2; + + /** + * Used to connect to database + */ @Inject DatabaseHelper db; + + /** + * Used to get information from download manager + */ + @Inject DownloadManager downloadManager; + + /** + * Used to get source from source id + */ @Inject SourceManager sourceManager; - private static final int GET_RECENT_CHAPTERS = 1; + /** + * List containing chapter and manga information + */ + private List mangaChapters; @Override protected void onCreate(Bundle savedState) { super.onCreate(savedState); + // Used to get recent chapters restartableLatestCache(GET_RECENT_CHAPTERS, this::getRecentChaptersObservable, - RecentChaptersFragment::onNextMangaChapters); + (recentChaptersFragment, chapters) -> { + // Update adapter to show recent manga's + recentChaptersFragment.onNextMangaChapters(chapters); + // Update download status + updateChapterStatus(convertToMangaChaptersList(chapters)); + }); - if (savedState == null) + // Used to update download status + startableLatestCache(CHAPTER_STATUS_CHANGES, + this::getChapterStatusObs, + RecentChaptersFragment::onChapterStatusChange, + (view, error) -> Timber.e(error.getCause(), error.getMessage())); + + if (savedState == null) { + // Start fetching recent chapters start(GET_RECENT_CHAPTERS); + } } + /** + * Returns a list only containing MangaChapter objects. + * + * @param input the list that will be converted. + * @return list containing MangaChapters objects. + */ + private List convertToMangaChaptersList(List input) { + // Create temp list + List tempMangaChapterList = new ArrayList<>(); + + // Only add MangaChapter objects + //noinspection Convert2streamapi + for (Object object : input) { + if (object instanceof MangaChapter) { + tempMangaChapterList.add((MangaChapter) object); + } + } + + // Return temp list + return tempMangaChapterList; + } + + /** + * Update status of chapters + * + * @param mangaChapters list containing recent chapters + */ + private void updateChapterStatus(List mangaChapters) { + // Set global list of chapters. + this.mangaChapters = mangaChapters; + + // Update status. + //noinspection Convert2streamapi + for (MangaChapter mangaChapter : mangaChapters) + setChapterStatus(mangaChapter); + + // Start onChapterStatusChange restartable. + start(CHAPTER_STATUS_CHANGES); + } + + /** + * Returns observable containing chapter status. + * + * @return download object containing download progress. + */ + private Observable getChapterStatusObs() { + return downloadManager.getQueue().getStatusObservable() + .observeOn(AndroidSchedulers.mainThread()) + .filter(download -> chapterIdEquals(download.chapter.id)) + .doOnNext(this::updateChapterStatus); + } + + /** + * Function to check if chapter is in recent list + * @param chaptersId id of chapter + * @return exist in recent list + */ + private boolean chapterIdEquals(Long chaptersId) { + for (MangaChapter mangaChapter : mangaChapters) { + if (chaptersId.equals(mangaChapter.chapter.id)) { + return true; + } + } + return false; + } + + /** + * Update status of chapters. + * + * @param download download object containing progress. + */ + private void updateChapterStatus(Download download) { + // Loop through list + for (MangaChapter item : mangaChapters) { + if (download.chapter.id.equals(item.chapter.id)) { + item.chapter.status = download.getStatus(); + break; + } + } + } + + /** + * Get observable containing recent chapters and date + * @return observable containing recent chapters and date + */ private Observable> getRecentChaptersObservable() { + // Set date for recent chapters Calendar cal = Calendar.getInstance(); cal.setTime(new Date()); cal.add(Calendar.MONTH, -1); + // Get recent chapters from database. return db.getRecentChapters(cal.getTime()).asRxObservable() - // group chapters by the date they were fetched on a ordered map + // Group chapters by the date they were fetched on a ordered map. .flatMap(recents -> Observable.from(recents) .toMultimap( recent -> getMapKey(recent.chapter.date_fetch), recent -> recent, () -> new TreeMap<>((d1, d2) -> d2.compareTo(d1)))) - // add every day and all its chapters to a single list + // Add every day and all its chapters to a single list. .map(recents -> { List items = new ArrayList<>(); for (Map.Entry> recent : recents.entrySet()) { @@ -66,6 +204,35 @@ public class RecentChaptersPresenter extends BasePresenter selectedChapter, Manga manga) { + add(selectedChapter + .toList() + .subscribe(chapters -> { + EventBus.getDefault().postSticky(new DownloadChaptersEvent(manga, chapters)); + })); + } + + /** + * Delete selected chapter + * @param chapter chapter that is selected + * @param manga manga that belongs to chapter + */ + public void deleteChapter(Chapter chapter, Manga manga) { + Source source = sourceManager.get(manga.source); + downloadManager.deleteChapter(source, manga, chapter); + } + + /** + * Delete selected chapter observable + * @param selectedChapters chapter that are selected + */ + public void deleteChapters(Observable selectedChapters) { + add(selectedChapters + .subscribe(chapter -> { + downloadManager.getQueue().remove(chapter); + }, error -> { + Timber.e(error.getMessage()); + })); + } + + /** + * Mark selected chapter as read + * @param selectedChapters chapter that is selected + * @param read read status + */ + public void markChaptersRead(Observable 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).asRxObservable()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe()); + } } diff --git a/app/src/main/res/layout/item_recent_chapter.xml b/app/src/main/res/layout/item_recent_chapter.xml index 4c2dd163a1..21d9111b7c 100644 --- a/app/src/main/res/layout/item_recent_chapter.xml +++ b/app/src/main/res/layout/item_recent_chapter.xml @@ -5,6 +5,7 @@ android:layout_height="?android:attr/listPreferredItemHeight" android:background="@drawable/selector_chapter_light"> + + + + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/menu/chapter_recent.xml b/app/src/main/res/menu/chapter_recent.xml new file mode 100644 index 0000000000..ec9883b9f7 --- /dev/null +++ b/app/src/main/res/menu/chapter_recent.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + \ No newline at end of file