mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-10-30 22:07:57 +01:00 
			
		
		
		
	Manga in Kotlin. Expect some errors yet
This commit is contained in:
		| @@ -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() }) | ||||
|   | ||||
| @@ -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<CataloguePresenter>(), 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<CataloguePresenter>(), 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 | ||||
|     } | ||||
|   | ||||
| @@ -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<LibraryPresenter>(), 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<LibraryPresenter>(), 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<LibraryPresenter>(), 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() | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -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<MangaPresenter> { | ||||
|  | ||||
|     @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]; | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
							
								
								
									
										118
									
								
								app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										118
									
								
								app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaActivity.kt
									
									
									
									
									
										Normal file
									
								
							| @@ -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<MangaPresenter>() { | ||||
|  | ||||
|     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] | ||||
|         } | ||||
|  | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<MangaActivity> { | ||||
|  | ||||
|     @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<Manga> 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); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<MangaActivity>() { | ||||
|  | ||||
|     /** | ||||
|      * 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) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<ChaptersHolder, Chapter> { | ||||
|  | ||||
|     private ChaptersFragment fragment; | ||||
|  | ||||
|     public ChaptersAdapter(ChaptersFragment fragment) { | ||||
|         this.fragment = fragment; | ||||
|         mItems = new ArrayList<>(); | ||||
|         setHasStableIds(true); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void updateDataSet(String param) {} | ||||
|  | ||||
|     @Override | ||||
|     public ChaptersHolder onCreateViewHolder(ViewGroup parent, int viewType) { | ||||
|         View v = LayoutInflater.from(fragment.getActivity()).inflate(R.layout.item_chapter, parent, false); | ||||
|         return new ChaptersHolder(v, this, fragment); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onBindViewHolder(ChaptersHolder holder, int position) { | ||||
|         final Chapter chapter = getItem(position); | ||||
|         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<Chapter> chapters) { | ||||
|         mItems = chapters; | ||||
|         notifyDataSetChanged(); | ||||
|     } | ||||
|  | ||||
|     public ChaptersFragment getFragment() { | ||||
|         return fragment; | ||||
|     } | ||||
| } | ||||
| @@ -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<ChaptersHolder, Chapter>() { | ||||
|  | ||||
|     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<Chapter>) { | ||||
|         mItems = chapters | ||||
|         notifyDataSetChanged() | ||||
|     } | ||||
| } | ||||
| @@ -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<ChaptersPresenter> implements | ||||
|         ActionMode.Callback, FlexibleViewHolder.OnListItemClickListener { | ||||
|  | ||||
|     @Bind(R.id.chapter_list) RecyclerView recyclerView; | ||||
|     @Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh; | ||||
|     @Bind(R.id.toolbar_bottom) ViewGroup toolbarBottom; | ||||
|  | ||||
|     @Bind(R.id.action_sort) ImageView sortBtn; | ||||
|     @Bind(R.id.action_next_unread) ImageView nextUnreadBtn; | ||||
|     @Bind(R.id.action_show_unread) CheckBox readCb; | ||||
|     @Bind(R.id.action_show_downloaded) CheckBox downloadedCb; | ||||
|  | ||||
|     private ChaptersAdapter adapter; | ||||
|     private LinearLayoutManager linearLayout; | ||||
|     private ActionMode actionMode; | ||||
|  | ||||
|     private Subscription downloadProgressSubscription; | ||||
|  | ||||
|     public static ChaptersFragment newInstance() { | ||||
|         return new ChaptersFragment(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public 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<Chapter> chapters) { | ||||
|         // If the list is empty, fetch chapters from source if the conditions are met | ||||
|         // We use presenter chapters instead because they are always unfiltered | ||||
|         if (getPresenter().getChapters().isEmpty()) | ||||
|             initialFetchChapters(); | ||||
|  | ||||
|         destroyActionModeIfNeeded(); | ||||
|         adapter.setItems(chapters); | ||||
|     } | ||||
|  | ||||
|     private void initialFetchChapters() { | ||||
|         // Only fetch if this view is from the catalog and it hasn't requested previously | ||||
|         if (isCatalogueManga() && !getPresenter().hasRequested()) { | ||||
|             fetchChapters(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void fetchChapters() { | ||||
|         if (getPresenter().getManga() != null) { | ||||
|             swipeRefresh.setRefreshing(true); | ||||
|             getPresenter().fetchChaptersFromSource(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void onFetchChaptersDone() { | ||||
|         swipeRefresh.setRefreshing(false); | ||||
|     } | ||||
|  | ||||
|     public void onFetchChaptersError(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<Chapter> 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<Chapter> getSelectedChapters() { | ||||
|         // Create a blocking copy of the selected chapters. | ||||
|         // When the action mode is closed the list is cleared. If we use background | ||||
|         // threads with this observable, some emissions could be lost. | ||||
|         List<Chapter> chapters = Observable.from(adapter.getSelectedItems()) | ||||
|                 .map(adapter::getItem).toList().toBlocking().single(); | ||||
|  | ||||
|         return Observable.from(chapters); | ||||
|     } | ||||
|  | ||||
|     public void destroyActionModeIfNeeded() { | ||||
|         if (actionMode != null) { | ||||
|             actionMode.finish(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     protected boolean onSelectAll() { | ||||
|         adapter.selectAll(); | ||||
|         setContextTitle(adapter.getSelectedItemCount()); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     protected boolean onMarkAsRead(Observable<Chapter> chapters) { | ||||
|         getPresenter().markChaptersRead(chapters, true); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     protected boolean onMarkAsUnread(Observable<Chapter> chapters) { | ||||
|         getPresenter().markChaptersRead(chapters, false); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     public boolean onMarkPreviousAsRead(Chapter chapter) { | ||||
|         getPresenter().markPreviousChaptersAsRead(chapter); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     protected boolean onDownload(Observable<Chapter> chapters) { | ||||
|         DownloadService.start(getActivity()); | ||||
|  | ||||
|         Observable<Chapter> observable = chapters | ||||
|                 .doOnCompleted(adapter::notifyDataSetChanged); | ||||
|  | ||||
|         getPresenter().downloadChapters(observable); | ||||
|         destroyActionModeIfNeeded(); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     protected boolean onDelete(Observable<Chapter> chapters) { | ||||
|         int size = adapter.getSelectedItemCount(); | ||||
|  | ||||
|         MaterialDialog dialog = new MaterialDialog.Builder(getActivity()) | ||||
|                 .title(R.string.deleting) | ||||
|                 .progress(false, size, true) | ||||
|                 .cancelable(false) | ||||
|                 .show(); | ||||
|  | ||||
|         Observable<Chapter> observable = chapters | ||||
|                 .concatMap(chapter -> { | ||||
|                     getPresenter().deleteChapter(chapter); | ||||
|                     return Observable.just(chapter); | ||||
|                 }) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .doOnNext(chapter -> { | ||||
|                     dialog.incrementProgress(1); | ||||
|                     chapter.status = Download.NOT_DOWNLOADED; | ||||
|                 }) | ||||
|                 .doOnCompleted(adapter::notifyDataSetChanged) | ||||
|                 .finallyDo(dialog::dismiss); | ||||
|  | ||||
|         getPresenter().deleteChapters(observable); | ||||
|         destroyActionModeIfNeeded(); | ||||
|         return true; | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public boolean onListItemClick(int position) { | ||||
|         if (actionMode != null && adapter.getMode() == ChaptersAdapter.MODE_MULTI) { | ||||
|             toggleSelection(position); | ||||
|             return true; | ||||
|         } else { | ||||
|             openChapter(adapter.getItem(position)); | ||||
|             return false; | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onListItemLongClick(int position) { | ||||
|         if (actionMode == null) | ||||
|             actionMode = getBaseActivity().startSupportActionMode(this); | ||||
|  | ||||
|         toggleSelection(position); | ||||
|     } | ||||
|  | ||||
|     private void toggleSelection(int position) { | ||||
|         adapter.toggleSelection(position, false); | ||||
|  | ||||
|         int count = adapter.getSelectedItemCount(); | ||||
|         if (count == 0) { | ||||
|             actionMode.finish(); | ||||
|         } else { | ||||
|             setContextTitle(count); | ||||
|             actionMode.invalidate(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void setContextTitle(int count) { | ||||
|         actionMode.setTitle(getString(R.string.label_selected, count)); | ||||
|     } | ||||
|  | ||||
|     public void setSortIcon() { | ||||
|         if (sortBtn != null) { | ||||
|             boolean aToZ = getPresenter().getSortOrder(); | ||||
|             sortBtn.setImageResource(!aToZ ? R.drawable.ic_expand_less_white_36dp : R.drawable.ic_expand_more_white_36dp); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void setReadFilter() { | ||||
|         if (readCb != null) { | ||||
|             readCb.setChecked(getPresenter().onlyUnread()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void setDownloadedFilter() { | ||||
|         if (downloadedCb != null) { | ||||
|             downloadedCb.setChecked(getPresenter().onlyDownloaded()); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<ChaptersPresenter>(), 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<Chapter>) { | ||||
|         // 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<Chapter>() | ||||
|  | ||||
|                     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<Chapter> { | ||||
|         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<Chapter>) { | ||||
|         presenter.markChaptersRead(chapters, true) | ||||
|     } | ||||
|  | ||||
|     fun onMarkAsUnread(chapters: Observable<Chapter>) { | ||||
|         presenter.markChaptersRead(chapters, false) | ||||
|     } | ||||
|  | ||||
|     fun onMarkPreviousAsRead(chapter: Chapter) { | ||||
|         presenter.markPreviousChaptersAsRead(chapter) | ||||
|     } | ||||
|  | ||||
|     fun onDownload(chapters: Observable<Chapter>) { | ||||
|         DownloadService.start(activity) | ||||
|  | ||||
|         val observable = chapters.doOnCompleted { adapter.notifyDataSetChanged() } | ||||
|  | ||||
|         presenter.downloadChapters(observable) | ||||
|         destroyActionModeIfNeeded() | ||||
|     } | ||||
|  | ||||
|     fun onDelete(chapters: Observable<Chapter>) { | ||||
|         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() | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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> 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(); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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() | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<ChaptersFragment> { | ||||
|  | ||||
|     @Inject DatabaseHelper db; | ||||
|     @Inject SourceManager sourceManager; | ||||
|     @Inject PreferencesHelper preferences; | ||||
|     @Inject DownloadManager downloadManager; | ||||
|  | ||||
|     private Manga manga; | ||||
|     private Source source; | ||||
|     private List<Chapter> chapters; | ||||
|     @State boolean hasRequested; | ||||
|  | ||||
|     private PublishSubject<List<Chapter>> 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<Pair<Integer, Integer>> getOnlineChaptersObs() { | ||||
|         return source.pullChaptersFromNetwork(manga.url) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .flatMap(chapters -> db.insertOrRemoveChapters(manga, chapters, source)) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()); | ||||
|     } | ||||
|  | ||||
|     private Observable<List<Chapter>> getDbChaptersObs() { | ||||
|         return chaptersSubject.flatMap(this::applyChapterFilters) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()); | ||||
|     } | ||||
|  | ||||
|     private Observable<List<Chapter>> applyChapterFilters(List<Chapter> chapters) { | ||||
|         Observable<Chapter> observable = Observable.from(chapters) | ||||
|                 .subscribeOn(Schedulers.io()); | ||||
|         if (onlyUnread()) { | ||||
|             observable = observable.filter(chapter -> !chapter.read); | ||||
|         } | ||||
|         if (onlyDownloaded()) { | ||||
|             observable = observable.filter(chapter -> chapter.status == Download.DOWNLOADED); | ||||
|         } | ||||
|         return observable.toSortedList((chapter, chapter2) -> 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<Download> getChapterStatusObs() { | ||||
|         return downloadManager.getQueue().getStatusObservable() | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .filter(download -> download.manga.id.equals(manga.id)) | ||||
|                 .doOnNext(this::updateChapterStatus); | ||||
|     } | ||||
|  | ||||
|     public void updateChapterStatus(Download download) { | ||||
|         for (Chapter chapter : chapters) { | ||||
|             if (download.chapter.id.equals(chapter.id)) { | ||||
|                 chapter.status = download.getStatus(); | ||||
|                 break; | ||||
|             } | ||||
|         } | ||||
|         if (onlyDownloaded() && download.getStatus() == Download.DOWNLOADED) | ||||
|             refreshChapters(); | ||||
|     } | ||||
|  | ||||
|     public Observable<Download> getDownloadProgressObs() { | ||||
|         return downloadManager.getQueue().getProgressObservable() | ||||
|                 .filter(download -> download.manga.id.equals(manga.id)) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()); | ||||
|     } | ||||
|  | ||||
|     public void onOpenChapter(Chapter chapter) { | ||||
|         EventBus.getDefault().postSticky(new ReaderEvent(source, manga, chapter)); | ||||
|     } | ||||
|  | ||||
|     public Chapter getNextUnreadChapter() { | ||||
|         return db.getNextUnreadChapter(manga).executeAsBlocking(); | ||||
|     } | ||||
|  | ||||
|     public void markChaptersRead(Observable<Chapter> selectedChapters, boolean read) { | ||||
|         add(selectedChapters | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .map(chapter -> { | ||||
|                     chapter.read = read; | ||||
|                     if (!read) chapter.last_page_read = 0; | ||||
|                     return chapter; | ||||
|                 }) | ||||
|                 .toList() | ||||
|                 .flatMap(chapters -> db.insertChapters(chapters).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<Chapter> selectedChapters) { | ||||
|         add(selectedChapters | ||||
|                 .toList() | ||||
|                 .subscribe(chapters -> { | ||||
|                     EventBus.getDefault().postSticky(new DownloadChaptersEvent(manga, chapters)); | ||||
|                 })); | ||||
|     } | ||||
|  | ||||
|     public void deleteChapters(Observable<Chapter> selectedChapters) { | ||||
|         add(selectedChapters | ||||
|                 .subscribe(chapter -> { | ||||
|                     downloadManager.getQueue().remove(chapter); | ||||
|                 }, error -> { | ||||
|                     Timber.e(error.getMessage()); | ||||
|                 }, () -> { | ||||
|                     if (onlyDownloaded()) | ||||
|                         refreshChapters(); | ||||
|                 })); | ||||
|     } | ||||
|  | ||||
|     public void deleteChapter(Chapter chapter) { | ||||
|         downloadManager.deleteChapter(source, manga, chapter); | ||||
|     } | ||||
|  | ||||
|     public void revertSortOrder() { | ||||
|         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<Chapter> getChapters() { | ||||
|         return chapters; | ||||
|     } | ||||
|  | ||||
|     public boolean hasRequested() { | ||||
|         return hasRequested; | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<ChaptersFragment>() { | ||||
|  | ||||
|     @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<Chapter> | ||||
|         private set | ||||
|  | ||||
|     lateinit var chaptersSubject: PublishSubject<List<Chapter>> | ||||
|         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<Pair<Int, Int>> { | ||||
|         return source.pullChaptersFromNetwork(manga.url) | ||||
|                 .subscribeOn(Schedulers.io()) | ||||
|                 .flatMap { chapters -> db.insertOrRemoveChapters(manga, chapters, source) } | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|     } | ||||
|  | ||||
|     fun getDbChaptersObs(): Observable<List<Chapter>> { | ||||
|         return chaptersSubject | ||||
|                 .flatMap { applyChapterFilters(it) } | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|     } | ||||
|  | ||||
|     fun getChapterStatusObs(): Observable<Download> { | ||||
|         return downloadManager.queue.statusObservable | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .filter { download -> download.manga.id == manga.id } | ||||
|                 .doOnNext { updateChapterStatus(it) } | ||||
|     } | ||||
|  | ||||
|     private fun applyChapterFilters(chapters: List<Chapter>): Observable<List<Chapter>> { | ||||
|         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<Chapter>, 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<Chapter>) { | ||||
|         add(selectedChapters.toList() | ||||
|                 .subscribe { chapters -> EventBus.getDefault().postSticky(DownloadChaptersEvent(manga, chapters)) }) | ||||
|     } | ||||
|  | ||||
|     fun deleteChapters(selectedChapters: Observable<Chapter>) { | ||||
|         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() | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<MangaInfoPresenter> { | ||||
|     /** | ||||
|      * 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); | ||||
|     } | ||||
| } | ||||
| @@ -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<MangaInfoPresenter>() { | ||||
|  | ||||
|     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 | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<MangaInfoFragment> { | ||||
|  | ||||
|     /** | ||||
|      * 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<Manga> 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); | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<MangaInfoFragment>() { | ||||
|  | ||||
|     /** | ||||
|      * 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<Manga> { | ||||
|         return source.pullMangaFromNetwork(manga.url) | ||||
|                 .flatMap { networkManga -> | ||||
|                     manga.copyFrom(networkManga) | ||||
|                     db.insertManga(manga).executeAsBlocking() | ||||
|                     Observable.just<Manga>(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) | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<String> querySubject = PublishSubject.create(); | ||||
|         searchText.addTextChangedListener(new SimpleTextChangeListener() { | ||||
|             @Override | ||||
|             public void onTextChanged(CharSequence s, int start, int before, int count) { | ||||
|                 querySubject.onNext(s.toString()); | ||||
|             } | ||||
|         }); | ||||
|  | ||||
|         // Listen to text changes | ||||
|         searchSubscription = querySubject.debounce(1, TimeUnit.SECONDS) | ||||
|                 .observeOn(AndroidSchedulers.mainThread()) | ||||
|                 .subscribe(this::search); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public void onPause() { | ||||
|         if (searchSubscription != null) { | ||||
|             searchSubscription.unsubscribe(); | ||||
|         } | ||||
|         super.onPause(); | ||||
|     } | ||||
|  | ||||
|     private void onPositiveButtonClick() { | ||||
|         if (adapter != null && selectedItem != null) { | ||||
|             getPresenter().registerManga(selectedItem); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void search(String query) { | ||||
|         if (!TextUtils.isEmpty(query)) { | ||||
|             searchResults.setVisibility(View.GONE); | ||||
|             progressBar.setVisibility(View.VISIBLE); | ||||
|             getPresenter().searchManga(query); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void onSearchResults(List<MangaSync> results) { | ||||
|         selectedItem = null; | ||||
|         progressBar.setVisibility(View.GONE); | ||||
|         searchResults.setVisibility(View.VISIBLE); | ||||
|         adapter.setItems(results); | ||||
|     } | ||||
|  | ||||
|     public void onSearchResultsError() { | ||||
|         progressBar.setVisibility(View.GONE); | ||||
|         searchResults.setVisibility(View.VISIBLE); | ||||
|         adapter.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) { | ||||
|  | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<String> | ||||
|         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<String>() | ||||
|  | ||||
|         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<MangaSync>) { | ||||
|         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 | ||||
|  | ||||
| } | ||||
| @@ -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<MyAnimeListPresenter> { | ||||
|  | ||||
|     @Bind(R.id.myanimelist_title) TextView title; | ||||
|     @Bind(R.id.myanimelist_chapters) TextView chapters; | ||||
|     @Bind(R.id.myanimelist_score) TextView score; | ||||
|     @Bind(R.id.myanimelist_status) TextView status; | ||||
|     @Bind(R.id.swipe_refresh) SwipeRefreshLayout swipeRefresh; | ||||
|  | ||||
|     private MyAnimeListDialogFragment dialog; | ||||
|  | ||||
|     private DecimalFormat decimalFormat = new DecimalFormat("#.##"); | ||||
|  | ||||
|     private final static String SEARCH_FRAGMENT_TAG = "mal_search"; | ||||
|  | ||||
|     public static MyAnimeListFragment newInstance() { | ||||
|         return new MyAnimeListFragment(); | ||||
|     } | ||||
|  | ||||
|     @Override | ||||
|     public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { | ||||
|         View view = inflater.inflate(R.layout.fragment_myanimelist, container, false); | ||||
|         ButterKnife.bind(this, view); | ||||
|  | ||||
|         swipeRefresh.setEnabled(false); | ||||
|         swipeRefresh.setOnRefreshListener(() -> getPresenter().refresh()); | ||||
|         return view; | ||||
|     } | ||||
|  | ||||
|     public void setMangaSync(MangaSync mangaSync) { | ||||
|         swipeRefresh.setEnabled(mangaSync != null); | ||||
|         if (mangaSync != null) { | ||||
|             title.setText(mangaSync.title); | ||||
|             chapters.setText(mangaSync.last_chapter_read + "/" + | ||||
|                     (mangaSync.total_chapters > 0 ? mangaSync.total_chapters : "-")); | ||||
|             score.setText(mangaSync.score == 0 ? "-" : decimalFormat.format(mangaSync.score)); | ||||
|             status.setText(getPresenter().myAnimeList.getStatus(mangaSync.status)); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void onRefreshDone() { | ||||
|         swipeRefresh.setRefreshing(false); | ||||
|     } | ||||
|  | ||||
|     public void onRefreshError() { | ||||
|         swipeRefresh.setRefreshing(false); | ||||
|     } | ||||
|  | ||||
|     public void setSearchResults(List<MangaSync> results) { | ||||
|         findSearchFragmentIfNeeded(); | ||||
|  | ||||
|         if (dialog != null) { | ||||
|             dialog.onSearchResults(results); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     public void setSearchResultsError() { | ||||
|         findSearchFragmentIfNeeded(); | ||||
|  | ||||
|         if (dialog != null) { | ||||
|             dialog.onSearchResultsError(); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     private void findSearchFragmentIfNeeded() { | ||||
|         if (dialog == null) { | ||||
|             dialog = (MyAnimeListDialogFragment) getChildFragmentManager() | ||||
|                     .findFragmentByTag(SEARCH_FRAGMENT_TAG); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @OnClick(R.id.myanimelist_title_layout) | ||||
|     void onTitleClick() { | ||||
|         if (dialog == null) | ||||
|             dialog = MyAnimeListDialogFragment.newInstance(); | ||||
|  | ||||
|         getPresenter().restartSearch(); | ||||
|         dialog.show(getChildFragmentManager(), SEARCH_FRAGMENT_TAG); | ||||
|     } | ||||
|  | ||||
|     @OnClick(R.id.myanimelist_status_layout) | ||||
|     void onStatusClick() { | ||||
|         if (getPresenter().mangaSync == null) | ||||
|             return; | ||||
|  | ||||
|         Context ctx = getActivity(); | ||||
|         new MaterialDialog.Builder(ctx) | ||||
|                 .title(R.string.status) | ||||
|                 .items(getPresenter().getAllStatus(ctx)) | ||||
|                 .itemsCallbackSingleChoice(getPresenter().getIndexFromStatus(), | ||||
|                         (materialDialog, view, i, charSequence) -> { | ||||
|                             getPresenter().setStatus(i); | ||||
|                             status.setText("..."); | ||||
|                             return true; | ||||
|                         }) | ||||
|                 .show(); | ||||
|     } | ||||
|  | ||||
|     @OnClick(R.id.myanimelist_chapters_layout) | ||||
|     void onChaptersClick() { | ||||
|         if (getPresenter().mangaSync == null) | ||||
|             return; | ||||
|  | ||||
|         MaterialDialog dialog = new MaterialDialog.Builder(getActivity()) | ||||
|                 .title(R.string.chapters) | ||||
|                 .customView(R.layout.dialog_myanimelist_chapters, false) | ||||
|                 .positiveText(R.string.button_ok) | ||||
|                 .negativeText(R.string.button_cancel) | ||||
|                 .onPositive((materialDialog, dialogAction) -> { | ||||
|                     View view = materialDialog.getCustomView(); | ||||
|                     if (view != null) { | ||||
|                         NumberPicker np = (NumberPicker) view.findViewById(R.id.chapters_picker); | ||||
|                         getPresenter().setLastChapterRead(np.getValue()); | ||||
|                         chapters.setText("..."); | ||||
|                     } | ||||
|                 }) | ||||
|                 .show(); | ||||
|  | ||||
|         View view = dialog.getCustomView(); | ||||
|         if (view != null) { | ||||
|             NumberPicker np  = (NumberPicker) view.findViewById(R.id.chapters_picker); | ||||
|             // Set initial value | ||||
|             np.setValue(getPresenter().mangaSync.last_chapter_read); | ||||
|             // Don't allow to go from 0 to 9999 | ||||
|             np.setWrapSelectorWheel(false); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @OnClick(R.id.myanimelist_score_layout) | ||||
|     void onScoreClick() { | ||||
|         if (getPresenter().mangaSync == null) | ||||
|             return; | ||||
|  | ||||
|         MaterialDialog dialog = new MaterialDialog.Builder(getActivity()) | ||||
|                 .title(R.string.score) | ||||
|                 .customView(R.layout.dialog_myanimelist_score, false) | ||||
|                 .positiveText(R.string.button_ok) | ||||
|                 .negativeText(R.string.button_cancel) | ||||
|                 .onPositive((materialDialog, dialogAction) -> { | ||||
|                     View view = materialDialog.getCustomView(); | ||||
|                     if (view != null) { | ||||
|                         NumberPicker np = (NumberPicker) view.findViewById(R.id.score_picker); | ||||
|                         getPresenter().setScore(np.getValue()); | ||||
|                         score.setText("..."); | ||||
|                     } | ||||
|                 }) | ||||
|                 .show(); | ||||
|  | ||||
|         View view = dialog.getCustomView(); | ||||
|         if (view != null) { | ||||
|             NumberPicker np  = (NumberPicker) view.findViewById(R.id.score_picker); | ||||
|             // Set initial value | ||||
|             np.setValue((int) getPresenter().mangaSync.score); | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -0,0 +1,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<MyAnimeListPresenter>() { | ||||
|  | ||||
|     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<MangaSync>) { | ||||
|         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() | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<MyAnimeListFragment> { | ||||
|  | ||||
|     @Inject DatabaseHelper db; | ||||
|     @Inject MangaSyncManager syncManager; | ||||
|  | ||||
|     protected MyAnimeList myAnimeList; | ||||
|     protected Manga manga; | ||||
|     protected MangaSync mangaSync; | ||||
|  | ||||
|     private String query; | ||||
|  | ||||
|     private static final int GET_MANGA_SYNC = 1; | ||||
|     private static final int GET_SEARCH_RESULTS = 2; | ||||
|     private static final int REFRESH = 3; | ||||
|  | ||||
|     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<List<MangaSync>> getSearchResultsObservable() { | ||||
|         Observable<List<MangaSync>> 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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<MyAnimeListFragment>() { | ||||
|  | ||||
|     @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<List<MangaSync>> { | ||||
|         return query?.let { query -> | ||||
|             val observable: Observable<List<MangaSync>> | ||||
|             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<PutResult> { | ||||
|         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<MangaSync>(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<String> { | ||||
|         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) | ||||
|         } | ||||
|     } | ||||
|  | ||||
| } | ||||
| @@ -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<MangaSync> { | ||||
|  | ||||
|     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<MangaSync> 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); | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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<MangaSync>(context, R.layout.dialog_myanimelist_search_item, ArrayList<MangaSync>()) { | ||||
|  | ||||
|     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<MangaSync>) { | ||||
|         setNotifyOnChange(false) | ||||
|         clear() | ||||
|         addAll(syncs) | ||||
|         notifyDataSetChanged() | ||||
|     } | ||||
|  | ||||
|     class SearchViewHolder(private val view: View) { | ||||
|  | ||||
|         fun onSetValues(sync: MangaSync) { | ||||
|             view.myanimelist_result_title.text = sync.title | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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() | ||||
|   | ||||
| @@ -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) | ||||
|                 }) | ||||
|     } | ||||
|   | ||||
| @@ -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. | ||||
|   | ||||
| @@ -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) | ||||
|     } | ||||
| } | ||||
| @@ -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) {} | ||||
| } | ||||
| @@ -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 | ||||
|   | ||||
		Reference in New Issue
	
	Block a user