Manga in Kotlin. Expect some errors yet

This commit is contained in:
len 2016-03-13 22:06:32 +01:00
parent a87c65872c
commit f49577bc77
36 changed files with 1928 additions and 2196 deletions

View File

@ -100,7 +100,7 @@ apt {
} }
dependencies { dependencies {
final SUPPORT_LIBRARY_VERSION = '23.2.0' final SUPPORT_LIBRARY_VERSION = '23.2.1'
final DAGGER_VERSION = '2.0.2' final DAGGER_VERSION = '2.0.2'
final OKHTTP_VERSION = '3.2.0' final OKHTTP_VERSION = '3.2.0'
final RETROFIT_VERSION = '2.0.0-beta4' final RETROFIT_VERSION = '2.0.0-beta4'

View File

@ -66,14 +66,14 @@ open class BaseActivity : AppCompatActivity() {
} }
fun snack(text: String?, duration: Int = Snackbar.LENGTH_LONG) { 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 val textView = snack.view.findViewById(android.support.design.R.id.snackbar_text) as TextView
textView.setTextColor(Color.WHITE) textView.setTextColor(Color.WHITE)
snack.show() snack.show()
} }
fun snack(text: String?, actionRes: Int, actionFunc: () -> Unit, 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) val snack = Snackbar.make(view, text ?: getString(R.string.unknown_error), duration)
.setAction(actionRes, { actionFunc() }) .setAction(actionRes, { actionFunc() })

View File

@ -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.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.ui.manga.MangaActivity 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.EndlessGridScrollListener
import eu.kanade.tachiyomi.widget.EndlessListScrollListener import eu.kanade.tachiyomi.widget.EndlessListScrollListener
import kotlinx.android.synthetic.main.fragment_catalogue.* 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 // Set previous selection if it's not a valid source and notify the user
if (!presenter.isValidSource(source)) { if (!presenter.isValidSource(source)) {
spinner.setSelection(presenter.findFirstValidSource()) spinner.setSelection(presenter.findFirstValidSource())
ToastUtil.showShort(activity, R.string.source_requires_login) context.toast(R.string.source_requires_login)
} else { } else {
selectedIndex = position selectedIndex = position
presenter.setEnabledSource(selectedIndex) presenter.setEnabledSource(selectedIndex)
@ -430,7 +430,7 @@ class CatalogueFragment : BaseRxFragment<CataloguePresenter>(), FlexibleViewHold
val selectedManga = adapter.getItem(position) val selectedManga = adapter.getItem(position)
val intent = MangaActivity.newIntent(activity, selectedManga) val intent = MangaActivity.newIntent(activity, selectedManga)
intent.putExtra(MangaActivity.MANGA_ONLINE, true) intent.putExtra(MangaActivity.FROM_CATALOGUE, true)
startActivity(intent) startActivity(intent)
return false return false
} }

View File

@ -8,7 +8,6 @@ import android.support.design.widget.TabLayout
import android.support.v7.view.ActionMode import android.support.v7.view.ActionMode
import android.support.v7.widget.SearchView import android.support.v7.widget.SearchView
import android.view.* import android.view.*
import butterknife.ButterKnife
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
import eu.davidea.flexibleadapter.FlexibleAdapter import eu.davidea.flexibleadapter.FlexibleAdapter
import eu.kanade.tachiyomi.R 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.base.fragment.BaseRxFragment
import eu.kanade.tachiyomi.ui.category.CategoryActivity import eu.kanade.tachiyomi.ui.category.CategoryActivity
import eu.kanade.tachiyomi.ui.main.MainActivity import eu.kanade.tachiyomi.ui.main.MainActivity
import eu.kanade.tachiyomi.util.ToastUtil
import eu.kanade.tachiyomi.util.inflate import eu.kanade.tachiyomi.util.inflate
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import kotlinx.android.synthetic.main.fragment_library.* import kotlinx.android.synthetic.main.fragment_library.*
@ -125,7 +123,6 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
override fun onViewCreated(view: View, savedState: Bundle?) { override fun onViewCreated(view: View, savedState: Bundle?) {
setToolbarTitle(getString(R.string.label_library)) setToolbarTitle(getString(R.string.label_library))
ButterKnife.bind(this, view)
appBar = (activity as MainActivity).appBar appBar = (activity as MainActivity).appBar
tabs = appBar.inflate(R.layout.library_tab_layout) as TabLayout tabs = appBar.inflate(R.layout.library_tab_layout) as TabLayout
@ -369,7 +366,7 @@ class LibraryFragment : BaseRxFragment<LibraryPresenter>(), ActionMode.Callback
startActivityForResult(Intent.createChooser(intent, startActivityForResult(Intent.createChooser(intent,
getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN) getString(R.string.file_select_cover)), REQUEST_IMAGE_OPEN)
} else { } 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() destroyActionModeIfNeeded()
true true
} }
.positiveText(R.string.button_ok) .positiveText(android.R.string.ok)
.negativeText(R.string.button_cancel) .negativeText(android.R.string.cancel)
.show() .show()
} }

View File

@ -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];
}
}
}

View 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]
}
}
}

View File

@ -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);
}
}

View File

@ -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)
}
}

View File

@ -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;
}
}

View File

@ -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()
}
}

View File

@ -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());
}
}
}

View File

@ -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()
}
}
}

View File

@ -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();
}
}

View File

@ -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()
}
}

View File

@ -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;
}
}

View File

@ -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()
}
}

View File

@ -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);
}
}

View File

@ -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
}
}

View File

@ -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);
}
}

View File

@ -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)
}
}

View File

@ -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) {
}
}
}

View File

@ -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
}

View File

@ -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);
}
}
}

View File

@ -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()
}
}
}

View File

@ -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);
}
}
}

View File

@ -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)
}
}
}

View File

@ -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);
}
}
}

View File

@ -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
}
}
}

View File

@ -7,7 +7,6 @@ import eu.kanade.tachiyomi.BuildConfig
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker import eu.kanade.tachiyomi.data.updater.GithubUpdateChecker
import eu.kanade.tachiyomi.data.updater.UpdateDownloader import eu.kanade.tachiyomi.data.updater.UpdateDownloader
import eu.kanade.tachiyomi.util.ToastUtil
import eu.kanade.tachiyomi.util.toast import eu.kanade.tachiyomi.util.toast
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -109,7 +108,7 @@ class SettingsAboutFragment : SettingsNestedFragment() {
UpdateDownloader(activity.applicationContext).execute(downloadLink) UpdateDownloader(activity.applicationContext).execute(downloadLink)
}.show() }.show()
} else { } else {
ToastUtil.showShort(activity, getString(R.string.update_check_no_new_updates)) context.toast(R.string.update_check_no_new_updates)
} }
}, { }, {
it.printStackTrace() it.printStackTrace()

View File

@ -7,7 +7,7 @@ import com.afollestad.materialdialogs.MaterialDialog
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.cache.ChapterCache import eu.kanade.tachiyomi.data.cache.ChapterCache
import eu.kanade.tachiyomi.data.database.DatabaseHelper import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.util.ToastUtil import eu.kanade.tachiyomi.util.toast
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -74,10 +74,10 @@ class SettingsAdvancedFragment : SettingsNestedFragment() {
dialog.incrementProgress(1) dialog.incrementProgress(1)
}, { }, {
dialog.dismiss() dialog.dismiss()
ToastUtil.showShort(activity, getString(R.string.cache_delete_error)) context.toast(R.string.cache_delete_error)
}, { }, {
dialog.dismiss() 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) preference.summary = getString(R.string.used_cache, chapterCache.readableSize)
}) })
} }

View File

@ -17,6 +17,15 @@ fun Context.toast(@StringRes resource: Int, duration: Int = Toast.LENGTH_SHORT)
Toast.makeText(this, resource, duration).show() 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. * Helper method to create a notification.
* @param func the function that will execute inside the builder. * @param func the function that will execute inside the builder.

View File

@ -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)
}
}

View File

@ -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) {}
}

View File

@ -5,8 +5,6 @@ import android.app.DialogFragment
import android.content.DialogInterface import android.content.DialogInterface
import android.content.Intent import android.content.Intent
import android.os.Bundle import android.os.Bundle
import android.text.Editable
import android.text.TextWatcher
import android.text.method.PasswordTransformationMethod import android.text.method.PasswordTransformationMethod
import android.view.View import android.view.View
import com.afollestad.materialdialogs.MaterialDialog import com.afollestad.materialdialogs.MaterialDialog
@ -14,6 +12,7 @@ import com.dd.processbutton.iml.ActionProcessButton
import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.R
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.ui.setting.SettingsActivity import eu.kanade.tachiyomi.ui.setting.SettingsActivity
import eu.kanade.tachiyomi.widget.SimpleTextWatcher
import kotlinx.android.synthetic.main.pref_account_login.view.* import kotlinx.android.synthetic.main.pref_account_login.view.*
import rx.Subscription import rx.Subscription
@ -54,11 +53,7 @@ abstract class LoginDialogPreference : DialogFragment() {
show_password.isEnabled = password.text.isNullOrEmpty() show_password.isEnabled = password.text.isNullOrEmpty()
password.addTextChangedListener(object : TextWatcher { password.addTextChangedListener(object : SimpleTextWatcher() {
override fun beforeTextChanged(s: CharSequence, start: Int, count: Int, after: Int) {}
override fun afterTextChanged(s: Editable) {}
override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) { override fun onTextChanged(s: CharSequence, start: Int, before: Int, count: Int) {
if (s.length == 0) { if (s.length == 0) {
show_password.isEnabled = true show_password.isEnabled = true

View File

@ -14,7 +14,7 @@
android:orientation="vertical"> android:orientation="vertical">
<android.support.v7.widget.RecyclerView <android.support.v7.widget.RecyclerView
android:id="@+id/chapter_list" android:id="@+id/recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginLeft="16dp" android:layout_marginLeft="16dp"
@ -41,7 +41,7 @@
android:theme="@style/AppTheme.Popup"> android:theme="@style/AppTheme.Popup">
<ImageView <ImageView
android:id="@+id/action_sort" android:id="@+id/sort_btn"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_gravity="center" android:layout_gravity="center"
@ -52,9 +52,9 @@
<LinearLayout <LinearLayout
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_toEndOf="@+id/action_sort" android:layout_toEndOf="@+id/sort_btn"
android:layout_toLeftOf="@+id/action_next_unread" android:layout_toLeftOf="@+id/next_unread_btn"
android:layout_toRightOf="@+id/action_sort" android:layout_toRightOf="@+id/sort_btn"
android:gravity="center_vertical"> android:gravity="center_vertical">
<View <View
@ -64,7 +64,7 @@
android:background="@color/white"/> android:background="@color/white"/>
<CheckBox <CheckBox
android:id="@+id/action_show_unread" android:id="@+id/show_unread"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
@ -73,7 +73,7 @@
android:title="@string/action_show_unread"/> android:title="@string/action_show_unread"/>
<CheckBox <CheckBox
android:id="@+id/action_show_downloaded" android:id="@+id/show_downloaded"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_weight="1" android:layout_weight="1"
@ -90,7 +90,7 @@
</LinearLayout> </LinearLayout>
<ImageView <ImageView
android:id="@+id/action_next_unread" android:id="@+id/next_unread_btn"
android:layout_width="wrap_content" android:layout_width="wrap_content"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_alignParentRight="true" android:layout_alignParentRight="true"

View File

@ -6,7 +6,7 @@ buildscript {
jcenter() jcenter()
} }
dependencies { dependencies {
classpath 'com.android.tools.build:gradle:2.0.0-beta6' classpath 'com.android.tools.build:gradle:2.1.0-alpha1'
classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8' classpath 'com.neenbedankt.gradle.plugins:android-apt:1.8'
classpath 'me.tatarka:gradle-retrolambda:3.2.4' classpath 'me.tatarka:gradle-retrolambda:3.2.4'
classpath 'com.github.ben-manes:gradle-versions-plugin:0.12.0' classpath 'com.github.ben-manes:gradle-versions-plugin:0.12.0'