diff --git a/app/build.gradle b/app/build.gradle index 61ffb665cf..3a33fc0f92 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -100,7 +100,7 @@ apt { } dependencies { - final SUPPORT_LIBRARY_VERSION = '23.2.0' + final SUPPORT_LIBRARY_VERSION = '23.2.1' final DAGGER_VERSION = '2.0.2' final OKHTTP_VERSION = '3.2.0' final RETROFIT_VERSION = '2.0.0-beta4' diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt index cc2749fffa..879caa31f7 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/base/activity/BaseActivity.kt @@ -66,14 +66,14 @@ open class BaseActivity : AppCompatActivity() { } fun snack(text: String?, duration: Int = Snackbar.LENGTH_LONG) { - val snack = Snackbar.make(findViewById(android.R.id.content), text ?: getString(R.string.unknown_error), duration) + val snack = Snackbar.make(findViewById(android.R.id.content)!!, text ?: getString(R.string.unknown_error), duration) val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView textView.setTextColor(Color.WHITE) snack.show() } fun snack(text: String?, actionRes: Int, actionFunc: () -> Unit, - duration: Int = Snackbar.LENGTH_LONG, view: View = findViewById(android.R.id.content)) { + duration: Int = Snackbar.LENGTH_LONG, view: View = findViewById(android.R.id.content)!!) { val snack = Snackbar.make(view, text ?: getString(R.string.unknown_error), duration) .setAction(actionRes, { actionFunc() }) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt index 78b7f81273..3193ed9833 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/catalogue/CatalogueFragment.kt @@ -20,7 +20,7 @@ import eu.kanade.tachiyomi.ui.base.decoration.DividerItemDecoration import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.manga.MangaActivity -import eu.kanade.tachiyomi.util.ToastUtil +import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.widget.EndlessGridScrollListener import eu.kanade.tachiyomi.widget.EndlessListScrollListener import kotlinx.android.synthetic.main.fragment_catalogue.* @@ -178,7 +178,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold // 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) + context.toast(R.string.source_requires_login) } else { selectedIndex = position presenter.setEnabledSource(selectedIndex) @@ -430,7 +430,7 @@ class CatalogueFragment : BaseRxFragment(), FlexibleViewHold val selectedManga = adapter.getItem(position) val intent = MangaActivity.newIntent(activity, selectedManga) - intent.putExtra(MangaActivity.MANGA_ONLINE, true) + intent.putExtra(MangaActivity.FROM_CATALOGUE, true) startActivity(intent) return false } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt index 7a0403920d..ff2c0d75df 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/library/LibraryFragment.kt @@ -8,7 +8,6 @@ import android.support.design.widget.TabLayout import android.support.v7.view.ActionMode import android.support.v7.widget.SearchView import android.view.* -import butterknife.ButterKnife import com.afollestad.materialdialogs.MaterialDialog import eu.davidea.flexibleadapter.FlexibleAdapter import eu.kanade.tachiyomi.R @@ -20,7 +19,6 @@ import eu.kanade.tachiyomi.event.LibraryMangasEvent import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment import eu.kanade.tachiyomi.ui.category.CategoryActivity import eu.kanade.tachiyomi.ui.main.MainActivity -import eu.kanade.tachiyomi.util.ToastUtil import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.toast import kotlinx.android.synthetic.main.fragment_library.* @@ -125,7 +123,6 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback override fun onViewCreated(view: View, savedState: Bundle?) { setToolbarTitle(getString(R.string.label_library)) - ButterKnife.bind(this, view) appBar = (activity as MainActivity).appBar tabs = appBar.inflate(R.layout.library_tab_layout) as TabLayout @@ -369,7 +366,7 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback startActivityForResult(Intent.createChooser(intent, getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN) } else { - ToastUtil.showShort(context, R.string.notification_first_add_to_library) + context.toast(R.string.notification_first_add_to_library) } } @@ -419,8 +416,8 @@ class LibraryFragment : BaseRxFragment(), ActionMode.Callback destroyActionModeIfNeeded() true } - .positiveText(R.string.button_ok) - .negativeText(R.string.button_cancel) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) .show() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.java deleted file mode 100644 index d357b0503b..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.java +++ /dev/null @@ -1,164 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga; - -import android.Manifest; -import android.content.Context; -import android.content.Intent; -import android.content.pm.PackageManager; -import android.os.Build; -import android.os.Bundle; -import android.support.design.widget.TabLayout; -import android.support.v4.app.ActivityCompat; -import android.support.v4.app.Fragment; -import android.support.v4.app.FragmentManager; -import android.support.v4.app.FragmentPagerAdapter; -import android.support.v4.content.ContextCompat; -import android.support.v4.view.ViewPager; -import android.support.v7.widget.Toolbar; - -import org.greenrobot.eventbus.EventBus; - -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 { - - @Bind(R.id.toolbar) Toolbar toolbar; - @Bind(R.id.tabs) TabLayout tabs; - @Bind(R.id.view_pager) ViewPager viewPager; - - @Inject PreferencesHelper preferences; - @Inject MangaSyncManager mangaSyncManager; - - private MangaDetailAdapter adapter; - private boolean isOnline; - - public final static String MANGA_ONLINE = "manga_online"; - - public static Intent newIntent(Context context, Manga manga) { - Intent intent = new Intent(context, MangaActivity.class); - if (manga != null) { - EventBus.getDefault().postSticky(manga); - } - 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(); - - isOnline = intent.getBooleanExtra(MANGA_ONLINE, false); - - setupViewPager(); - - requestPermissionsOnMarshmallow(); - } - - private void setupViewPager() { - adapter = new MangaDetailAdapter(getSupportFragmentManager(), this); - - viewPager.setAdapter(adapter); - - // Workaround to prevent: Tab belongs to a different TabLayout. - // Internal bug in Support library v23.2.0. - // See https://code.google.com/p/android/issues/detail?id=201827 - for (int j = 0; j < 17; j++) - tabs.newTab(); - - tabs.setupWithViewPager(viewPager); - - if (!isOnline) - viewPager.setCurrentItem(MangaDetailAdapter.CHAPTERS_FRAGMENT); - } - - public void setManga(Manga manga) { - setToolbarTitle(manga.title); - } - - public boolean isCatalogueManga() { - return isOnline; - } - - private void requestPermissionsOnMarshmallow() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { - if (ContextCompat.checkSelfPermission(this, - Manifest.permission.WRITE_EXTERNAL_STORAGE) - != PackageManager.PERMISSION_GRANTED) { - - ActivityCompat.requestPermissions(this, - new String[]{Manifest.permission.WRITE_EXTERNAL_STORAGE, - Manifest.permission.READ_EXTERNAL_STORAGE}, - 1); - - } - } - } - - 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 (!isOnline && 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]; - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt new file mode 100644 index 0000000000..e65b7eb70e --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt @@ -0,0 +1,118 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.Manifest +import android.content.Context +import android.content.Intent +import android.content.pm.PackageManager +import android.os.Build +import android.os.Bundle +import android.support.v4.app.ActivityCompat +import android.support.v4.app.Fragment +import android.support.v4.app.FragmentManager +import android.support.v4.app.FragmentPagerAdapter +import android.support.v4.content.ContextCompat +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +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 kotlinx.android.synthetic.main.activity_manga.* +import kotlinx.android.synthetic.main.tab_layout.* +import kotlinx.android.synthetic.main.toolbar.* +import nucleus.factory.RequiresPresenter +import org.greenrobot.eventbus.EventBus + +@RequiresPresenter(MangaPresenter::class) +class MangaActivity : BaseRxActivity() { + + companion object { + + val FROM_CATALOGUE = "from_catalogue" + val INFO_FRAGMENT = 0 + val CHAPTERS_FRAGMENT = 1 + val MYANIMELIST_FRAGMENT = 2 + + fun newIntent(context: Context, manga: Manga?): Intent { + val intent = Intent(context, MangaActivity::class.java) + if (manga != null) { + EventBus.getDefault().postSticky(manga) + } + return intent + } + } + + private lateinit var adapter: MangaDetailAdapter + + var isCatalogueManga: Boolean = false + private set + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + setContentView(R.layout.activity_manga) + + setupToolbar(toolbar) + + isCatalogueManga = intent.getBooleanExtra(FROM_CATALOGUE, false) + + adapter = MangaDetailAdapter(supportFragmentManager, this) + view_pager.adapter = adapter + + tabs.setupWithViewPager(view_pager) + + if (!isCatalogueManga) + view_pager.currentItem = CHAPTERS_FRAGMENT + + requestPermissionsOnMarshmallow() + } + + fun onSetManga(manga: Manga) { + setToolbarTitle(manga.title) + } + + private fun requestPermissionsOnMarshmallow() { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) { + if (ContextCompat.checkSelfPermission(this, + Manifest.permission.WRITE_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { + + ActivityCompat.requestPermissions(this, + arrayOf(Manifest.permission.WRITE_EXTERNAL_STORAGE, Manifest.permission.READ_EXTERNAL_STORAGE), + 1) + + } + } + } + + internal class MangaDetailAdapter(fm: FragmentManager, activity: MangaActivity) : FragmentPagerAdapter(fm) { + + private var pageCount: Int = 0 + private val tabTitles = arrayOf(activity.getString(R.string.manga_detail_tab), + activity.getString(R.string.manga_chapters_tab), "MAL") + + init { + pageCount = 2 + if (!activity.isCatalogueManga && activity.presenter.syncManager.myAnimeList.isLogged) + pageCount++ + } + + override fun getCount(): Int { + return pageCount + } + + override fun getItem(position: Int): Fragment? { + when (position) { + INFO_FRAGMENT -> return MangaInfoFragment.newInstance() + CHAPTERS_FRAGMENT -> return ChaptersFragment.newInstance() + MYANIMELIST_FRAGMENT -> return MyAnimeListFragment.newInstance() + else -> return null + } + } + + override fun getPageTitle(position: Int): CharSequence { + // Generate title based on item position + return tabTitles[position] + } + + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.java deleted file mode 100644 index c976f9e28f..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.java +++ /dev/null @@ -1,56 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga; - -import android.os.Bundle; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import javax.inject.Inject; - -import eu.kanade.tachiyomi.data.database.DatabaseHelper; -import eu.kanade.tachiyomi.data.database.models.Manga; -import eu.kanade.tachiyomi.event.MangaEvent; -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; -import icepick.State; -import rx.Observable; - -public class MangaPresenter extends BasePresenter { - - @Inject DatabaseHelper db; - - @State Manga manga; - - private static final int GET_MANGA = 1; - - @Override - protected void onCreate(Bundle savedState) { - super.onCreate(savedState); - - restartableLatestCache(GET_MANGA, this::getMangaObservable, MangaActivity::setManga); - - if (savedState == null) - registerForEvents(); - } - - @Override - protected void onDestroy() { - super.onDestroy(); - // Avoid new instances receiving wrong manga - EventBus.getDefault().removeStickyEvent(MangaEvent.class); - } - - private Observable getMangaObservable() { - return Observable.just(manga) - .doOnNext(manga -> EventBus.getDefault().postSticky(new MangaEvent(manga))); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEvent(Manga manga) { - EventBus.getDefault().removeStickyEvent(manga); - unregisterForEvents(); - this.manga = manga; - start(GET_MANGA); - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt new file mode 100644 index 0000000000..3b68a39869 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt @@ -0,0 +1,82 @@ +package eu.kanade.tachiyomi.ui.manga + +import android.os.Bundle +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.mangasync.MangaSyncManager +import eu.kanade.tachiyomi.event.MangaEvent +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import rx.Observable +import javax.inject.Inject + +/** + * Presenter of [MangaActivity]. + */ +class MangaPresenter : BasePresenter() { + + /** + * Database helper. + */ + @Inject lateinit var db: DatabaseHelper + + /** + * Manga sync manager. + */ + @Inject lateinit var syncManager: MangaSyncManager + + /** + * Manga associated with this instance. + */ + lateinit var manga: Manga + + /** + * Key to save and restore [manga] from a bundle. + */ + private val MANGA_KEY = "manga_key" + + /** + * Id of the restartable that notifies the view of a manga. + */ + private val GET_MANGA = 1 + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + if (savedState != null) { + manga = savedState.getSerializable(MANGA_KEY) as Manga + } + + restartableLatestCache(GET_MANGA, + { Observable.just(manga) + .doOnNext { EventBus.getDefault().postSticky(MangaEvent(it)) } }, + { view, manga -> view.onSetManga(manga) }) + + if (savedState == null) { + registerForEvents() + } + } + + override fun onDestroy() { + // Avoid new instances receiving wrong manga + EventBus.getDefault().removeStickyEvent(MangaEvent::class.java) + + super.onDestroy() + } + + override fun onSave(state: Bundle) { + state.putSerializable(MANGA_KEY, manga) + super.onSave(state) + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onEvent(manga: Manga) { + EventBus.getDefault().removeStickyEvent(manga) + unregisterForEvents() + this.manga = manga + start(GET_MANGA) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.java deleted file mode 100644 index ba7db26bcb..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.java +++ /dev/null @@ -1,57 +0,0 @@ -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; -import eu.kanade.tachiyomi.data.database.models.Manga; - -public class ChaptersAdapter extends FlexibleAdapter { - - 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); - final Manga manga = fragment.getPresenter().getManga(); - holder.onSetValues(chapter, manga); - - //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 chapters) { - mItems = chapters; - notifyDataSetChanged(); - } - - public ChaptersFragment getFragment() { - return fragment; - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt new file mode 100644 index 0000000000..9282d425b1 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersAdapter.kt @@ -0,0 +1,40 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.view.ViewGroup +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.util.inflate + +class ChaptersAdapter(val fragment: ChaptersFragment) : FlexibleAdapter() { + + init { + setHasStableIds(true) + } + + override fun updateDataSet(param: String) { + } + + override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): ChaptersHolder { + val v = parent.inflate(R.layout.item_chapter) + return ChaptersHolder(v, this, fragment) + } + + override fun onBindViewHolder(holder: ChaptersHolder, position: Int) { + val chapter = getItem(position) + val manga = fragment.presenter.manga + holder.onSetValues(chapter, manga) + + //When user scrolls this bind the correct selection status + holder.itemView.isActivated = isSelected(position) + } + + override fun getItemId(position: Int): Long { + return mItems[position].id + } + + fun setItems(chapters: List) { + mItems = chapters + notifyDataSetChanged() + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.java deleted file mode 100644 index 462f13df1d..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.java +++ /dev/null @@ -1,439 +0,0 @@ -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.MenuInflater; -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.ArrayList; -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.database.models.Manga; -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.decoration.DividerItemDecoration; -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment; -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 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 void onCreate(Bundle bundle) { - super.onCreate(bundle); - 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_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); - - swipeRefresh.setOnRefreshListener(this::fetchChapters); - - 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 onPause() { - // Stop recycler's scrolling when onPause is called. If the activity is finishing - // the presenter will be destroyed, and it could cause NPE - // https://github.com/inorichi/tachiyomi/issues/159 - recyclerView.stopScroll(); - super.onPause(); - } - - @Override - public void onCreateOptionsMenu(Menu menu, MenuInflater inflater) { - inflater.inflate(R.menu.chapters, menu); - } - - @Override - public boolean onOptionsItemSelected(MenuItem item) { - switch (item.getItemId()) { - case R.id.action_display_mode: - showDisplayModeDialog(); - return true; - case R.id.manga_download: - showDownloadDialog(); - return true; - } - return false; - } - - public void onNextManga(Manga manga) { - // Remove listeners before setting the values - readCb.setOnCheckedChangeListener(null); - downloadedCb.setOnCheckedChangeListener(null); - sortBtn.setOnClickListener(null); - - // Set initial values - setReadFilter(); - setDownloadedFilter(); - setSortIcon(); - - // Init listeners - readCb.setOnCheckedChangeListener((arg, isChecked) -> - getPresenter().setReadFilter(isChecked)); - downloadedCb.setOnCheckedChangeListener((v, isChecked) -> - getPresenter().setDownloadedFilter(isChecked)); - sortBtn.setOnClickListener(v -> { - getPresenter().revertSortOrder(); - setSortIcon(); - }); - } - - public void onNextChapters(List 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(Throwable error) { - swipeRefresh.setRefreshing(false); - ToastUtil.showShort(getContext(), error.getMessage()); - } - - public boolean isCatalogueManga() { - return ((MangaActivity) getActivity()).isCatalogueManga(); - } - - protected void openChapter(Chapter chapter) { - getPresenter().onOpenChapter(chapter); - Intent intent = ReaderActivity.newIntent(getActivity()); - startActivity(intent); - } - - private void showDisplayModeDialog() { - final Manga manga = getPresenter().getManga(); - if (manga == null) - return; - - // Get available modes, ids and the selected mode - String[] modes = {getString(R.string.show_title), getString(R.string.show_chapter_number)}; - int[] ids = {Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER}; - int selectedIndex = manga.getDisplayMode() == Manga.DISPLAY_NAME ? 0 : 1; - - new MaterialDialog.Builder(getActivity()) - .title(R.string.action_display_mode) - .items(modes) - .itemsIds(ids) - .itemsCallbackSingleChoice(selectedIndex, (dialog, itemView, which, text) -> { - // Save the new display mode - getPresenter().setDisplayMode(itemView.getId()); - // Refresh ui - adapter.notifyDataSetChanged(); - return true; - }) - .show(); - } - - private void showDownloadDialog() { - - // Get available modes - String[] modes = {getString(R.string.download_all), getString(R.string.download_unread)}; - - new MaterialDialog.Builder(getActivity()) - .title(R.string.manga_download) - .items(modes) - .itemsCallback((dialog, view, i, charSequence) -> { - List chapters = new ArrayList<>(); - - for(Chapter chapter : getPresenter().getChapters()) { - if(!chapter.isDownloaded()) { - if(i == 0 || (i == 1 && !chapter.read)) { - chapters.add(chapter); - } - } - } - if(chapters.size() > 0) { - onDownload(Observable.from(chapters)); - } - }) - .negativeText(R.string.button_cancel) - .show(); - } - - 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(Download download) { - ChaptersHolder holder = getHolder(download.chapter); - if (holder != null) - holder.onStatusChange(download.getStatus()); - } - - @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 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 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 chapters) { - getPresenter().markChaptersRead(chapters, true); - return true; - } - - protected boolean onMarkAsUnread(Observable chapters) { - getPresenter().markChaptersRead(chapters, false); - return true; - } - - public boolean onMarkPreviousAsRead(Chapter chapter) { - getPresenter().markPreviousChaptersAsRead(chapter); - return true; - } - - protected boolean onDownload(Observable chapters) { - DownloadService.start(getActivity()); - - Observable observable = chapters - .doOnCompleted(adapter::notifyDataSetChanged); - - getPresenter().downloadChapters(observable); - destroyActionModeIfNeeded(); - return true; - } - - protected boolean onDelete(Observable chapters) { - 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); - 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().onlyUnread()); - } - } - - public void setDownloadedFilter() { - if (downloadedCb != null) { - downloadedCb.setChecked(getPresenter().onlyDownloaded()); - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt new file mode 100644 index 0000000000..942f78c724 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersFragment.kt @@ -0,0 +1,362 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.os.Bundle +import android.support.v4.content.ContextCompat +import android.support.v7.view.ActionMode +import android.support.v7.widget.LinearLayoutManager +import android.view.* +import com.afollestad.materialdialogs.MaterialDialog +import eu.davidea.flexibleadapter.FlexibleAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +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.decoration.DividerItemDecoration +import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment +import eu.kanade.tachiyomi.ui.manga.MangaActivity +import eu.kanade.tachiyomi.ui.reader.ReaderActivity +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.fragment_manga_chapters.* +import nucleus.factory.RequiresPresenter +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import java.util.* + +@RequiresPresenter(ChaptersPresenter::class) +class ChaptersFragment : BaseRxFragment(), ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener { + + companion object { + /** + * Creates a new instance of this fragment. + * + * @return a new instance of [ChaptersFragment]. + */ + fun newInstance(): ChaptersFragment { + return ChaptersFragment() + } + } + + /** + * Adapter containing a list of chapters. + */ + private lateinit var adapter: ChaptersAdapter + + /** + * Action mode for multiple selection. + */ + private var actionMode: ActionMode? = null + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + setHasOptionsMenu(true) + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_manga_chapters, container, false) + } + + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + // Init RecyclerView and adapter + adapter = ChaptersAdapter(this) + + recycler.adapter = adapter + recycler.layoutManager = LinearLayoutManager(activity) + recycler.addItemDecoration(DividerItemDecoration( + ContextCompat.getDrawable(context, R.drawable.line_divider))) + recycler.setHasFixedSize(true) + + swipe_refresh.setOnRefreshListener { fetchChapters() } + + next_unread_btn.setOnClickListener { v -> + val chapter = presenter.getNextUnreadChapter() + if (chapter != null) { + openChapter(chapter) + } else { + context.toast(R.string.no_next_chapter) + } + } + + } + + override fun onPause() { + // Stop recycler's scrolling when onPause is called. If the activity is finishing + // the presenter will be destroyed, and it could cause NPE + // https://github.com/inorichi/tachiyomi/issues/159 + recycler.stopScroll() + + super.onPause() + } + + override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) { + inflater.inflate(R.menu.chapters, menu) + } + + override fun onOptionsItemSelected(item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_display_mode -> showDisplayModeDialog() + R.id.manga_download -> showDownloadDialog() + else -> return super.onOptionsItemSelected(item) + } + return true + } + + fun onNextManga(manga: Manga) { + // Remove listeners before setting the values + show_unread.setOnCheckedChangeListener(null) + show_downloaded.setOnCheckedChangeListener(null) + sort_btn.setOnClickListener(null) + + // Set initial values + setReadFilter() + setDownloadedFilter() + setSortIcon() + + // Init listeners + show_unread.setOnCheckedChangeListener { arg, isChecked -> presenter.setReadFilter(isChecked) } + show_downloaded.setOnCheckedChangeListener { v, isChecked -> presenter.setDownloadedFilter(isChecked) } + sort_btn.setOnClickListener { + presenter.revertSortOrder() + setSortIcon() + } + } + + fun onNextChapters(chapters: List) { + // 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 (presenter.chapters.isEmpty()) + initialFetchChapters() + + destroyActionModeIfNeeded() + adapter.setItems(chapters) + } + + private fun initialFetchChapters() { + // Only fetch if this view is from the catalog and it hasn't requested previously + if (isCatalogueManga && !presenter.hasRequested) { + fetchChapters() + } + } + + fun fetchChapters() { + swipe_refresh.isRefreshing = true + presenter.fetchChaptersFromSource() + } + + fun onFetchChaptersDone() { + swipe_refresh.isRefreshing = false + } + + fun onFetchChaptersError(error: Throwable) { + swipe_refresh.isRefreshing = false + context.toast(error.message) + } + + val isCatalogueManga: Boolean + get() = (activity as MangaActivity).isCatalogueManga + + protected fun openChapter(chapter: Chapter) { + presenter.onOpenChapter(chapter) + val intent = ReaderActivity.newIntent(activity) + startActivity(intent) + } + + private fun showDisplayModeDialog() { + + // Get available modes, ids and the selected mode + val modes = listOf(getString(R.string.show_title), getString(R.string.show_chapter_number)) + val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER) + val selectedIndex = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1 + + MaterialDialog.Builder(activity) + .title(R.string.action_display_mode) + .items(modes) + .itemsIds(ids) + .itemsCallbackSingleChoice(selectedIndex) { dialog, itemView, which, text -> + // Save the new display mode + presenter.setDisplayMode(itemView.id) + // Refresh ui + adapter.notifyDataSetChanged() + true + } + .show() + } + + private fun showDownloadDialog() { + // Get available modes + val modes = listOf(getString(R.string.download_all), getString(R.string.download_unread)) + + MaterialDialog.Builder(activity) + .title(R.string.manga_download) + .negativeText(android.R.string.cancel) + .items(modes) + .itemsCallback { dialog, view, i, charSequence -> + val chapters = ArrayList() + + for (chapter in presenter.chapters) { + if (!chapter.isDownloaded) { + if (i == 0 || (i == 1 && !chapter.read)) { + chapters.add(chapter) + } + } + } + if (chapters.size > 0) { + onDownload(Observable.from(chapters)) + } + } + .show() + } + + fun onChapterStatusChange(download: Download) { + getHolder(download.chapter)?.notifyStatus(download.status) + } + + private fun getHolder(chapter: Chapter): ChaptersHolder? { + return recycler.findViewHolderForItemId(chapter.id) as? ChaptersHolder + } + + override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean { + mode.menuInflater.inflate(R.menu.chapter_selection, menu) + adapter.mode = FlexibleAdapter.MODE_MULTI + return true + } + + override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean { + return false + } + + override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean { + when (item.itemId) { + R.id.action_select_all -> onSelectAll() + R.id.action_mark_as_read -> onMarkAsRead(getSelectedChapters()) + R.id.action_mark_as_unread -> onMarkAsUnread(getSelectedChapters()) + R.id.action_download -> onDownload(getSelectedChapters()) + R.id.action_delete -> onDelete(getSelectedChapters()) + else -> return false + } + return true + } + + override fun onDestroyActionMode(mode: ActionMode) { + adapter.mode = FlexibleAdapter.MODE_SINGLE + adapter.clearSelection() + actionMode = null + } + + fun getSelectedChapters(): Observable { + val chapters = adapter.selectedItems.map { adapter.getItem(it) } + return Observable.from(chapters) + } + + fun destroyActionModeIfNeeded() { + actionMode?.finish() + } + + protected fun onSelectAll() { + adapter.selectAll() + setContextTitle(adapter.selectedItemCount) + } + + fun onMarkAsRead(chapters: Observable) { + presenter.markChaptersRead(chapters, true) + } + + fun onMarkAsUnread(chapters: Observable) { + presenter.markChaptersRead(chapters, false) + } + + fun onMarkPreviousAsRead(chapter: Chapter) { + presenter.markPreviousChaptersAsRead(chapter) + } + + fun onDownload(chapters: Observable) { + DownloadService.start(activity) + + val observable = chapters.doOnCompleted { adapter.notifyDataSetChanged() } + + presenter.downloadChapters(observable) + destroyActionModeIfNeeded() + } + + fun onDelete(chapters: Observable) { + val size = adapter.selectedItemCount + + val dialog = MaterialDialog.Builder(activity) + .title(R.string.deleting) + .progress(false, size, true) + .cancelable(false) + .show() + + val observable = chapters + .concatMap { chapter -> + presenter.deleteChapter(chapter) + Observable.just(chapter) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { chapter -> + dialog.incrementProgress(1) + chapter.status = Download.NOT_DOWNLOADED + } + .doOnCompleted { adapter.notifyDataSetChanged() } + .doAfterTerminate { dialog.dismiss() } + + presenter.deleteChapters(observable) + destroyActionModeIfNeeded() + } + + override fun onListItemClick(position: Int): Boolean { + if (actionMode != null && adapter.mode == FlexibleAdapter.MODE_MULTI) { + toggleSelection(position) + return true + } else { + openChapter(adapter.getItem(position)) + return false + } + } + + override fun onListItemLongClick(position: Int) { + if (actionMode == null) + actionMode = baseActivity.startSupportActionMode(this) + + toggleSelection(position) + } + + private fun toggleSelection(position: Int) { + adapter.toggleSelection(position, false) + + val count = adapter.selectedItemCount + if (count == 0) { + actionMode?.finish() + } else { + setContextTitle(count) + actionMode?.invalidate() + } + } + + private fun setContextTitle(count: Int) { + actionMode?.title = getString(R.string.label_selected, count) + } + + fun setSortIcon() { + sort_btn?.let { + val aToZ = presenter.sortOrder() + it.setImageResource(if (!aToZ) R.drawable.ic_expand_less_white_36dp else R.drawable.ic_expand_more_white_36dp) + } + } + + fun setReadFilter() { + show_unread?.let { + it.isChecked = presenter.onlyUnread() + } + } + + fun setDownloadedFilter() { + show_downloaded?.let { + it.isChecked = presenter.onlyDownloaded() + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersHolder.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersHolder.java deleted file mode 100644 index 883cc79cf7..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersHolder.java +++ /dev/null @@ -1,150 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter; - -import android.content.Context; -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 java.text.DateFormat; -import java.text.DecimalFormat; -import java.text.DecimalFormatSymbols; -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.database.models.Manga; -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 final int readColor; - private final int unreadColor; - private final DecimalFormat decimalFormat; - private final DateFormat df = DateFormat.getDateInstance(DateFormat.SHORT); - @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; - private Context context; - private Chapter item; - - public ChaptersHolder(View view, ChaptersAdapter adapter, OnListItemClickListener listener) { - super(view, adapter, listener); - this.adapter = adapter; - context = view.getContext(); - ButterKnife.bind(this, view); - - readColor = ContextCompat.getColor(view.getContext(), R.color.hint_text); - unreadColor = ContextCompat.getColor(view.getContext(), R.color.primary_text); - - DecimalFormatSymbols symbols = new DecimalFormatSymbols(); - symbols.setDecimalSeparator('.'); - decimalFormat = new DecimalFormat("#.###", symbols); - - chapterMenu.setOnClickListener(v -> v.post(() -> showPopupMenu(v))); - } - - public void onSetValues(Chapter chapter, Manga manga) { - this.item = chapter; - String name; - switch (manga.getDisplayMode()) { - case Manga.DISPLAY_NAME: - default: - name = chapter.name; - break; - case Manga.DISPLAY_NUMBER: - String formattedNumber = decimalFormat.format(chapter.chapter_number); - name = context.getString(R.string.display_mode_chapter, formattedNumber); - break; - } - title.setText(name); - title.setTextColor(chapter.read ? readColor : unreadColor); - date.setTextColor(chapter.read ? readColor : unreadColor); - - 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(df.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()); - - // Hide download and show delete if the chapter is downloaded and - if(item.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(!item.read && item.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(item.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 chapter = Observable.just(item); - - switch (menuItem.getItemId()) { - case R.id.action_download: - return adapter.getFragment().onDownload(chapter); - case R.id.action_delete: - return adapter.getFragment().onDelete(chapter); - 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_mark_previous_as_read: - return adapter.getFragment().onMarkPreviousAsRead(item); - } - return false; - }); - - // Finally show the PopupMenu - popup.show(); - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersHolder.kt new file mode 100644 index 0000000000..5e8ec0b435 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersHolder.kt @@ -0,0 +1,116 @@ +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 eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.download.model.Download +import eu.kanade.tachiyomi.ui.base.adapter.FlexibleViewHolder +import kotlinx.android.synthetic.main.item_chapter.view.* +import rx.Observable +import java.text.DateFormat +import java.text.DecimalFormat +import java.text.DecimalFormatSymbols +import java.util.* + +class ChaptersHolder(private val view: View, private val adapter: ChaptersAdapter, listener: FlexibleViewHolder.OnListItemClickListener) : + FlexibleViewHolder(view, adapter, listener) { + + private val readColor = ContextCompat.getColor(view.context, R.color.hint_text) + private val unreadColor = ContextCompat.getColor(view.context, R.color.primary_text) + private val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols().apply { decimalSeparator = '.' }) + private val df = DateFormat.getDateInstance(DateFormat.SHORT) + + private var item: Chapter? = null + + init { + view.chapter_menu.setOnClickListener { v -> v.post { showPopupMenu(v) } } + } + + fun onSetValues(chapter: Chapter, manga: Manga?) = with(view) { + item = chapter + + val name: String + when (manga?.displayMode) { + Manga.DISPLAY_NUMBER -> { + val formattedNumber = decimalFormat.format(chapter.chapter_number.toDouble()) + name = context.getString(R.string.display_mode_chapter, formattedNumber) + } + else -> name = chapter.name + } + + chapter_title.text = name + chapter_title.setTextColor(if (chapter.read) readColor else unreadColor) + + chapter_date.text = df.format(Date(chapter.date_upload)) + chapter_date.setTextColor(if (chapter.read) readColor else unreadColor) + + if (!chapter.read && chapter.last_page_read > 0) { + chapter_pages.text = context.getString(R.string.chapter_progress, chapter.last_page_read + 1) + } else { + chapter_pages.text = "" + } + + notifyStatus(chapter.status) + } + + fun notifyStatus(status: Int) = with(view) { + when (status) { + Download.QUEUE -> download_text.setText(R.string.chapter_queued) + Download.DOWNLOADING -> download_text.setText(R.string.chapter_downloading) + Download.DOWNLOADED -> download_text.setText(R.string.chapter_downloaded) + Download.ERROR -> download_text.setText(R.string.chapter_error) + else -> download_text.text = "" + } + } + + fun onProgressChange(context: Context, downloaded: Int, total: Int) { + view.download_text.text = context.getString( + R.string.chapter_downloading_progress, downloaded, total) + } + + private fun showPopupMenu(view: View) = item?.let { item -> + // Create a PopupMenu, giving it the clicked view for an anchor + val popup = PopupMenu(view.context, view) + + // Inflate our menu resource into the PopupMenu's Menu + popup.menuInflater.inflate(R.menu.chapter_single, popup.menu) + + // Hide download and show delete if the chapter is downloaded + if (item.isDownloaded) { + popup.menu.findItem(R.id.action_download).isVisible = false + popup.menu.findItem(R.id.action_delete).isVisible = true + } + + // Hide mark as unread when the chapter is unread + if (!item.read && item.last_page_read == 0) { + popup.menu.findItem(R.id.action_mark_as_unread).isVisible = false + } + + // Hide mark as read when the chapter is read + if (item.read) { + popup.menu.findItem(R.id.action_mark_as_read).isVisible = false + } + + // Set a listener so we are notified if a menu item is clicked + popup.setOnMenuItemClickListener { menuItem -> + val chapter = Observable.just(item) + + when (menuItem.itemId) { + R.id.action_download -> adapter.fragment.onDownload(chapter) + R.id.action_delete -> adapter.fragment.onDelete(chapter) + R.id.action_mark_as_read -> adapter.fragment.onMarkAsRead(chapter) + R.id.action_mark_as_unread -> adapter.fragment.onMarkAsUnread(chapter) + R.id.action_mark_previous_as_read -> adapter.fragment.onMarkPreviousAsRead(item) + } + true + } + + // Finally show the PopupMenu + popup.show() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.java deleted file mode 100644 index 8a86e277b2..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.java +++ /dev/null @@ -1,286 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.chapter; - -import android.os.Bundle; -import android.util.Pair; - -import org.greenrobot.eventbus.EventBus; -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.List; - -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.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.MangaEvent; -import eu.kanade.tachiyomi.event.ReaderEvent; -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; -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 { - - @Inject DatabaseHelper db; - @Inject SourceManager sourceManager; - @Inject PreferencesHelper preferences; - @Inject DownloadManager downloadManager; - - private Manga manga; - private Source source; - private List chapters; - @State boolean hasRequested; - - private PublishSubject> chaptersSubject; - - private static final int GET_MANGA = 1; - private static final int DB_CHAPTERS = 2; - private static final int FETCH_CHAPTERS = 3; - private static final int CHAPTER_STATUS_CHANGES = 4; - - @Override - protected void onCreate(Bundle savedState) { - super.onCreate(savedState); - - chaptersSubject = PublishSubject.create(); - - startableLatestCache(GET_MANGA, - () -> Observable.just(manga), - ChaptersFragment::onNextManga); - - startableLatestCache(DB_CHAPTERS, - this::getDbChaptersObs, - ChaptersFragment::onNextChapters); - - startableFirst(FETCH_CHAPTERS, - this::getOnlineChaptersObs, - (view, result) -> view.onFetchChaptersDone(), - (view, error) -> view.onFetchChaptersError(error)); - - startableLatestCache(CHAPTER_STATUS_CHANGES, - this::getChapterStatusObs, - (view, download) -> view.onChapterStatusChange(download), - (view, error) -> Timber.e(error.getCause(), error.getMessage())); - - registerForEvents(); - } - - @Override - protected void onDestroy() { - unregisterForEvents(); - EventBus.getDefault().removeStickyEvent(ChapterCountEvent.class); - super.onDestroy(); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEvent(MangaEvent event) { - this.manga = event.manga; - start(GET_MANGA); - - if (isUnsubscribed(DB_CHAPTERS)) { - source = sourceManager.get(manga.source); - start(DB_CHAPTERS); - - add(db.getChapters(manga).asRxObservable() - .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> getOnlineChaptersObs() { - return source.pullChaptersFromNetwork(manga.url) - .subscribeOn(Schedulers.io()) - .flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters, source)) - .observeOn(AndroidSchedulers.mainThread()); - } - - private Observable> getDbChaptersObs() { - return chaptersSubject.flatMap(this::applyChapterFilters) - .observeOn(AndroidSchedulers.mainThread()); - } - - private Observable> applyChapterFilters(List chapters) { - Observable 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) -> getSortOrder() ? - 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 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 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 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()); - } - - public void markPreviousChaptersAsRead(Chapter selected) { - Observable.from(chapters) - .filter(c -> c.chapter_number > -1 && c.chapter_number < selected.chapter_number) - .doOnNext(c -> c.read = true) - .toList() - .flatMap(chapters -> db.insertChapters(chapters).asRxObservable()) - .subscribe(); - } - - public void downloadChapters(Observable selectedChapters) { - add(selectedChapters - .toList() - .subscribe(chapters -> { - EventBus.getDefault().postSticky(new DownloadChaptersEvent(manga, chapters)); - })); - } - - public void deleteChapters(Observable 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() { - manga.setChapterOrder(getSortOrder() ? Manga.SORT_ZA : Manga.SORT_AZ); - db.insertManga(manga).executeAsBlocking(); - refreshChapters(); - } - - public void setReadFilter(boolean onlyUnread) { - manga.setReadFilter(onlyUnread ? Manga.SHOW_UNREAD : Manga.SHOW_ALL); - db.insertManga(manga).executeAsBlocking(); - refreshChapters(); - } - - public void setDownloadedFilter(boolean onlyDownloaded) { - manga.setDownloadedFilter(onlyDownloaded ? Manga.SHOW_DOWNLOADED : Manga.SHOW_ALL); - db.insertManga(manga).executeAsBlocking(); - refreshChapters(); - } - - public void setDisplayMode(int mode) { - manga.setDisplayMode(mode); - db.insertManga(manga).executeAsBlocking(); - } - - public boolean onlyDownloaded() { - return manga.getDownloadedFilter() == Manga.SHOW_DOWNLOADED; - } - - public boolean onlyUnread() { - return manga.getReadFilter() == Manga.SHOW_UNREAD; - } - - public boolean getSortOrder() { - return manga.sortChaptersAZ(); - } - - public Manga getManga() { - return manga; - } - - public List getChapters() { - return chapters; - } - - public boolean hasRequested() { - return hasRequested; - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt new file mode 100644 index 0000000000..9dc5cc5b07 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/chapter/ChaptersPresenter.kt @@ -0,0 +1,264 @@ +package eu.kanade.tachiyomi.ui.manga.chapter + +import android.os.Bundle +import android.util.Pair +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.MangaEvent +import eu.kanade.tachiyomi.event.ReaderEvent +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import org.greenrobot.eventbus.EventBus +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import rx.subjects.PublishSubject +import timber.log.Timber +import javax.inject.Inject + +class ChaptersPresenter : BasePresenter() { + + @Inject lateinit var db: DatabaseHelper + @Inject lateinit var sourceManager: SourceManager + @Inject lateinit var preferences: PreferencesHelper + @Inject lateinit var downloadManager: DownloadManager + + lateinit var manga: Manga + private set + + lateinit var source: Source + private set + + lateinit var chapters: List + private set + + lateinit var chaptersSubject: PublishSubject> + private set + + var hasRequested: Boolean = false + private set + + private val GET_MANGA = 1 + private val DB_CHAPTERS = 2 + private val FETCH_CHAPTERS = 3 + private val CHAPTER_STATUS_CHANGES = 4 + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + chaptersSubject = PublishSubject.create() + + startableLatestCache(GET_MANGA, + { Observable.just(manga) }, + { view, manga -> view.onNextManga(manga) }) + + startableLatestCache(DB_CHAPTERS, + { getDbChaptersObs() }, + { view, chapters -> view.onNextChapters(chapters) }) + + startableFirst(FETCH_CHAPTERS, + { getOnlineChaptersObs() }, + { view, result -> view.onFetchChaptersDone() }, + { view, error -> view.onFetchChaptersError(error) }) + + startableLatestCache(CHAPTER_STATUS_CHANGES, + { getChapterStatusObs() }, + { view, download -> view.onChapterStatusChange(download) }, + { view, error -> Timber.e(error.cause, error.message) }) + + registerForEvents() + } + + override fun onDestroy() { + unregisterForEvents() + EventBus.getDefault().removeStickyEvent(ChapterCountEvent::class.java) + super.onDestroy() + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onEvent(event: MangaEvent) { + this.manga = event.manga + start(GET_MANGA) + + if (isUnsubscribed(DB_CHAPTERS)) { + source = sourceManager.get(manga.source)!! + start(DB_CHAPTERS) + + add(db.getChapters(manga).asRxObservable() + .subscribeOn(Schedulers.io()) + .doOnNext { chapters -> + this.chapters = chapters + EventBus.getDefault().postSticky(ChapterCountEvent(chapters.size)) + for (chapter in chapters) { + setChapterStatus(chapter) + } + start(CHAPTER_STATUS_CHANGES) + } + .subscribe{ chaptersSubject.onNext(it) }) + } + } + + fun fetchChaptersFromSource() { + hasRequested = true + start(FETCH_CHAPTERS) + } + + private fun refreshChapters() { + chaptersSubject.onNext(chapters) + } + + fun getOnlineChaptersObs(): Observable> { + return source.pullChaptersFromNetwork(manga.url) + .subscribeOn(Schedulers.io()) + .flatMap { chapters -> db.insertOrRemoveChapters(manga, chapters, source) } + .observeOn(AndroidSchedulers.mainThread()) + } + + fun getDbChaptersObs(): Observable> { + return chaptersSubject + .flatMap { applyChapterFilters(it) } + .observeOn(AndroidSchedulers.mainThread()) + } + + fun getChapterStatusObs(): Observable { + return downloadManager.queue.statusObservable + .observeOn(AndroidSchedulers.mainThread()) + .filter { download -> download.manga.id == manga.id } + .doOnNext { updateChapterStatus(it) } + } + + private fun applyChapterFilters(chapters: List): Observable> { + var 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 -> + if (sortOrder()) + chapter2.chapter_number.compareTo(chapter.chapter_number) + else + chapter.chapter_number.compareTo(chapter2.chapter_number) + } + } + + private fun setChapterStatus(chapter: Chapter) { + for (download in downloadManager.queue) { + if (chapter.id == download.chapter.id) { + chapter.status = download.status + return + } + } + + if (downloadManager.isChapterDownloaded(source, manga, chapter)) { + chapter.status = Download.DOWNLOADED + } else { + chapter.status = Download.NOT_DOWNLOADED + } + } + + fun updateChapterStatus(download: Download) { + for (chapter in chapters) { + if (download.chapter.id == chapter.id) { + chapter.status = download.status + break + } + } + if (onlyDownloaded() && download.status == Download.DOWNLOADED) + refreshChapters() + } + + fun onOpenChapter(chapter: Chapter) { + EventBus.getDefault().postSticky(ReaderEvent(source, manga, chapter)) + } + + fun getNextUnreadChapter(): Chapter? { + return db.getNextUnreadChapter(manga).executeAsBlocking() + } + + fun markChaptersRead(selectedChapters: Observable, read: Boolean) { + add(selectedChapters.subscribeOn(Schedulers.io()) + .doOnNext { chapter -> + chapter.read = read + if (!read) chapter.last_page_read = 0 + } + .toList() + .flatMap { chapters -> db.insertChapters(chapters).asRxObservable() } + .observeOn(AndroidSchedulers.mainThread()) + .subscribe()) + } + + fun markPreviousChaptersAsRead(selected: Chapter) { + Observable.from(chapters) + .filter { c -> c.chapter_number > -1 && c.chapter_number < selected.chapter_number } + .doOnNext { c -> c.read = true } + .toList() + .flatMap { chapters -> db.insertChapters(chapters).asRxObservable() } + .subscribe() + } + + fun downloadChapters(selectedChapters: Observable) { + add(selectedChapters.toList() + .subscribe { chapters -> EventBus.getDefault().postSticky(DownloadChaptersEvent(manga, chapters)) }) + } + + fun deleteChapters(selectedChapters: Observable) { + add(selectedChapters.subscribe( + { chapter -> downloadManager.queue.remove(chapter) }, + { error -> Timber.e(error.message) }, + { + if (onlyDownloaded()) + refreshChapters() + })) + } + + fun deleteChapter(chapter: Chapter) { + downloadManager.deleteChapter(source, manga, chapter) + } + + fun revertSortOrder() { + manga.setChapterOrder(if (sortOrder()) Manga.SORT_ZA else Manga.SORT_AZ) + db.insertManga(manga).executeAsBlocking() + refreshChapters() + } + + fun setReadFilter(onlyUnread: Boolean) { + manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL + db.insertManga(manga).executeAsBlocking() + refreshChapters() + } + + fun setDownloadedFilter(onlyDownloaded: Boolean) { + manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL + db.insertManga(manga).executeAsBlocking() + refreshChapters() + } + + fun setDisplayMode(mode: Int) { + manga.displayMode = mode + db.insertManga(manga).executeAsBlocking() + } + + fun onlyDownloaded(): Boolean { + return manga.downloadedFilter == Manga.SHOW_DOWNLOADED + } + + fun onlyUnread(): Boolean { + return manga.readFilter == Manga.SHOW_UNREAD + } + + fun sortOrder(): Boolean { + return manga.sortChaptersAZ() + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.java deleted file mode 100644 index 1c1e855f21..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.java +++ /dev/null @@ -1,244 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info; - -import android.os.Bundle; -import android.support.design.widget.FloatingActionButton; -import android.support.v4.content.ContextCompat; -import android.support.v4.widget.SwipeRefreshLayout; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -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.data.source.base.Source; -import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment; -import nucleus.factory.RequiresPresenter; - -/** - * Fragment that shows manga information. - * Uses R.layout.fragment_manga_info. - * UI related actions should be called from here. - */ -@RequiresPresenter(MangaInfoPresenter.class) -public class MangaInfoFragment extends BaseRxFragment { - /** - * SwipeRefreshLayout showing refresh status - */ - @Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh; - - /** - * TextView containing artist information. - */ - @Bind(R.id.manga_artist) TextView artist; - - /** - * TextView containing author information. - */ - @Bind(R.id.manga_author) TextView author; - - /** - * TextView containing chapter count. - */ - @Bind(R.id.manga_chapters) TextView chapterCount; - - /** - * TextView containing genres. - */ - @Bind(R.id.manga_genres) TextView genres; - - /** - * TextView containing status (ongoing, finished). - */ - @Bind(R.id.manga_status) TextView status; - - /** - * TextView containing source. - */ - @Bind(R.id.manga_source) TextView source; - - /** - * TextView containing manga summary. - */ - @Bind(R.id.manga_summary) TextView description; - - /** - * ImageView of cover. - */ - @Bind(R.id.manga_cover) ImageView cover; - - /** - * ImageView containing manga cover shown as blurred backdrop. - */ - @Bind(R.id.backdrop) ImageView backdrop; - - /** - * FAB anchored to bottom of top view used to (add / remove) manga (to / from) library. - */ - @Bind(R.id.fab_favorite) FloatingActionButton fabFavorite; - - /** - * Create new instance of MangaInfoFragment. - * - * @return MangaInfoFragment. - */ - public static MangaInfoFragment newInstance() { - return new MangaInfoFragment(); - } - - @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); - - // Bind layout objects. - ButterKnife.bind(this, view); - - // Set onclickListener to toggle favorite when FAB clicked. - fabFavorite.setOnClickListener(v -> getPresenter().toggleFavorite()); - - // Set SwipeRefresh to refresh manga data. - swipeRefresh.setOnRefreshListener(this::fetchMangaFromSource); - - return view; - } - - /** - * Check if manga is initialized. - * If true update view with manga information, - * if false fetch manga information - * - * @param manga manga object containing information about manga. - * @param source the source of the manga. - */ - public void onNextManga(Manga manga, Source source) { - if (manga.initialized) { - // Update view. - setMangaInfo(manga, source); - } else { - // Initialize manga. - fetchMangaFromSource(); - } - } - - /** - * Update the view with manga information. - * - * @param manga manga object containing information about manga. - * @param mangaSource the source of the manga. - */ - private void setMangaInfo(Manga manga, Source mangaSource) { - // Update artist TextView. - artist.setText(manga.artist); - - // Update author TextView. - author.setText(manga.author); - - // If manga source is known update source TextView. - if (mangaSource != null) { - source.setText(mangaSource.getVisibleName()); - } - - // Update genres TextView. - genres.setText(manga.genre); - - // Update status TextView. - status.setText(manga.getStatus(getActivity())); - - // Update description TextView. - description.setText(manga.description); - - // Set the favorite drawable to the correct one. - setFavoriteDrawable(manga.favorite); - - // Initialize CoverCache and Glide headers to retrieve cover information. - CoverCache coverCache = getPresenter().coverCache; - LazyHeaders headers = getPresenter().source.getGlideHeaders(); - - // Check if thumbnail_url is given. - if (manga.thumbnail_url != null) { - // Check if cover is already drawn. - if (cover.getDrawable() == null) { - // If manga is in library then (download / save) (from / to) local cache if available, - // else download from network. - if (manga.favorite) { - coverCache.saveOrLoadFromCache(cover, manga.thumbnail_url, headers); - } else { - coverCache.loadFromNetwork(cover, manga.thumbnail_url, headers); - } - } - // Check if backdrop is already drawn. - if (backdrop.getDrawable() == null) { - // If manga is in library then (download / save) (from / to) local cache if available, - // else download from network. - if (manga.favorite) { - coverCache.saveOrLoadFromCache(backdrop, manga.thumbnail_url, headers); - } else { - coverCache.loadFromNetwork(backdrop, manga.thumbnail_url, headers); - } - } - } - } - - /** - * Update chapter count TextView. - * - * @param count number of chapters. - */ - public void setChapterCount(int count) { - chapterCount.setText(String.valueOf(count)); - } - - /** - * Update FAB with correct drawable. - * - * @param isFavorite determines if manga is favorite or not. - */ - private void setFavoriteDrawable(boolean isFavorite) { - // Set the Favorite drawable to the correct one. - // Border drawable if false, filled drawable if true. - fabFavorite.setImageDrawable(ContextCompat.getDrawable(getContext(), isFavorite ? - R.drawable.ic_bookmark_white_24dp : - R.drawable.ic_bookmark_border_white_24dp)); - } - - /** - * Start fetching manga information from source. - */ - private void fetchMangaFromSource() { - setRefreshing(true); - // Call presenter and start fetching manga information - getPresenter().fetchMangaFromSource(); - } - - - /** - * Update swipeRefresh to stop showing refresh in progress spinner. - */ - public void onFetchMangaDone() { - setRefreshing(false); - } - - /** - * Update swipeRefresh to start showing refresh in progress spinner. - */ - public void onFetchMangaError() { - setRefreshing(false); - } - - /** - * Set swipeRefresh status. - * - * @param value status of manga fetch. - */ - private void setRefreshing(boolean value) { - swipeRefresh.setRefreshing(value); - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt new file mode 100644 index 0000000000..0840e4b44c --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoFragment.kt @@ -0,0 +1,179 @@ +package eu.kanade.tachiyomi.ui.manga.info + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +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.fragment.BaseRxFragment +import eu.kanade.tachiyomi.util.setDrawableCompat +import kotlinx.android.synthetic.main.fragment_manga_info.* +import nucleus.factory.RequiresPresenter + +/** + * Fragment that shows manga information. + * Uses R.layout.fragment_manga_info. + * UI related actions should be called from here. + */ +@RequiresPresenter(MangaInfoPresenter::class) +class MangaInfoFragment : BaseRxFragment() { + + companion object { + /** + * Create new instance of MangaInfoFragment. + * + * @return MangaInfoFragment. + */ + fun newInstance(): MangaInfoFragment { + return MangaInfoFragment() + } + } + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_manga_info, container, false) + } + + override fun onViewCreated(view: View?, savedState: Bundle?) { + // Set onclickListener to toggle favorite when FAB clicked. + fab_favorite.setOnClickListener { presenter.toggleFavorite() } + + // Set SwipeRefresh to refresh manga data. + swipe_refresh.setOnRefreshListener { fetchMangaFromSource() } + } + + /** + * Check if manga is initialized. + * If true update view with manga information, + * if false fetch manga information + * + * @param manga manga object containing information about manga. + * @param source the source of the manga. + */ + fun onNextManga(manga: Manga, source: Source) { + if (manga.initialized) { + // Update view. + setMangaInfo(manga, source) + } else { + // Initialize manga. + fetchMangaFromSource() + } + } + + /** + * Update the view with manga information. + * + * @param manga manga object containing information about manga. + * @param source the source of the manga. + */ + private fun setMangaInfo(manga: Manga, source: Source?) { + // Update artist TextView. + manga_artist.text = manga.artist + + // Update author TextView. + manga_author.text = manga.author + + // If manga source is known update source TextView. + if (source != null) { + manga_source.text = source.visibleName + } + + // Update genres TextView. + manga_genres.text = manga.genre + + // Update status TextView. + manga_status.text = manga.getStatus(activity) + + // Update description TextView. + manga_summary.text = manga.description + + // Set the favorite drawable to the correct one. + setFavoriteDrawable(manga.favorite) + + // Initialize CoverCache and Glide headers to retrieve cover information. + val coverCache = presenter.coverCache + val headers = presenter.source.glideHeaders + + // Check if thumbnail_url is given. + if (manga.thumbnail_url != null) { + // Check if cover is already drawn. + if (manga_cover.drawable == null) { + // If manga is in library then (download / save) (from / to) local cache if available, + // else download from network. + if (manga.favorite) { + coverCache.saveOrLoadFromCache(manga_cover, manga.thumbnail_url, headers) + } else { + coverCache.loadFromNetwork(manga_cover, manga.thumbnail_url, headers) + } + } + // Check if backdrop is already drawn. + if (backdrop.drawable == null) { + // If manga is in library then (download / save) (from / to) local cache if available, + // else download from network. + if (manga.favorite) { + coverCache.saveOrLoadFromCache(backdrop, manga.thumbnail_url, headers) + } else { + coverCache.loadFromNetwork(backdrop, manga.thumbnail_url, headers) + } + } + } + } + + /** + * Update chapter count TextView. + * + * @param count number of chapters. + */ + fun setChapterCount(count: Int) { + manga_chapters.text = count.toString() + } + + /** + * Update FAB with correct drawable. + * + * @param isFavorite determines if manga is favorite or not. + */ + private fun setFavoriteDrawable(isFavorite: Boolean) { + // Set the Favorite drawable to the correct one. + // Border drawable if false, filled drawable if true. + fab_favorite.setDrawableCompat(if (isFavorite) + R.drawable.ic_bookmark_white_24dp + else + R.drawable.ic_bookmark_border_white_24dp) + } + + /** + * Start fetching manga information from source. + */ + private fun fetchMangaFromSource() { + setRefreshing(true) + // Call presenter and start fetching manga information + presenter.fetchMangaFromSource() + } + + + /** + * Update swipe refresh to stop showing refresh in progress spinner. + */ + fun onFetchMangaDone() { + setRefreshing(false) + } + + /** + * Update swipe refresh to start showing refresh in progress spinner. + */ + fun onFetchMangaError() { + setRefreshing(false) + } + + /** + * Set swipe refresh status. + * + * @param value whether it should be refreshing or not. + */ + private fun setRefreshing(value: Boolean) { + swipe_refresh.isRefreshing = value + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.java deleted file mode 100644 index 8fcbe9ec72..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.java +++ /dev/null @@ -1,177 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.info; - -import android.os.Bundle; - -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -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.event.MangaEvent; -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; -import rx.Observable; -import rx.android.schedulers.AndroidSchedulers; -import rx.schedulers.Schedulers; - -/** - * Presenter of MangaInfoFragment. - * Contains information and data for fragment. - * Observable updates should be called from here. - */ -public class MangaInfoPresenter extends BasePresenter { - - /** - * The id of the restartable. - */ - private static final int GET_MANGA = 1; - - /** - * The id of the restartable. - */ - private static final int GET_CHAPTER_COUNT = 2; - - /** - * The id of the restartable. - */ - private static final int FETCH_MANGA_INFO = 3; - - /** - * Source information. - */ - protected Source source; - - /** - * Used to connect to database. - */ - @Inject DatabaseHelper db; - - /** - * Used to connect to different manga sources. - */ - @Inject SourceManager sourceManager; - - /** - * Used to connect to cache. - */ - @Inject CoverCache coverCache; - - /** - * Selected manga information. - */ - private Manga manga; - - /** - * Count of chapters. - */ - private int count = -1; - - @Override - protected void onCreate(Bundle savedState) { - super.onCreate(savedState); - - // Notify the view a manga is available or has changed. - startableLatestCache(GET_MANGA, - () -> Observable.just(manga), - (view, manga) -> view.onNextManga(manga, source)); - - // Update chapter count. - startableLatestCache(GET_CHAPTER_COUNT, - () -> Observable.just(count), - MangaInfoFragment::setChapterCount); - - // Fetch manga info from source. - startableFirst(FETCH_MANGA_INFO, - this::fetchMangaObs, - (view, manga) -> view.onFetchMangaDone(), - (view, error) -> view.onFetchMangaError()); - - // Listen for events. - registerForEvents(); - } - - @Override - protected void onDestroy() { - unregisterForEvents(); - super.onDestroy(); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEvent(MangaEvent event) { - this.manga = event.manga; - source = sourceManager.get(manga.source); - refreshManga(); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEvent(ChapterCountEvent event) { - if (count != event.getCount()) { - count = event.getCount(); - // Update chapter count - start(GET_CHAPTER_COUNT); - } - } - - /** - * Fetch manga information from source. - */ - public void fetchMangaFromSource() { - if (isUnsubscribed(FETCH_MANGA_INFO)) { - start(FETCH_MANGA_INFO); - } - } - - /** - * Fetch manga information from source. - * - * @return manga information. - */ - private Observable fetchMangaObs() { - return source.pullMangaFromNetwork(manga.url) - .flatMap(networkManga -> { - manga.copyFrom(networkManga); - db.insertManga(manga).executeAsBlocking(); - return Observable.just(manga); - }) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()) - .doOnNext(manga -> refreshManga()); - } - - /** - * Update favorite status of manga, (removes / adds) manga (to / from) library. - */ - public void toggleFavorite() { - manga.favorite = !manga.favorite; - onMangaFavoriteChange(manga.favorite); - db.insertManga(manga).executeAsBlocking(); - refreshManga(); - } - - - /** - * (Removes / Saves) cover depending on favorite status. - * - * @param isFavorite determines if manga is favorite or not. - */ - private void onMangaFavoriteChange(boolean isFavorite) { - if (isFavorite) { - coverCache.save(manga.thumbnail_url, source.getGlideHeaders()); - } else { - coverCache.deleteCoverFromCache(manga.thumbnail_url); - } - } - - /** - * Refresh MangaInfo view. - */ - private void refreshManga() { - start(GET_MANGA); - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt new file mode 100644 index 0000000000..d472e829cb --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/info/MangaInfoPresenter.kt @@ -0,0 +1,173 @@ +package eu.kanade.tachiyomi.ui.manga.info + +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.source.SourceManager +import eu.kanade.tachiyomi.data.source.base.Source +import eu.kanade.tachiyomi.event.ChapterCountEvent +import eu.kanade.tachiyomi.event.MangaEvent +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import javax.inject.Inject + +/** + * Presenter of MangaInfoFragment. + * Contains information and data for fragment. + * Observable updates should be called from here. + */ +class MangaInfoPresenter : BasePresenter() { + + /** + * Active manga. + */ + lateinit var manga: Manga + private set + + /** + * Source of the manga. + */ + lateinit var source: Source + private set + + /** + * Used to connect to database. + */ + @Inject lateinit var db: DatabaseHelper + + /** + * Used to connect to different manga sources. + */ + @Inject lateinit var sourceManager: SourceManager + + /** + * Used to connect to cache. + */ + @Inject lateinit var coverCache: CoverCache + + /** + * Count of chapters. + */ + private var count = -1 + + /** + * The id of the restartable. + */ + private val GET_MANGA = 1 + + /** + * The id of the restartable. + */ + private val GET_CHAPTER_COUNT = 2 + + /** + * The id of the restartable. + */ + private val FETCH_MANGA_INFO = 3 + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + // Notify the view a manga is available or has changed. + startableLatestCache(GET_MANGA, + { Observable.just(manga) }, + { view, manga -> view.onNextManga(manga, source) }) + + // Update chapter count. + startableLatestCache(GET_CHAPTER_COUNT, + { Observable.just(count) }, + { view, count -> view.setChapterCount(count) }) + + // Fetch manga info from source. + startableFirst(FETCH_MANGA_INFO, + { fetchMangaObs() }, + { view, manga -> view.onFetchMangaDone() }, + { view, error -> view.onFetchMangaError() }) + + // Listen for events. + registerForEvents() + } + + override fun onDestroy() { + unregisterForEvents() + super.onDestroy() + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onEvent(event: MangaEvent) { + manga = event.manga + source = sourceManager.get(manga.source)!! + refreshManga() + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onEvent(event: ChapterCountEvent) { + if (count != event.count) { + count = event.count + // Update chapter count + start(GET_CHAPTER_COUNT) + } + } + + /** + * Fetch manga information from source. + */ + fun fetchMangaFromSource() { + if (isUnsubscribed(FETCH_MANGA_INFO)) { + start(FETCH_MANGA_INFO) + } + } + + /** + * Fetch manga information from source. + * + * @return manga information. + */ + private fun fetchMangaObs(): Observable { + return source.pullMangaFromNetwork(manga.url) + .flatMap { networkManga -> + manga.copyFrom(networkManga) + db.insertManga(manga).executeAsBlocking() + Observable.just(manga) + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnNext { refreshManga() } + } + + /** + * Update favorite status of manga, (removes / adds) manga (to / from) library. + */ + fun toggleFavorite() { + manga.favorite = !manga.favorite + onMangaFavoriteChange(manga.favorite) + db.insertManga(manga).executeAsBlocking() + refreshManga() + } + + /** + * (Removes / Saves) cover depending on favorite status. + * + * @param isFavorite determines if manga is favorite or not. + */ + private fun onMangaFavoriteChange(isFavorite: Boolean) { + if (isFavorite) { + coverCache.save(manga.thumbnail_url, source.glideHeaders) + } else { + coverCache.deleteCoverFromCache(manga.thumbnail_url) + } + } + + /** + * Refresh MangaInfo view. + */ + private fun refreshManga() { + start(GET_MANGA) + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListDialogFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListDialogFragment.java deleted file mode 100644 index 6d4d9f3bc2..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListDialogFragment.java +++ /dev/null @@ -1,151 +0,0 @@ -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 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; - -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 MyAnimeListSearchAdapter 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 MyAnimeListSearchAdapter(getActivity()); - 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 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 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.clear(); - } - - public MyAnimeListFragment getMALFragment() { - return (MyAnimeListFragment) getParentFragment(); - } - - public MyAnimeListPresenter getPresenter() { - return getMALFragment().getPresenter(); - } - - 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) { - - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListDialogFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListDialogFragment.kt new file mode 100644 index 0000000000..1ac704282b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListDialogFragment.kt @@ -0,0 +1,126 @@ +package eu.kanade.tachiyomi.ui.manga.myanimelist + +import android.app.Dialog +import android.os.Bundle +import android.support.v4.app.DialogFragment +import android.view.View +import com.afollestad.materialdialogs.MaterialDialog +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.MangaSync +import eu.kanade.tachiyomi.widget.SimpleTextWatcher +import kotlinx.android.synthetic.main.dialog_myanimelist_search.view.* +import rx.Subscription +import rx.android.schedulers.AndroidSchedulers +import rx.subjects.PublishSubject +import java.util.concurrent.TimeUnit + +class MyAnimeListDialogFragment : DialogFragment() { + + companion object { + + fun newInstance(): MyAnimeListDialogFragment { + return MyAnimeListDialogFragment() + } + } + + private lateinit var v: View + + lateinit var adapter: MyAnimeListSearchAdapter + private set + + lateinit var querySubject: PublishSubject + private set + + private var selectedItem: MangaSync? = null + + private var searchSubscription: Subscription? = null + + override fun onCreateDialog(savedState: Bundle?): Dialog { + val dialog = MaterialDialog.Builder(activity) + .customView(R.layout.dialog_myanimelist_search, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { dialog1, which -> onPositiveButtonClick() } + .build() + + onViewCreated(dialog.view, savedState) + + return dialog + } + + override fun onViewCreated(view: View, savedState: Bundle?) { + v = view + + // Create adapter + adapter = MyAnimeListSearchAdapter(activity) + view.myanimelist_search_results.adapter = adapter + + // Set listeners + view.myanimelist_search_results.setOnItemClickListener { parent, viewList, position, id -> + selectedItem = adapter.getItem(position) + } + + // Do an initial search based on the manga's title + if (savedState == null) { + val title = presenter.manga.title + view.myanimelist_search_field.append(title) + search(title) + } + + querySubject = PublishSubject.create() + + view.myanimelist_search_field.addTextChangedListener(object : SimpleTextWatcher() { + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { + querySubject.onNext(s.toString()) + } + }) + } + + override fun onResume() { + super.onResume() + + // Listen to text changes + searchSubscription = querySubject.debounce(1, TimeUnit.SECONDS) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe { search(it) } + } + + override fun onPause() { + searchSubscription?.unsubscribe() + super.onPause() + } + + private fun onPositiveButtonClick() { + selectedItem?.let { + presenter.registerManga(it) + } + } + + private fun search(query: String) { + if (!query.isNullOrEmpty()) { + v.myanimelist_search_results.visibility = View.GONE + v.progress.visibility = View.VISIBLE + presenter.searchManga(query) + } + } + + fun onSearchResults(results: List) { + selectedItem = null + v.progress.visibility = View.GONE + v.myanimelist_search_results.visibility = View.VISIBLE + adapter.setItems(results) + } + + fun onSearchResultsError() { + v.progress.visibility = View.GONE + v.myanimelist_search_results.visibility = View.VISIBLE + adapter.clear() + } + + val malFragment: MyAnimeListFragment + get() = parentFragment as MyAnimeListFragment + + val presenter: MyAnimeListPresenter + get() = malFragment.presenter + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListFragment.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListFragment.java deleted file mode 100644 index e532c1c190..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListFragment.java +++ /dev/null @@ -1,181 +0,0 @@ -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 { - - @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 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); - } - } - -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListFragment.kt new file mode 100644 index 0000000000..7d31eeb026 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListFragment.kt @@ -0,0 +1,168 @@ +package eu.kanade.tachiyomi.ui.manga.myanimelist + +import android.os.Bundle +import android.view.LayoutInflater +import android.view.View +import android.view.ViewGroup +import android.widget.NumberPicker +import com.afollestad.materialdialogs.MaterialDialog +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.MangaSync +import eu.kanade.tachiyomi.ui.base.fragment.BaseRxFragment +import eu.kanade.tachiyomi.util.toast +import kotlinx.android.synthetic.main.card_myanimelist_personal.* +import kotlinx.android.synthetic.main.fragment_myanimelist.* +import nucleus.factory.RequiresPresenter +import java.text.DecimalFormat + +@RequiresPresenter(MyAnimeListPresenter::class) +class MyAnimeListFragment : BaseRxFragment() { + + companion object { + fun newInstance(): MyAnimeListFragment { + return MyAnimeListFragment() + } + } + + private var dialog: MyAnimeListDialogFragment? = null + + private val decimalFormat = DecimalFormat("#.##") + + private val SEARCH_FRAGMENT_TAG = "mal_search" + + override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, savedState: Bundle?): View? { + return inflater.inflate(R.layout.fragment_myanimelist, container, false) + } + + override fun onViewCreated(view: View, savedState: Bundle?) { + swipe_refresh.isEnabled = false + swipe_refresh.setOnRefreshListener { presenter.refresh() } + myanimelist_title_layout.setOnClickListener { onTitleClick() } + myanimelist_status_layout.setOnClickListener { onStatusClick() } + myanimelist_chapters_layout.setOnClickListener { onChaptersClick() } + myanimelist_score_layout.setOnClickListener { onScoreClick() } + } + + fun setMangaSync(mangaSync: MangaSync?) { + swipe_refresh.isEnabled = mangaSync != null + mangaSync?.let { + myanimelist_title.text = it.title + val chaptersText = if (it.total_chapters > 0) + "${it.last_chapter_read}/${it.total_chapters}" else "${it.last_chapter_read}/-" + + myanimelist_chapters.text = chaptersText + myanimelist_score.text = if (it.score == 0f) "-" else decimalFormat.format(it.score) + myanimelist_status.text = presenter.myAnimeList.getStatus(it.status) + } + } + + fun onRefreshDone() { + swipe_refresh.isRefreshing = false + } + + fun onRefreshError() { + swipe_refresh.isRefreshing = false + } + + fun setSearchResults(results: List) { + findSearchFragmentIfNeeded() + + dialog?.onSearchResults(results) + } + + fun setSearchResultsError(error: Throwable) { + findSearchFragmentIfNeeded() + context.toast(error.message) + + dialog?.onSearchResultsError() + } + + private fun findSearchFragmentIfNeeded() { + if (dialog == null) { + dialog = childFragmentManager.findFragmentByTag(SEARCH_FRAGMENT_TAG) as MyAnimeListDialogFragment + } + } + + fun onTitleClick() { + if (dialog == null) { + dialog = MyAnimeListDialogFragment.newInstance() + } + + presenter.restartSearch() + dialog?.show(childFragmentManager, SEARCH_FRAGMENT_TAG) + } + + fun onStatusClick() { + if (presenter.mangaSync == null) + return + + MaterialDialog.Builder(activity) + .title(R.string.status) + .items(presenter.getAllStatus()) + .itemsCallbackSingleChoice(presenter.getIndexFromStatus(), { dialog, view, i, charSequence -> + presenter.setStatus(i) + myanimelist_status.text = "..." + true + }) + .show() + } + + fun onChaptersClick() { + if (presenter.mangaSync == null) + return + + val dialog = MaterialDialog.Builder(activity) + .title(R.string.chapters) + .customView(R.layout.dialog_myanimelist_chapters, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { d, action -> + val view = d.customView + if (view != null) { + val np = view.findViewById(R.id.chapters_picker) as NumberPicker + np.clearFocus() + presenter.setLastChapterRead(np.value) + myanimelist_chapters.text = "..." + } + } + .show() + + val view = dialog.customView + if (view != null) { + val np = view.findViewById(R.id.chapters_picker) as NumberPicker + // Set initial value + np.value = presenter.mangaSync!!.last_chapter_read + // Don't allow to go from 0 to 9999 + np.wrapSelectorWheel = false + } + } + + fun onScoreClick() { + if (presenter.mangaSync == null) + return + + val dialog = MaterialDialog.Builder(activity) + .title(R.string.score) + .customView(R.layout.dialog_myanimelist_score, false) + .positiveText(android.R.string.ok) + .negativeText(android.R.string.cancel) + .onPositive { d, action -> + val view = d.customView + if (view != null) { + val np = view.findViewById(R.id.score_picker) as NumberPicker + np.clearFocus() + presenter.setScore(np.value) + myanimelist_score.text = "..." + } + } + .show() + + val view = dialog.customView + if (view != null) { + val np = view.findViewById(R.id.score_picker) as NumberPicker + // Set initial value + np.value = presenter.mangaSync!!.score.toInt() + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListPresenter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListPresenter.java deleted file mode 100644 index 0ccff700dd..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListPresenter.java +++ /dev/null @@ -1,197 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.myanimelist; - -import android.content.Context; -import android.os.Bundle; -import android.text.TextUtils; - -import org.greenrobot.eventbus.Subscribe; -import org.greenrobot.eventbus.ThreadMode; - -import java.util.List; - -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.event.MangaEvent; -import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter; -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 { - - @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; - - private static final String PREFIX_MY = "my:"; - - @Override - protected void onCreate(Bundle savedState) { - super.onCreate(savedState); - - myAnimeList = syncManager.getMyAnimeList(); - - startableLatestCache(GET_MANGA_SYNC, - () -> db.getMangaSync(manga, myAnimeList).asRxObservable() - .doOnNext(mangaSync -> this.mangaSync = mangaSync) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()), - MyAnimeListFragment::setMangaSync); - - startableLatestCache(GET_SEARCH_RESULTS, - this::getSearchResultsObservable, - (view, results) -> { - view.setSearchResults(results); - }, (view, error) -> { - Timber.e(error.getMessage()); - view.setSearchResultsError(); - }); - - startableFirst(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).asRxObservable()) - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()), - (view, result) -> view.onRefreshDone(), - (view, error) -> view.onRefreshError()); - - } - - @Override - protected void onTakeView(MyAnimeListFragment view) { - super.onTakeView(view); - registerForEvents(); - } - - @Override - protected void onDropView() { - unregisterForEvents(); - super.onDropView(); - } - - @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) - public void onEvent(MangaEvent event) { - this.manga = event.manga; - start(GET_MANGA_SYNC); - } - - private Observable> getSearchResultsObservable() { - Observable> observable; - if (query.startsWith(PREFIX_MY)) { - String realQuery = query.substring(PREFIX_MY.length()).toLowerCase().trim(); - observable = myAnimeList.getList() - .flatMap(Observable::from) - .filter(manga -> manga.title.toLowerCase().contains(realQuery)) - .toList(); - } else { - observable = myAnimeList.search(query); - } - return observable - .subscribeOn(Schedulers.io()) - .observeOn(AndroidSchedulers.mainThread()); - } - - private void updateRemote() { - add(myAnimeList.update(mangaSync) - .flatMap(response -> db.insertMangaSync(mangaSync).asRxObservable()) - .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).asRxObservable(); - } - 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); - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListPresenter.kt new file mode 100644 index 0000000000..16abc10f6a --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListPresenter.kt @@ -0,0 +1,191 @@ +package eu.kanade.tachiyomi.ui.manga.myanimelist + +import android.os.Bundle +import com.pushtorefresh.storio.sqlite.operations.put.PutResult +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.event.MangaEvent +import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter +import eu.kanade.tachiyomi.util.toast +import org.greenrobot.eventbus.Subscribe +import org.greenrobot.eventbus.ThreadMode +import rx.Observable +import rx.android.schedulers.AndroidSchedulers +import rx.schedulers.Schedulers +import timber.log.Timber +import javax.inject.Inject + +class MyAnimeListPresenter : BasePresenter() { + + @Inject lateinit var db: DatabaseHelper + @Inject lateinit var syncManager: MangaSyncManager + + val myAnimeList by lazy { syncManager.myAnimeList } + + lateinit var manga: Manga + private set + + var mangaSync: MangaSync? = null + private set + + private var query: String? = null + + private val GET_MANGA_SYNC = 1 + private val GET_SEARCH_RESULTS = 2 + private val REFRESH = 3 + + private val PREFIX_MY = "my:" + + override fun onCreate(savedState: Bundle?) { + super.onCreate(savedState) + + startableLatestCache(GET_MANGA_SYNC, + { db.getMangaSync(manga, myAnimeList).asRxObservable() + .doOnNext { mangaSync = it } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) }, + { view, mangaSync -> view.setMangaSync(mangaSync) }) + + startableLatestCache(GET_SEARCH_RESULTS, + { getSearchResultsObservable() }, + { view, results -> view.setSearchResults(results) }, + { view, error -> view.setSearchResultsError(error) }) + + startableFirst(REFRESH, + { getRefreshObservable() }, + { view, result -> view.onRefreshDone() }, + { view, error -> view.onRefreshError() }) + + registerForEvents() + } + + override fun onDestroy() { + unregisterForEvents() + super.onDestroy() + } + + @Subscribe(sticky = true, threadMode = ThreadMode.MAIN) + fun onEvent(event: MangaEvent) { + manga = event.manga + start(GET_MANGA_SYNC) + } + + fun getSearchResultsObservable(): Observable> { + return query?.let { query -> + val observable: Observable> + if (query.startsWith(PREFIX_MY)) { + val realQuery = query.substring(PREFIX_MY.length).toLowerCase().trim() + observable = myAnimeList.getList() + .flatMap { Observable.from(it) } + .filter { it.title.toLowerCase().contains(realQuery) } + .toList() + } else { + observable = myAnimeList.search(query) + } + observable.subscribeOn(Schedulers.io()).observeOn(AndroidSchedulers.mainThread()) + } ?: Observable.error(Exception("Null query")) + + } + + fun getRefreshObservable(): Observable { + return mangaSync?.let { mangaSync -> + myAnimeList.getList() + .flatMap { myList -> + for (myManga in myList) { + if (myManga.remote_id == mangaSync.remote_id) { + mangaSync.copyPersonalFrom(myManga) + mangaSync.total_chapters = myManga.total_chapters + return@flatMap Observable.just(mangaSync) + } + } + Observable.error(Exception("Could not find manga")) + } + .flatMap { db.insertMangaSync(it).asRxObservable() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + } ?: Observable.error(Exception("Not found")) + } + + private fun updateRemote() { + mangaSync?.let { mangaSync -> + add(myAnimeList.update(mangaSync) + .flatMap { response -> db.insertMangaSync(mangaSync).asRxObservable() } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ next -> }, + { error -> + Timber.e(error.message) + // Restart on error to set old values + start(GET_MANGA_SYNC) + })) + } + } + + fun searchManga(query: String) { + if (query.isNullOrEmpty() || query == this.query) + return + + this.query = query + start(GET_SEARCH_RESULTS) + } + + fun restartSearch() { + query = null + stop(GET_SEARCH_RESULTS) + } + + fun registerManga(sync: MangaSync) { + sync.manga_id = manga.id + add(myAnimeList.bind(sync) + .flatMap { response -> + if (response.isSuccessful) { + db.insertMangaSync(sync).asRxObservable() + } else { + Observable.error(Exception("Could not bind manga")) + } + } + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe({ }, + { error -> context.toast(error.message) })) + } + + fun getAllStatus(): List { + return listOf(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)) + } + + fun getIndexFromStatus(): Int { + return mangaSync?.let { mangaSync -> + if (mangaSync.status == 6) 4 else mangaSync.status - 1 + } ?: 0 + } + + fun setStatus(index: Int) { + mangaSync?.status = if (index == 4) 6 else index + 1 + updateRemote() + } + + fun setScore(score: Int) { + mangaSync?.score = score.toFloat() + updateRemote() + } + + fun setLastChapterRead(chapterNumber: Int) { + mangaSync?.last_chapter_read = chapterNumber + updateRemote() + } + + fun refresh() { + if (mangaSync != null) { + start(REFRESH) + } + } + +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListSearchAdapter.java b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListSearchAdapter.java deleted file mode 100644 index 9eed6d2690..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListSearchAdapter.java +++ /dev/null @@ -1,61 +0,0 @@ -package eu.kanade.tachiyomi.ui.manga.myanimelist; - -import android.content.Context; -import android.view.LayoutInflater; -import android.view.View; -import android.view.ViewGroup; -import android.widget.ArrayAdapter; -import android.widget.TextView; - -import java.util.ArrayList; -import java.util.List; - -import butterknife.Bind; -import butterknife.ButterKnife; -import eu.kanade.tachiyomi.R; -import eu.kanade.tachiyomi.data.database.models.MangaSync; - -public class MyAnimeListSearchAdapter extends ArrayAdapter { - - public MyAnimeListSearchAdapter(Context context) { - super(context, R.layout.dialog_myanimelist_search_item, new ArrayList<>()); - } - - @Override - public View getView(int position, View view, ViewGroup parent) { - // Get the data item for this position - MangaSync sync = getItem(position); - // Check if an existing view is being reused, otherwise inflate the view - SearchViewHolder holder; // view lookup cache stored in tag - if (view == null) { - LayoutInflater inflater = LayoutInflater.from(getContext()); - view = inflater.inflate(R.layout.dialog_myanimelist_search_item, parent, false); - holder = new SearchViewHolder(view); - view.setTag(holder); - } else { - holder = (SearchViewHolder) view.getTag(); - } - holder.onSetValues(sync); - return view; - } - - public void setItems(List syncs) { - setNotifyOnChange(false); - clear(); - addAll(syncs); - notifyDataSetChanged(); - } - - public static class SearchViewHolder { - - @Bind(R.id.myanimelist_result_title) TextView title; - - public SearchViewHolder(View view) { - ButterKnife.bind(this, view); - } - - public void onSetValues(MangaSync sync) { - title.setText(sync.title); - } - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListSearchAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListSearchAdapter.kt new file mode 100644 index 0000000000..8ce5207d18 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/myanimelist/MyAnimeListSearchAdapter.kt @@ -0,0 +1,46 @@ +package eu.kanade.tachiyomi.ui.manga.myanimelist + +import android.content.Context +import android.view.View +import android.view.ViewGroup +import android.widget.ArrayAdapter +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.MangaSync +import eu.kanade.tachiyomi.util.inflate +import kotlinx.android.synthetic.main.dialog_myanimelist_search_item.view.* +import java.util.* + +class MyAnimeListSearchAdapter(context: Context) : + ArrayAdapter(context, R.layout.dialog_myanimelist_search_item, ArrayList()) { + + override fun getView(position: Int, view: View?, parent: ViewGroup): View { + var v = view + // Get the data item for this position + val sync = getItem(position) + // Check if an existing view is being reused, otherwise inflate the view + val holder: SearchViewHolder // view lookup cache stored in tag + if (v == null) { + v = parent.inflate(R.layout.dialog_myanimelist_search_item) + holder = SearchViewHolder(v) + v.tag = holder + } else { + holder = v.tag as SearchViewHolder + } + holder.onSetValues(sync) + return v + } + + fun setItems(syncs: List) { + setNotifyOnChange(false) + clear() + addAll(syncs) + notifyDataSetChanged() + } + + class SearchViewHolder(private val view: View) { + + fun onSetValues(sync: MangaSync) { + view.myanimelist_result_title.text = sync.title + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt index 6bcfa82cd4..9b9e5d390b 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAboutFragment.kt @@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.BuildConfig import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker import eu.kanade.tachiyomi.data.updater.UpdateDownloader -import eu.kanade.tachiyomi.util.ToastUtil import eu.kanade.tachiyomi.util.toast import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -109,7 +108,7 @@ class SettingsAboutFragment : SettingsNestedFragment() { UpdateDownloader(activity.applicationContext).execute(downloadLink) }.show() } else { - ToastUtil.showShort(activity, getString(R.string.update_check_no_new_updates)) + context.toast(R.string.update_check_no_new_updates) } }, { it.printStackTrace() diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt index 2ef8aa44b2..e51c9c7ab4 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsAdvancedFragment.kt @@ -7,7 +7,7 @@ import com.afollestad.materialdialogs.MaterialDialog import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.database.DatabaseHelper -import eu.kanade.tachiyomi.util.ToastUtil +import eu.kanade.tachiyomi.util.toast import rx.Observable import rx.Subscription import rx.android.schedulers.AndroidSchedulers @@ -74,10 +74,10 @@ class SettingsAdvancedFragment : SettingsNestedFragment() { dialog.incrementProgress(1) }, { dialog.dismiss() - ToastUtil.showShort(activity, getString(R.string.cache_delete_error)) + context.toast(R.string.cache_delete_error) }, { dialog.dismiss() - ToastUtil.showShort(activity, getString(R.string.cache_deleted, deletedFiles.get())) + context.toast(getString(R.string.cache_deleted, deletedFiles.get())) preference.summary = getString(R.string.used_cache, chapterCache.readableSize) }) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt index fcd1bb7142..0dac66c7a8 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ContextExtensions.kt @@ -17,6 +17,15 @@ fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT) Toast.makeText(this, resource, duration).show() } +/** + * Display a toast in this context. + * @param text the text to display. + * @param duration the duration of the toast. Defaults to short. + */ +fun Context.toast(text: String?, duration: Int = Toast.LENGTH_SHORT) { + Toast.makeText(this, text, duration).show() +} + /** * Helper method to create a notification. * @param func the function that will execute inside the builder. diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/ImageViewExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/ImageViewExtensions.kt new file mode 100644 index 0000000000..2456e4f856 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/ImageViewExtensions.kt @@ -0,0 +1,18 @@ +package eu.kanade.tachiyomi.util + +import android.support.annotation.DrawableRes +import android.support.v4.content.ContextCompat +import android.widget.ImageView + +/** + * Set a drawable on a [ImageView] using [ContextCompat] for backwards compatibility. + * + * @param drawable id of drawable resource + */ +fun ImageView.setDrawableCompat(@DrawableRes drawable: Int?) { + if (drawable != null) { + setImageDrawable(ContextCompat.getDrawable(context, drawable)) + } else { + setImageResource(android.R.color.transparent) + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleTextWatcher.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleTextWatcher.kt new file mode 100644 index 0000000000..ee39d58719 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/SimpleTextWatcher.kt @@ -0,0 +1,12 @@ +package eu.kanade.tachiyomi.widget + +import android.text.Editable +import android.text.TextWatcher + +open class SimpleTextWatcher : TextWatcher { + override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} + + override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {} + + override fun afterTextChanged(s: Editable) {} +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt index e37ded4754..7fa2473496 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/widget/preference/LoginDialogPreference.kt @@ -5,8 +5,6 @@ import android.app.DialogFragment import android.content.DialogInterface import android.content.Intent import android.os.Bundle -import android.text.Editable -import android.text.TextWatcher import android.text.method.PasswordTransformationMethod import android.view.View import com.afollestad.materialdialogs.MaterialDialog @@ -14,6 +12,7 @@ import com.dd.processbutton.iml.ActionProcessButton import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.ui.setting.SettingsActivity +import eu.kanade.tachiyomi.widget.SimpleTextWatcher import kotlinx.android.synthetic.main.pref_account_login.view.* import rx.Subscription @@ -54,11 +53,7 @@ abstract class LoginDialogPreference : DialogFragment() { show_password.isEnabled = password.text.isNullOrEmpty() - password.addTextChangedListener(object : TextWatcher { - override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {} - - override fun afterTextChanged(s: Editable) {} - + password.addTextChangedListener(object : SimpleTextWatcher() { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { if (s.length == 0) { show_password.isEnabled = true diff --git a/app/src/main/res/layout/fragment_manga_chapters.xml b/app/src/main/res/layout/fragment_manga_chapters.xml index d2aa66dee1..267404551b 100644 --- a/app/src/main/res/layout/fragment_manga_chapters.xml +++ b/app/src/main/res/layout/fragment_manga_chapters.xml @@ -14,7 +14,7 @@ android:orientation="vertical">