Update files to use unix line endings
cmd: `find . -type f -print0 | xargs -0 dos2unix`
This commit is contained in:
@@ -1,193 +1,193 @@
|
||||
package eu.kanade.tachiyomi.ui.manga
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.support.RouterPagerAdapter
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
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.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
|
||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import kotlinx.android.synthetic.main.manga_controller.*
|
||||
import rx.Subscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
class MangaController : RxController, TabbedController {
|
||||
|
||||
constructor(manga: Manga?, fromCatalogue: Boolean = false) : super(Bundle().apply {
|
||||
putLong(MANGA_EXTRA, manga?.id ?: 0)
|
||||
putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
|
||||
}) {
|
||||
this.manga = manga
|
||||
if (manga != null) {
|
||||
source = Injekt.get<SourceManager>().getOrStub(manga.source)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(mangaId: Long) : this(
|
||||
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
|
||||
|
||||
var manga: Manga? = null
|
||||
private set
|
||||
|
||||
var source: Source? = null
|
||||
private set
|
||||
|
||||
private var adapter: MangaDetailAdapter? = null
|
||||
|
||||
val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
|
||||
|
||||
val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create()
|
||||
|
||||
val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
|
||||
|
||||
val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
|
||||
|
||||
private val trackingIconRelay: BehaviorRelay<Boolean> = BehaviorRelay.create()
|
||||
|
||||
private var trackingIconSubscription: Subscription? = null
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return manga?.title
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.manga_controller, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
if (manga == null || source == null) return
|
||||
|
||||
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
||||
|
||||
adapter = MangaDetailAdapter()
|
||||
manga_pager.offscreenPageLimit = 3
|
||||
manga_pager.adapter = adapter
|
||||
|
||||
if (!fromCatalogue)
|
||||
manga_pager.currentItem = CHAPTERS_CONTROLLER
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
adapter = null
|
||||
}
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeStarted(handler, type)
|
||||
if (type.isEnter) {
|
||||
activity?.tabs?.setupWithViewPager(manga_pager)
|
||||
trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeEnded(handler, type)
|
||||
if (manga == null || source == null) {
|
||||
activity?.toast(R.string.manga_not_in_db)
|
||||
router.popController(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureTabs(tabs: TabLayout) {
|
||||
with(tabs) {
|
||||
tabGravity = TabLayout.GRAVITY_FILL
|
||||
tabMode = TabLayout.MODE_FIXED
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanupTabs(tabs: TabLayout) {
|
||||
trackingIconSubscription?.unsubscribe()
|
||||
setTrackingIconInternal(false)
|
||||
}
|
||||
|
||||
fun setTrackingIcon(visible: Boolean) {
|
||||
trackingIconRelay.call(visible)
|
||||
}
|
||||
|
||||
private fun setTrackingIconInternal(visible: Boolean) {
|
||||
val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return
|
||||
val drawable = if (visible)
|
||||
VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null)
|
||||
else null
|
||||
|
||||
val view = tabField.get(tab) as LinearLayout
|
||||
val textView = view.getChildAt(1) as TextView
|
||||
textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
|
||||
textView.compoundDrawablePadding = if (visible) 4 else 0
|
||||
}
|
||||
|
||||
private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) {
|
||||
|
||||
private val tabCount = if (Injekt.get<TrackManager>().hasLoggedServices()) 3 else 2
|
||||
|
||||
private val tabTitles = listOf(
|
||||
R.string.manga_detail_tab,
|
||||
R.string.manga_chapters_tab,
|
||||
R.string.manga_tracking_tab)
|
||||
.map { resources!!.getString(it) }
|
||||
|
||||
override fun getCount(): Int {
|
||||
return tabCount
|
||||
}
|
||||
|
||||
override fun configureRouter(router: Router, position: Int) {
|
||||
if (!router.hasRootController()) {
|
||||
val controller = when (position) {
|
||||
INFO_CONTROLLER -> MangaInfoController()
|
||||
CHAPTERS_CONTROLLER -> ChaptersController()
|
||||
TRACK_CONTROLLER -> TrackController()
|
||||
else -> error("Wrong position $position")
|
||||
}
|
||||
router.setRoot(RouterTransaction.with(controller))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return tabTitles[position]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
||||
const val MANGA_EXTRA = "manga"
|
||||
|
||||
const val INFO_CONTROLLER = 0
|
||||
const val CHAPTERS_CONTROLLER = 1
|
||||
const val TRACK_CONTROLLER = 2
|
||||
|
||||
private val tabField = TabLayout.Tab::class.java.getDeclaredField("view")
|
||||
.apply { isAccessible = true }
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga
|
||||
|
||||
import android.Manifest.permission.WRITE_EXTERNAL_STORAGE
|
||||
import android.os.Bundle
|
||||
import com.google.android.material.tabs.TabLayout
|
||||
import androidx.vectordrawable.graphics.drawable.VectorDrawableCompat
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.LinearLayout
|
||||
import android.widget.TextView
|
||||
import com.bluelinelabs.conductor.ControllerChangeHandler
|
||||
import com.bluelinelabs.conductor.ControllerChangeType
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import com.bluelinelabs.conductor.RouterTransaction
|
||||
import com.bluelinelabs.conductor.support.RouterPagerAdapter
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
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.track.TrackManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.source.SourceManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.RxController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.TabbedController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.requestPermissionsSafe
|
||||
import eu.kanade.tachiyomi.ui.manga.chapter.ChaptersController
|
||||
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoController
|
||||
import eu.kanade.tachiyomi.ui.manga.track.TrackController
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.main_activity.*
|
||||
import kotlinx.android.synthetic.main.manga_controller.*
|
||||
import rx.Subscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
class MangaController : RxController, TabbedController {
|
||||
|
||||
constructor(manga: Manga?, fromCatalogue: Boolean = false) : super(Bundle().apply {
|
||||
putLong(MANGA_EXTRA, manga?.id ?: 0)
|
||||
putBoolean(FROM_CATALOGUE_EXTRA, fromCatalogue)
|
||||
}) {
|
||||
this.manga = manga
|
||||
if (manga != null) {
|
||||
source = Injekt.get<SourceManager>().getOrStub(manga.source)
|
||||
}
|
||||
}
|
||||
|
||||
constructor(mangaId: Long) : this(
|
||||
Injekt.get<DatabaseHelper>().getManga(mangaId).executeAsBlocking())
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : this(bundle.getLong(MANGA_EXTRA))
|
||||
|
||||
var manga: Manga? = null
|
||||
private set
|
||||
|
||||
var source: Source? = null
|
||||
private set
|
||||
|
||||
private var adapter: MangaDetailAdapter? = null
|
||||
|
||||
val fromCatalogue = args.getBoolean(FROM_CATALOGUE_EXTRA, false)
|
||||
|
||||
val lastUpdateRelay: BehaviorRelay<Date> = BehaviorRelay.create()
|
||||
|
||||
val chapterCountRelay: BehaviorRelay<Float> = BehaviorRelay.create()
|
||||
|
||||
val mangaFavoriteRelay: PublishRelay<Boolean> = PublishRelay.create()
|
||||
|
||||
private val trackingIconRelay: BehaviorRelay<Boolean> = BehaviorRelay.create()
|
||||
|
||||
private var trackingIconSubscription: Subscription? = null
|
||||
|
||||
override fun getTitle(): String? {
|
||||
return manga?.title
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.manga_controller, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
if (manga == null || source == null) return
|
||||
|
||||
requestPermissionsSafe(arrayOf(WRITE_EXTERNAL_STORAGE), 301)
|
||||
|
||||
adapter = MangaDetailAdapter()
|
||||
manga_pager.offscreenPageLimit = 3
|
||||
manga_pager.adapter = adapter
|
||||
|
||||
if (!fromCatalogue)
|
||||
manga_pager.currentItem = CHAPTERS_CONTROLLER
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
adapter = null
|
||||
}
|
||||
|
||||
override fun onChangeStarted(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeStarted(handler, type)
|
||||
if (type.isEnter) {
|
||||
activity?.tabs?.setupWithViewPager(manga_pager)
|
||||
trackingIconSubscription = trackingIconRelay.subscribe { setTrackingIconInternal(it) }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onChangeEnded(handler: ControllerChangeHandler, type: ControllerChangeType) {
|
||||
super.onChangeEnded(handler, type)
|
||||
if (manga == null || source == null) {
|
||||
activity?.toast(R.string.manga_not_in_db)
|
||||
router.popController(this)
|
||||
}
|
||||
}
|
||||
|
||||
override fun configureTabs(tabs: TabLayout) {
|
||||
with(tabs) {
|
||||
tabGravity = TabLayout.GRAVITY_FILL
|
||||
tabMode = TabLayout.MODE_FIXED
|
||||
}
|
||||
}
|
||||
|
||||
override fun cleanupTabs(tabs: TabLayout) {
|
||||
trackingIconSubscription?.unsubscribe()
|
||||
setTrackingIconInternal(false)
|
||||
}
|
||||
|
||||
fun setTrackingIcon(visible: Boolean) {
|
||||
trackingIconRelay.call(visible)
|
||||
}
|
||||
|
||||
private fun setTrackingIconInternal(visible: Boolean) {
|
||||
val tab = activity?.tabs?.getTabAt(TRACK_CONTROLLER) ?: return
|
||||
val drawable = if (visible)
|
||||
VectorDrawableCompat.create(resources!!, R.drawable.ic_done_white_18dp, null)
|
||||
else null
|
||||
|
||||
val view = tabField.get(tab) as LinearLayout
|
||||
val textView = view.getChildAt(1) as TextView
|
||||
textView.setCompoundDrawablesWithIntrinsicBounds(null, null, drawable, null)
|
||||
textView.compoundDrawablePadding = if (visible) 4 else 0
|
||||
}
|
||||
|
||||
private inner class MangaDetailAdapter : RouterPagerAdapter(this@MangaController) {
|
||||
|
||||
private val tabCount = if (Injekt.get<TrackManager>().hasLoggedServices()) 3 else 2
|
||||
|
||||
private val tabTitles = listOf(
|
||||
R.string.manga_detail_tab,
|
||||
R.string.manga_chapters_tab,
|
||||
R.string.manga_tracking_tab)
|
||||
.map { resources!!.getString(it) }
|
||||
|
||||
override fun getCount(): Int {
|
||||
return tabCount
|
||||
}
|
||||
|
||||
override fun configureRouter(router: Router, position: Int) {
|
||||
if (!router.hasRootController()) {
|
||||
val controller = when (position) {
|
||||
INFO_CONTROLLER -> MangaInfoController()
|
||||
CHAPTERS_CONTROLLER -> ChaptersController()
|
||||
TRACK_CONTROLLER -> TrackController()
|
||||
else -> error("Wrong position $position")
|
||||
}
|
||||
router.setRoot(RouterTransaction.with(controller))
|
||||
}
|
||||
}
|
||||
|
||||
override fun getPageTitle(position: Int): CharSequence {
|
||||
return tabTitles[position]
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val FROM_CATALOGUE_EXTRA = "from_catalogue"
|
||||
const val MANGA_EXTRA = "manga"
|
||||
|
||||
const val INFO_CONTROLLER = 0
|
||||
const val CHAPTERS_CONTROLLER = 1
|
||||
const val TRACK_CONTROLLER = 2
|
||||
|
||||
private val tabField = TabLayout.Tab::class.java.getDeclaredField("view")
|
||||
.apply { isAccessible = true }
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,122 +1,122 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.PopupMenu
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.gone
|
||||
import eu.kanade.tachiyomi.util.setVectorCompat
|
||||
import kotlinx.android.synthetic.main.chapters_item.*
|
||||
import java.util.*
|
||||
|
||||
class ChapterHolder(
|
||||
private val view: View,
|
||||
private val adapter: ChaptersAdapter
|
||||
) : BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
init {
|
||||
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
|
||||
// correctly positioned. The reason being that the view may change position before the
|
||||
// PopupMenu is shown.
|
||||
chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
|
||||
}
|
||||
|
||||
fun bind(item: ChapterItem, manga: Manga) {
|
||||
val chapter = item.chapter
|
||||
|
||||
chapter_title.text = when (manga.displayMode) {
|
||||
Manga.DISPLAY_NUMBER -> {
|
||||
val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
|
||||
itemView.context.getString(R.string.display_mode_chapter, number)
|
||||
}
|
||||
else -> chapter.name
|
||||
}
|
||||
|
||||
// Set the correct drawable for dropdown and update the tint to match theme.
|
||||
chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color))
|
||||
|
||||
// Set correct text color
|
||||
chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
|
||||
if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor)
|
||||
|
||||
if (chapter.date_upload > 0) {
|
||||
chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload))
|
||||
chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
|
||||
} else {
|
||||
chapter_date.text = ""
|
||||
}
|
||||
|
||||
//add scanlator if exists
|
||||
chapter_scanlator.text = chapter.scanlator
|
||||
//allow longer titles if there is no scanlator (most sources)
|
||||
if (chapter_scanlator.text.isNullOrBlank()) {
|
||||
chapter_title.maxLines = 2
|
||||
chapter_scanlator.gone()
|
||||
} else {
|
||||
chapter_title.maxLines = 1
|
||||
}
|
||||
|
||||
chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) {
|
||||
itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
notifyStatus(item.status)
|
||||
}
|
||||
|
||||
fun notifyStatus(status: Int) = with(download_text) {
|
||||
when (status) {
|
||||
Download.QUEUE -> setText(R.string.chapter_queued)
|
||||
Download.DOWNLOADING -> setText(R.string.chapter_downloading)
|
||||
Download.DOWNLOADED -> setText(R.string.chapter_downloaded)
|
||||
Download.ERROR -> setText(R.string.chapter_error)
|
||||
else -> text = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPopupMenu(view: View) {
|
||||
val item = adapter.getItem(adapterPosition) ?: return
|
||||
|
||||
// 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)
|
||||
|
||||
val chapter = item.chapter
|
||||
|
||||
// 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 bookmark if bookmark
|
||||
popup.menu.findItem(R.id.action_bookmark).isVisible = !chapter.bookmark
|
||||
popup.menu.findItem(R.id.action_remove_bookmark).isVisible = chapter.bookmark
|
||||
|
||||
// Hide mark as unread when the chapter is unread
|
||||
if (!chapter.read && chapter.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 (chapter.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 ->
|
||||
adapter.menuItemListener.onMenuItemClick(adapterPosition, menuItem)
|
||||
true
|
||||
}
|
||||
|
||||
// Finally show the PopupMenu
|
||||
popup.show()
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.view.View
|
||||
import android.widget.PopupMenu
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.download.model.Download
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseFlexibleViewHolder
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import eu.kanade.tachiyomi.util.gone
|
||||
import eu.kanade.tachiyomi.util.setVectorCompat
|
||||
import kotlinx.android.synthetic.main.chapters_item.*
|
||||
import java.util.*
|
||||
|
||||
class ChapterHolder(
|
||||
private val view: View,
|
||||
private val adapter: ChaptersAdapter
|
||||
) : BaseFlexibleViewHolder(view, adapter) {
|
||||
|
||||
init {
|
||||
// We need to post a Runnable to show the popup to make sure that the PopupMenu is
|
||||
// correctly positioned. The reason being that the view may change position before the
|
||||
// PopupMenu is shown.
|
||||
chapter_menu.setOnClickListener { it.post { showPopupMenu(it) } }
|
||||
}
|
||||
|
||||
fun bind(item: ChapterItem, manga: Manga) {
|
||||
val chapter = item.chapter
|
||||
|
||||
chapter_title.text = when (manga.displayMode) {
|
||||
Manga.DISPLAY_NUMBER -> {
|
||||
val number = adapter.decimalFormat.format(chapter.chapter_number.toDouble())
|
||||
itemView.context.getString(R.string.display_mode_chapter, number)
|
||||
}
|
||||
else -> chapter.name
|
||||
}
|
||||
|
||||
// Set the correct drawable for dropdown and update the tint to match theme.
|
||||
chapter_menu.setVectorCompat(R.drawable.ic_more_vert_black_24dp, view.context.getResourceColor(R.attr.icon_color))
|
||||
|
||||
// Set correct text color
|
||||
chapter_title.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
|
||||
if (chapter.bookmark) chapter_title.setTextColor(adapter.bookmarkedColor)
|
||||
|
||||
if (chapter.date_upload > 0) {
|
||||
chapter_date.text = adapter.dateFormat.format(Date(chapter.date_upload))
|
||||
chapter_date.setTextColor(if (chapter.read) adapter.readColor else adapter.unreadColor)
|
||||
} else {
|
||||
chapter_date.text = ""
|
||||
}
|
||||
|
||||
//add scanlator if exists
|
||||
chapter_scanlator.text = chapter.scanlator
|
||||
//allow longer titles if there is no scanlator (most sources)
|
||||
if (chapter_scanlator.text.isNullOrBlank()) {
|
||||
chapter_title.maxLines = 2
|
||||
chapter_scanlator.gone()
|
||||
} else {
|
||||
chapter_title.maxLines = 1
|
||||
}
|
||||
|
||||
chapter_pages.text = if (!chapter.read && chapter.last_page_read > 0) {
|
||||
itemView.context.getString(R.string.chapter_progress, chapter.last_page_read + 1)
|
||||
} else {
|
||||
""
|
||||
}
|
||||
|
||||
notifyStatus(item.status)
|
||||
}
|
||||
|
||||
fun notifyStatus(status: Int) = with(download_text) {
|
||||
when (status) {
|
||||
Download.QUEUE -> setText(R.string.chapter_queued)
|
||||
Download.DOWNLOADING -> setText(R.string.chapter_downloading)
|
||||
Download.DOWNLOADED -> setText(R.string.chapter_downloaded)
|
||||
Download.ERROR -> setText(R.string.chapter_error)
|
||||
else -> text = ""
|
||||
}
|
||||
}
|
||||
|
||||
private fun showPopupMenu(view: View) {
|
||||
val item = adapter.getItem(adapterPosition) ?: return
|
||||
|
||||
// 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)
|
||||
|
||||
val chapter = item.chapter
|
||||
|
||||
// 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 bookmark if bookmark
|
||||
popup.menu.findItem(R.id.action_bookmark).isVisible = !chapter.bookmark
|
||||
popup.menu.findItem(R.id.action_remove_bookmark).isVisible = chapter.bookmark
|
||||
|
||||
// Hide mark as unread when the chapter is unread
|
||||
if (!chapter.read && chapter.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 (chapter.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 ->
|
||||
adapter.menuItemListener.onMenuItemClick(adapterPosition, menuItem)
|
||||
true
|
||||
}
|
||||
|
||||
// Finally show the PopupMenu
|
||||
popup.show()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,55 +1,55 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
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
|
||||
|
||||
class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem<ChapterHolder>(),
|
||||
Chapter by chapter {
|
||||
|
||||
private var _status: Int = 0
|
||||
|
||||
var status: Int
|
||||
get() = download?.status ?: _status
|
||||
set(value) { _status = value }
|
||||
|
||||
@Transient var download: Download? = null
|
||||
|
||||
val isDownloaded: Boolean
|
||||
get() = status == Download.DOWNLOADED
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.chapters_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): ChapterHolder {
|
||||
return ChapterHolder(view, adapter as ChaptersAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ChapterHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?) {
|
||||
|
||||
holder.bind(this, manga)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is ChapterItem) {
|
||||
return chapter.id!! == other.chapter.id!!
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return chapter.id!!.hashCode()
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.view.View
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.items.AbstractFlexibleItem
|
||||
import eu.davidea.flexibleadapter.items.IFlexible
|
||||
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
|
||||
|
||||
class ChapterItem(val chapter: Chapter, val manga: Manga) : AbstractFlexibleItem<ChapterHolder>(),
|
||||
Chapter by chapter {
|
||||
|
||||
private var _status: Int = 0
|
||||
|
||||
var status: Int
|
||||
get() = download?.status ?: _status
|
||||
set(value) { _status = value }
|
||||
|
||||
@Transient var download: Download? = null
|
||||
|
||||
val isDownloaded: Boolean
|
||||
get() = status == Download.DOWNLOADED
|
||||
|
||||
override fun getLayoutRes(): Int {
|
||||
return R.layout.chapters_item
|
||||
}
|
||||
|
||||
override fun createViewHolder(view: View, adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>): ChapterHolder {
|
||||
return ChapterHolder(view, adapter as ChaptersAdapter)
|
||||
}
|
||||
|
||||
override fun bindViewHolder(adapter: FlexibleAdapter<IFlexible<RecyclerView.ViewHolder>>,
|
||||
holder: ChapterHolder,
|
||||
position: Int,
|
||||
payloads: List<Any?>?) {
|
||||
|
||||
holder.bind(this, manga)
|
||||
}
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
if (this === other) return true
|
||||
if (other is ChapterItem) {
|
||||
return chapter.id!! == other.chapter.id!!
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
return chapter.id!!.hashCode()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,45 +1,45 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.MenuItem
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import java.text.DateFormat
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
class ChaptersAdapter(
|
||||
controller: ChaptersController,
|
||||
context: Context
|
||||
) : FlexibleAdapter<ChapterItem>(null, controller, true) {
|
||||
|
||||
var items: List<ChapterItem> = emptyList()
|
||||
|
||||
val menuItemListener: OnMenuItemClickListener = controller
|
||||
|
||||
val readColor = context.getResourceColor(android.R.attr.textColorHint)
|
||||
|
||||
val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary)
|
||||
|
||||
val bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
|
||||
|
||||
val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols()
|
||||
.apply { decimalSeparator = '.' })
|
||||
|
||||
val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
|
||||
override fun updateDataSet(items: List<ChapterItem>?) {
|
||||
this.items = items ?: emptyList()
|
||||
super.updateDataSet(items)
|
||||
}
|
||||
|
||||
fun indexOf(item: ChapterItem): Int {
|
||||
return items.indexOf(item)
|
||||
}
|
||||
|
||||
interface OnMenuItemClickListener {
|
||||
fun onMenuItemClick(position: Int, item: MenuItem)
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.content.Context
|
||||
import android.view.MenuItem
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.getResourceColor
|
||||
import java.text.DateFormat
|
||||
import java.text.DecimalFormat
|
||||
import java.text.DecimalFormatSymbols
|
||||
|
||||
class ChaptersAdapter(
|
||||
controller: ChaptersController,
|
||||
context: Context
|
||||
) : FlexibleAdapter<ChapterItem>(null, controller, true) {
|
||||
|
||||
var items: List<ChapterItem> = emptyList()
|
||||
|
||||
val menuItemListener: OnMenuItemClickListener = controller
|
||||
|
||||
val readColor = context.getResourceColor(android.R.attr.textColorHint)
|
||||
|
||||
val unreadColor = context.getResourceColor(android.R.attr.textColorPrimary)
|
||||
|
||||
val bookmarkedColor = context.getResourceColor(R.attr.colorAccent)
|
||||
|
||||
val decimalFormat = DecimalFormat("#.###", DecimalFormatSymbols()
|
||||
.apply { decimalSeparator = '.' })
|
||||
|
||||
val dateFormat: DateFormat = DateFormat.getDateInstance(DateFormat.SHORT)
|
||||
|
||||
override fun updateDataSet(items: List<ChapterItem>?) {
|
||||
this.items = items ?: emptyList()
|
||||
super.updateDataSet(items)
|
||||
}
|
||||
|
||||
fun indexOf(item: ChapterItem): Int {
|
||||
return items.indexOf(item)
|
||||
}
|
||||
|
||||
interface OnMenuItemClickListener {
|
||||
fun onMenuItemClick(position: Int, item: MenuItem)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,486 +1,486 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.*
|
||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
||||
import com.jakewharton.rxbinding.view.clicks
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
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.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.getCoordinates
|
||||
import eu.kanade.tachiyomi.util.snack
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.chapters_controller.*
|
||||
import timber.log.Timber
|
||||
|
||||
class ChaptersController : NucleusController<ChaptersPresenter>(),
|
||||
ActionMode.Callback,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
ChaptersAdapter.OnMenuItemClickListener,
|
||||
SetDisplayModeDialog.Listener,
|
||||
SetSortingDialog.Listener,
|
||||
DownloadChaptersDialog.Listener,
|
||||
DownloadCustomChaptersDialog.Listener,
|
||||
DeleteChaptersDialog.Listener {
|
||||
|
||||
/**
|
||||
* Adapter containing a list of chapters.
|
||||
*/
|
||||
private var adapter: ChaptersAdapter? = null
|
||||
|
||||
/**
|
||||
* Action mode for multiple selection.
|
||||
*/
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
/**
|
||||
* Selected items. Used to restore selections after a rotation.
|
||||
*/
|
||||
private val selectedItems = mutableSetOf<ChapterItem>()
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
setOptionsMenuHidden(true)
|
||||
}
|
||||
|
||||
override fun createPresenter(): ChaptersPresenter {
|
||||
val ctrl = parentController as MangaController
|
||||
return ChaptersPresenter(ctrl.manga!!, ctrl.source!!,
|
||||
ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.chapters_controller, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
// Init RecyclerView and adapter
|
||||
adapter = ChaptersAdapter(this, view.context)
|
||||
|
||||
recycler.adapter = adapter
|
||||
recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||
recycler.setHasFixedSize(true)
|
||||
adapter?.fastScroller = fast_scroller
|
||||
|
||||
swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
|
||||
|
||||
fab.clicks().subscribeUntilDestroy {
|
||||
val item = presenter.getNextUnreadChapter()
|
||||
if (item != null) {
|
||||
// Create animation listener
|
||||
val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationStart(animation: Animator?) {
|
||||
openChapter(item.chapter, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Get coordinates and start animation
|
||||
val coordinates = fab.getCoordinates()
|
||||
if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
|
||||
openChapter(item.chapter)
|
||||
}
|
||||
} else {
|
||||
view.context.toast(R.string.no_next_chapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
actionMode = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
if (view == null) return
|
||||
|
||||
// Check if animation view is visible
|
||||
if (reveal_view.visibility == View.VISIBLE) {
|
||||
// Show the unReveal effect
|
||||
val coordinates = fab.getCoordinates()
|
||||
reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
|
||||
}
|
||||
super.onActivityResumed(activity)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.chapters, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
// Initialize menu items.
|
||||
val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return
|
||||
val menuFilterUnread = menu.findItem(R.id.action_filter_unread)
|
||||
val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded)
|
||||
val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked)
|
||||
|
||||
// Set correct checkbox values.
|
||||
menuFilterRead.isChecked = presenter.onlyRead()
|
||||
menuFilterUnread.isChecked = presenter.onlyUnread()
|
||||
menuFilterDownloaded.isChecked = presenter.onlyDownloaded()
|
||||
menuFilterBookmarked.isChecked = presenter.onlyBookmarked()
|
||||
|
||||
if (presenter.onlyRead())
|
||||
//Disable unread filter option if read filter is enabled.
|
||||
menuFilterUnread.isEnabled = false
|
||||
if (presenter.onlyUnread())
|
||||
//Disable read filter option if unread filter is enabled.
|
||||
menuFilterRead.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_display_mode -> showDisplayModeDialog()
|
||||
R.id.manga_download -> showDownloadDialog()
|
||||
R.id.action_sorting_mode -> showSortingDialog()
|
||||
R.id.action_filter_unread -> {
|
||||
item.isChecked = !item.isChecked
|
||||
presenter.setUnreadFilter(item.isChecked)
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
R.id.action_filter_read -> {
|
||||
item.isChecked = !item.isChecked
|
||||
presenter.setReadFilter(item.isChecked)
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
R.id.action_filter_downloaded -> {
|
||||
item.isChecked = !item.isChecked
|
||||
presenter.setDownloadedFilter(item.isChecked)
|
||||
}
|
||||
R.id.action_filter_bookmarked -> {
|
||||
item.isChecked = !item.isChecked
|
||||
presenter.setBookmarkedFilter(item.isChecked)
|
||||
}
|
||||
R.id.action_filter_empty -> {
|
||||
presenter.removeFilters()
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
R.id.action_sort -> presenter.revertSortOrder()
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun onNextChapters(chapters: List<ChapterItem>) {
|
||||
// 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()
|
||||
|
||||
val adapter = adapter ?: return
|
||||
adapter.updateDataSet(chapters)
|
||||
|
||||
if (selectedItems.isNotEmpty()) {
|
||||
adapter.clearSelection() // we need to start from a clean state, index may have changed
|
||||
createActionModeIfNeeded()
|
||||
selectedItems.forEach { item ->
|
||||
val position = adapter.indexOf(item)
|
||||
if (position != -1 && !adapter.isSelected(position)) {
|
||||
adapter.toggleSelection(position)
|
||||
}
|
||||
}
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun initialFetchChapters() {
|
||||
// Only fetch if this view is from the catalog and it hasn't requested previously
|
||||
if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) {
|
||||
fetchChaptersFromSource()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchChaptersFromSource() {
|
||||
swipe_refresh?.isRefreshing = true
|
||||
presenter.fetchChaptersFromSource()
|
||||
}
|
||||
|
||||
fun onFetchChaptersDone() {
|
||||
swipe_refresh?.isRefreshing = false
|
||||
}
|
||||
|
||||
fun onFetchChaptersError(error: Throwable) {
|
||||
swipe_refresh?.isRefreshing = false
|
||||
activity?.toast(error.message)
|
||||
}
|
||||
|
||||
fun onChapterStatusChange(download: Download) {
|
||||
getHolder(download.chapter)?.notifyStatus(download.status)
|
||||
}
|
||||
|
||||
private fun getHolder(chapter: Chapter): ChapterHolder? {
|
||||
return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
|
||||
}
|
||||
|
||||
fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) {
|
||||
val activity = activity ?: return
|
||||
val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter)
|
||||
if (hasAnimation) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
val adapter = adapter ?: return false
|
||||
val item = adapter.getItem(position) ?: return false
|
||||
if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
toggleSelection(position)
|
||||
return true
|
||||
} else {
|
||||
openChapter(item.chapter)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int) {
|
||||
createActionModeIfNeeded()
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
// SELECTIONS & ACTION MODE
|
||||
|
||||
private fun toggleSelection(position: Int) {
|
||||
val adapter = adapter ?: return
|
||||
val item = adapter.getItem(position) ?: return
|
||||
adapter.toggleSelection(position)
|
||||
if (adapter.isSelected(position)) {
|
||||
selectedItems.add(item)
|
||||
} else {
|
||||
selectedItems.remove(item)
|
||||
}
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
private fun getSelectedChapters(): List<ChapterItem> {
|
||||
val adapter = adapter ?: return emptyList()
|
||||
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) }
|
||||
}
|
||||
|
||||
private fun createActionModeIfNeeded() {
|
||||
if (actionMode == null) {
|
||||
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun destroyActionModeIfNeeded() {
|
||||
actionMode?.finish()
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.chapter_selection, menu)
|
||||
adapter?.mode = SelectableAdapter.Mode.MULTI
|
||||
return true
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val count = adapter?.selectedItemCount ?: 0
|
||||
if (count == 0) {
|
||||
// Destroy action mode if there are no items selected.
|
||||
destroyActionModeIfNeeded()
|
||||
} else {
|
||||
mode.title = resources?.getString(R.string.label_selected, count)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_select_all -> selectAll()
|
||||
R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
|
||||
R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
|
||||
R.id.action_download -> downloadChapters(getSelectedChapters())
|
||||
R.id.action_delete -> showDeleteChaptersConfirmationDialog()
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
adapter?.mode = SelectableAdapter.Mode.SINGLE
|
||||
adapter?.clearSelection()
|
||||
selectedItems.clear()
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(position: Int, item: MenuItem) {
|
||||
val chapter = adapter?.getItem(position) ?: return
|
||||
val chapters = listOf(chapter)
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.action_download -> downloadChapters(chapters)
|
||||
R.id.action_bookmark -> bookmarkChapters(chapters, true)
|
||||
R.id.action_remove_bookmark -> bookmarkChapters(chapters, false)
|
||||
R.id.action_delete -> deleteChapters(chapters)
|
||||
R.id.action_mark_as_read -> markAsRead(chapters)
|
||||
R.id.action_mark_as_unread -> markAsUnread(chapters)
|
||||
R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
// SELECTION MODE ACTIONS
|
||||
|
||||
private fun selectAll() {
|
||||
val adapter = adapter ?: return
|
||||
adapter.selectAll()
|
||||
selectedItems.addAll(adapter.items)
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
private fun markAsRead(chapters: List<ChapterItem>) {
|
||||
presenter.markChaptersRead(chapters, true)
|
||||
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
||||
deleteChapters(chapters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun markAsUnread(chapters: List<ChapterItem>) {
|
||||
presenter.markChaptersRead(chapters, false)
|
||||
}
|
||||
|
||||
private fun downloadChapters(chapters: List<ChapterItem>) {
|
||||
val view = view
|
||||
destroyActionModeIfNeeded()
|
||||
presenter.downloadChapters(chapters)
|
||||
if (view != null && !presenter.manga.favorite) {
|
||||
recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
|
||||
setAction(R.string.action_add) {
|
||||
presenter.addToLibrary()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun showDeleteChaptersConfirmationDialog() {
|
||||
DeleteChaptersDialog(this).showDialog(router)
|
||||
}
|
||||
|
||||
override fun deleteChapters() {
|
||||
deleteChapters(getSelectedChapters())
|
||||
}
|
||||
|
||||
private fun markPreviousAsRead(chapter: ChapterItem) {
|
||||
val adapter = adapter ?: return
|
||||
val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
|
||||
val chapterPos = chapters.indexOf(chapter)
|
||||
if (chapterPos != -1) {
|
||||
markAsRead(chapters.take(chapterPos))
|
||||
}
|
||||
}
|
||||
|
||||
private fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
|
||||
destroyActionModeIfNeeded()
|
||||
presenter.bookmarkChapters(chapters, bookmarked)
|
||||
}
|
||||
|
||||
fun deleteChapters(chapters: List<ChapterItem>) {
|
||||
destroyActionModeIfNeeded()
|
||||
if (chapters.isEmpty()) return
|
||||
|
||||
DeletingChaptersDialog().showDialog(router)
|
||||
presenter.deleteChapters(chapters)
|
||||
}
|
||||
|
||||
fun onChaptersDeleted() {
|
||||
dismissDeletingDialog()
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun onChaptersDeletedError(error: Throwable) {
|
||||
dismissDeletingDialog()
|
||||
Timber.e(error)
|
||||
}
|
||||
|
||||
private fun dismissDeletingDialog() {
|
||||
router.popControllerWithTag(DeletingChaptersDialog.TAG)
|
||||
}
|
||||
|
||||
// OVERFLOW MENU DIALOGS
|
||||
|
||||
private fun showDisplayModeDialog() {
|
||||
val preselected = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1
|
||||
SetDisplayModeDialog(this, preselected).showDialog(router)
|
||||
}
|
||||
|
||||
override fun setDisplayMode(id: Int) {
|
||||
presenter.setDisplayMode(id)
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun showSortingDialog() {
|
||||
val preselected = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1
|
||||
SetSortingDialog(this, preselected).showDialog(router)
|
||||
}
|
||||
|
||||
override fun setSorting(id: Int) {
|
||||
presenter.setSorting(id)
|
||||
}
|
||||
|
||||
private fun showDownloadDialog() {
|
||||
DownloadChaptersDialog(this).showDialog(router)
|
||||
}
|
||||
|
||||
private fun getUnreadChaptersSorted() = presenter.chapters
|
||||
.filter { !it.read && it.status == Download.NOT_DOWNLOADED }
|
||||
.distinctBy { it.name }
|
||||
.sortedByDescending { it.source_order }
|
||||
|
||||
override fun downloadCustomChapters(amount: Int) {
|
||||
val chaptersToDownload = getUnreadChaptersSorted().take(amount)
|
||||
if (chaptersToDownload.isNotEmpty()) {
|
||||
downloadChapters(chaptersToDownload)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCustomDownloadDialog() {
|
||||
DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router)
|
||||
}
|
||||
|
||||
|
||||
override fun downloadChapters(choice: Int) {
|
||||
// i = 0: Download 1
|
||||
// i = 1: Download 5
|
||||
// i = 2: Download 10
|
||||
// i = 3: Download x
|
||||
// i = 4: Download unread
|
||||
// i = 5: Download all
|
||||
val chaptersToDownload = when (choice) {
|
||||
0 -> getUnreadChaptersSorted().take(1)
|
||||
1 -> getUnreadChaptersSorted().take(5)
|
||||
2 -> getUnreadChaptersSorted().take(10)
|
||||
3 -> {
|
||||
showCustomDownloadDialog()
|
||||
return
|
||||
}
|
||||
4 -> presenter.chapters.filter { !it.read }
|
||||
5 -> presenter.chapters
|
||||
else -> emptyList()
|
||||
}
|
||||
if (chaptersToDownload.isNotEmpty()) {
|
||||
downloadChapters(chaptersToDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.animation.Animator
|
||||
import android.animation.AnimatorListenerAdapter
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.Activity
|
||||
import android.content.Intent
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import androidx.appcompat.view.ActionMode
|
||||
import androidx.recyclerview.widget.DividerItemDecoration
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.*
|
||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
||||
import com.jakewharton.rxbinding.view.clicks
|
||||
import eu.davidea.flexibleadapter.FlexibleAdapter
|
||||
import eu.davidea.flexibleadapter.SelectableAdapter
|
||||
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.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.base.controller.popControllerWithTag
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.ui.reader.ReaderActivity
|
||||
import eu.kanade.tachiyomi.util.getCoordinates
|
||||
import eu.kanade.tachiyomi.util.snack
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.chapters_controller.*
|
||||
import timber.log.Timber
|
||||
|
||||
class ChaptersController : NucleusController<ChaptersPresenter>(),
|
||||
ActionMode.Callback,
|
||||
FlexibleAdapter.OnItemClickListener,
|
||||
FlexibleAdapter.OnItemLongClickListener,
|
||||
ChaptersAdapter.OnMenuItemClickListener,
|
||||
SetDisplayModeDialog.Listener,
|
||||
SetSortingDialog.Listener,
|
||||
DownloadChaptersDialog.Listener,
|
||||
DownloadCustomChaptersDialog.Listener,
|
||||
DeleteChaptersDialog.Listener {
|
||||
|
||||
/**
|
||||
* Adapter containing a list of chapters.
|
||||
*/
|
||||
private var adapter: ChaptersAdapter? = null
|
||||
|
||||
/**
|
||||
* Action mode for multiple selection.
|
||||
*/
|
||||
private var actionMode: ActionMode? = null
|
||||
|
||||
/**
|
||||
* Selected items. Used to restore selections after a rotation.
|
||||
*/
|
||||
private val selectedItems = mutableSetOf<ChapterItem>()
|
||||
|
||||
init {
|
||||
setHasOptionsMenu(true)
|
||||
setOptionsMenuHidden(true)
|
||||
}
|
||||
|
||||
override fun createPresenter(): ChaptersPresenter {
|
||||
val ctrl = parentController as MangaController
|
||||
return ChaptersPresenter(ctrl.manga!!, ctrl.source!!,
|
||||
ctrl.chapterCountRelay, ctrl.lastUpdateRelay, ctrl.mangaFavoriteRelay)
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.chapters_controller, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
// Init RecyclerView and adapter
|
||||
adapter = ChaptersAdapter(this, view.context)
|
||||
|
||||
recycler.adapter = adapter
|
||||
recycler.layoutManager = LinearLayoutManager(view.context)
|
||||
recycler.addItemDecoration(DividerItemDecoration(view.context, DividerItemDecoration.VERTICAL))
|
||||
recycler.setHasFixedSize(true)
|
||||
adapter?.fastScroller = fast_scroller
|
||||
|
||||
swipe_refresh.refreshes().subscribeUntilDestroy { fetchChaptersFromSource() }
|
||||
|
||||
fab.clicks().subscribeUntilDestroy {
|
||||
val item = presenter.getNextUnreadChapter()
|
||||
if (item != null) {
|
||||
// Create animation listener
|
||||
val revealAnimationListener: Animator.AnimatorListener = object : AnimatorListenerAdapter() {
|
||||
override fun onAnimationStart(animation: Animator?) {
|
||||
openChapter(item.chapter, true)
|
||||
}
|
||||
}
|
||||
|
||||
// Get coordinates and start animation
|
||||
val coordinates = fab.getCoordinates()
|
||||
if (!reveal_view.showRevealEffect(coordinates.x, coordinates.y, revealAnimationListener)) {
|
||||
openChapter(item.chapter)
|
||||
}
|
||||
} else {
|
||||
view.context.toast(R.string.no_next_chapter)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
actionMode = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
override fun onActivityResumed(activity: Activity) {
|
||||
if (view == null) return
|
||||
|
||||
// Check if animation view is visible
|
||||
if (reveal_view.visibility == View.VISIBLE) {
|
||||
// Show the unReveal effect
|
||||
val coordinates = fab.getCoordinates()
|
||||
reveal_view.hideRevealEffect(coordinates.x, coordinates.y, 1920)
|
||||
}
|
||||
super.onActivityResumed(activity)
|
||||
}
|
||||
|
||||
override fun onCreateOptionsMenu(menu: Menu, inflater: MenuInflater) {
|
||||
inflater.inflate(R.menu.chapters, menu)
|
||||
}
|
||||
|
||||
override fun onPrepareOptionsMenu(menu: Menu) {
|
||||
// Initialize menu items.
|
||||
val menuFilterRead = menu.findItem(R.id.action_filter_read) ?: return
|
||||
val menuFilterUnread = menu.findItem(R.id.action_filter_unread)
|
||||
val menuFilterDownloaded = menu.findItem(R.id.action_filter_downloaded)
|
||||
val menuFilterBookmarked = menu.findItem(R.id.action_filter_bookmarked)
|
||||
|
||||
// Set correct checkbox values.
|
||||
menuFilterRead.isChecked = presenter.onlyRead()
|
||||
menuFilterUnread.isChecked = presenter.onlyUnread()
|
||||
menuFilterDownloaded.isChecked = presenter.onlyDownloaded()
|
||||
menuFilterBookmarked.isChecked = presenter.onlyBookmarked()
|
||||
|
||||
if (presenter.onlyRead())
|
||||
//Disable unread filter option if read filter is enabled.
|
||||
menuFilterUnread.isEnabled = false
|
||||
if (presenter.onlyUnread())
|
||||
//Disable read filter option if unread filter is enabled.
|
||||
menuFilterRead.isEnabled = false
|
||||
}
|
||||
|
||||
override fun onOptionsItemSelected(item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_display_mode -> showDisplayModeDialog()
|
||||
R.id.manga_download -> showDownloadDialog()
|
||||
R.id.action_sorting_mode -> showSortingDialog()
|
||||
R.id.action_filter_unread -> {
|
||||
item.isChecked = !item.isChecked
|
||||
presenter.setUnreadFilter(item.isChecked)
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
R.id.action_filter_read -> {
|
||||
item.isChecked = !item.isChecked
|
||||
presenter.setReadFilter(item.isChecked)
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
R.id.action_filter_downloaded -> {
|
||||
item.isChecked = !item.isChecked
|
||||
presenter.setDownloadedFilter(item.isChecked)
|
||||
}
|
||||
R.id.action_filter_bookmarked -> {
|
||||
item.isChecked = !item.isChecked
|
||||
presenter.setBookmarkedFilter(item.isChecked)
|
||||
}
|
||||
R.id.action_filter_empty -> {
|
||||
presenter.removeFilters()
|
||||
activity?.invalidateOptionsMenu()
|
||||
}
|
||||
R.id.action_sort -> presenter.revertSortOrder()
|
||||
else -> return super.onOptionsItemSelected(item)
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun onNextChapters(chapters: List<ChapterItem>) {
|
||||
// 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()
|
||||
|
||||
val adapter = adapter ?: return
|
||||
adapter.updateDataSet(chapters)
|
||||
|
||||
if (selectedItems.isNotEmpty()) {
|
||||
adapter.clearSelection() // we need to start from a clean state, index may have changed
|
||||
createActionModeIfNeeded()
|
||||
selectedItems.forEach { item ->
|
||||
val position = adapter.indexOf(item)
|
||||
if (position != -1 && !adapter.isSelected(position)) {
|
||||
adapter.toggleSelection(position)
|
||||
}
|
||||
}
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private fun initialFetchChapters() {
|
||||
// Only fetch if this view is from the catalog and it hasn't requested previously
|
||||
if ((parentController as MangaController).fromCatalogue && !presenter.hasRequested) {
|
||||
fetchChaptersFromSource()
|
||||
}
|
||||
}
|
||||
|
||||
private fun fetchChaptersFromSource() {
|
||||
swipe_refresh?.isRefreshing = true
|
||||
presenter.fetchChaptersFromSource()
|
||||
}
|
||||
|
||||
fun onFetchChaptersDone() {
|
||||
swipe_refresh?.isRefreshing = false
|
||||
}
|
||||
|
||||
fun onFetchChaptersError(error: Throwable) {
|
||||
swipe_refresh?.isRefreshing = false
|
||||
activity?.toast(error.message)
|
||||
}
|
||||
|
||||
fun onChapterStatusChange(download: Download) {
|
||||
getHolder(download.chapter)?.notifyStatus(download.status)
|
||||
}
|
||||
|
||||
private fun getHolder(chapter: Chapter): ChapterHolder? {
|
||||
return recycler?.findViewHolderForItemId(chapter.id!!) as? ChapterHolder
|
||||
}
|
||||
|
||||
fun openChapter(chapter: Chapter, hasAnimation: Boolean = false) {
|
||||
val activity = activity ?: return
|
||||
val intent = ReaderActivity.newIntent(activity, presenter.manga, chapter)
|
||||
if (hasAnimation) {
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
|
||||
}
|
||||
startActivity(intent)
|
||||
}
|
||||
|
||||
override fun onItemClick(view: View, position: Int): Boolean {
|
||||
val adapter = adapter ?: return false
|
||||
val item = adapter.getItem(position) ?: return false
|
||||
if (actionMode != null && adapter.mode == SelectableAdapter.Mode.MULTI) {
|
||||
toggleSelection(position)
|
||||
return true
|
||||
} else {
|
||||
openChapter(item.chapter)
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
override fun onItemLongClick(position: Int) {
|
||||
createActionModeIfNeeded()
|
||||
toggleSelection(position)
|
||||
}
|
||||
|
||||
// SELECTIONS & ACTION MODE
|
||||
|
||||
private fun toggleSelection(position: Int) {
|
||||
val adapter = adapter ?: return
|
||||
val item = adapter.getItem(position) ?: return
|
||||
adapter.toggleSelection(position)
|
||||
if (adapter.isSelected(position)) {
|
||||
selectedItems.add(item)
|
||||
} else {
|
||||
selectedItems.remove(item)
|
||||
}
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
private fun getSelectedChapters(): List<ChapterItem> {
|
||||
val adapter = adapter ?: return emptyList()
|
||||
return adapter.selectedPositions.mapNotNull { adapter.getItem(it) }
|
||||
}
|
||||
|
||||
private fun createActionModeIfNeeded() {
|
||||
if (actionMode == null) {
|
||||
actionMode = (activity as? AppCompatActivity)?.startSupportActionMode(this)
|
||||
}
|
||||
}
|
||||
|
||||
private fun destroyActionModeIfNeeded() {
|
||||
actionMode?.finish()
|
||||
}
|
||||
|
||||
override fun onCreateActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
mode.menuInflater.inflate(R.menu.chapter_selection, menu)
|
||||
adapter?.mode = SelectableAdapter.Mode.MULTI
|
||||
return true
|
||||
}
|
||||
|
||||
@SuppressLint("StringFormatInvalid")
|
||||
override fun onPrepareActionMode(mode: ActionMode, menu: Menu): Boolean {
|
||||
val count = adapter?.selectedItemCount ?: 0
|
||||
if (count == 0) {
|
||||
// Destroy action mode if there are no items selected.
|
||||
destroyActionModeIfNeeded()
|
||||
} else {
|
||||
mode.title = resources?.getString(R.string.label_selected, count)
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
override fun onActionItemClicked(mode: ActionMode, item: MenuItem): Boolean {
|
||||
when (item.itemId) {
|
||||
R.id.action_select_all -> selectAll()
|
||||
R.id.action_mark_as_read -> markAsRead(getSelectedChapters())
|
||||
R.id.action_mark_as_unread -> markAsUnread(getSelectedChapters())
|
||||
R.id.action_download -> downloadChapters(getSelectedChapters())
|
||||
R.id.action_delete -> showDeleteChaptersConfirmationDialog()
|
||||
else -> return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
override fun onDestroyActionMode(mode: ActionMode) {
|
||||
adapter?.mode = SelectableAdapter.Mode.SINGLE
|
||||
adapter?.clearSelection()
|
||||
selectedItems.clear()
|
||||
actionMode = null
|
||||
}
|
||||
|
||||
override fun onMenuItemClick(position: Int, item: MenuItem) {
|
||||
val chapter = adapter?.getItem(position) ?: return
|
||||
val chapters = listOf(chapter)
|
||||
|
||||
when (item.itemId) {
|
||||
R.id.action_download -> downloadChapters(chapters)
|
||||
R.id.action_bookmark -> bookmarkChapters(chapters, true)
|
||||
R.id.action_remove_bookmark -> bookmarkChapters(chapters, false)
|
||||
R.id.action_delete -> deleteChapters(chapters)
|
||||
R.id.action_mark_as_read -> markAsRead(chapters)
|
||||
R.id.action_mark_as_unread -> markAsUnread(chapters)
|
||||
R.id.action_mark_previous_as_read -> markPreviousAsRead(chapter)
|
||||
}
|
||||
}
|
||||
|
||||
// SELECTION MODE ACTIONS
|
||||
|
||||
private fun selectAll() {
|
||||
val adapter = adapter ?: return
|
||||
adapter.selectAll()
|
||||
selectedItems.addAll(adapter.items)
|
||||
actionMode?.invalidate()
|
||||
}
|
||||
|
||||
private fun markAsRead(chapters: List<ChapterItem>) {
|
||||
presenter.markChaptersRead(chapters, true)
|
||||
if (presenter.preferences.removeAfterMarkedAsRead()) {
|
||||
deleteChapters(chapters)
|
||||
}
|
||||
}
|
||||
|
||||
private fun markAsUnread(chapters: List<ChapterItem>) {
|
||||
presenter.markChaptersRead(chapters, false)
|
||||
}
|
||||
|
||||
private fun downloadChapters(chapters: List<ChapterItem>) {
|
||||
val view = view
|
||||
destroyActionModeIfNeeded()
|
||||
presenter.downloadChapters(chapters)
|
||||
if (view != null && !presenter.manga.favorite) {
|
||||
recycler?.snack(view.context.getString(R.string.snack_add_to_library), Snackbar.LENGTH_INDEFINITE) {
|
||||
setAction(R.string.action_add) {
|
||||
presenter.addToLibrary()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private fun showDeleteChaptersConfirmationDialog() {
|
||||
DeleteChaptersDialog(this).showDialog(router)
|
||||
}
|
||||
|
||||
override fun deleteChapters() {
|
||||
deleteChapters(getSelectedChapters())
|
||||
}
|
||||
|
||||
private fun markPreviousAsRead(chapter: ChapterItem) {
|
||||
val adapter = adapter ?: return
|
||||
val chapters = if (presenter.sortDescending()) adapter.items.reversed() else adapter.items
|
||||
val chapterPos = chapters.indexOf(chapter)
|
||||
if (chapterPos != -1) {
|
||||
markAsRead(chapters.take(chapterPos))
|
||||
}
|
||||
}
|
||||
|
||||
private fun bookmarkChapters(chapters: List<ChapterItem>, bookmarked: Boolean) {
|
||||
destroyActionModeIfNeeded()
|
||||
presenter.bookmarkChapters(chapters, bookmarked)
|
||||
}
|
||||
|
||||
fun deleteChapters(chapters: List<ChapterItem>) {
|
||||
destroyActionModeIfNeeded()
|
||||
if (chapters.isEmpty()) return
|
||||
|
||||
DeletingChaptersDialog().showDialog(router)
|
||||
presenter.deleteChapters(chapters)
|
||||
}
|
||||
|
||||
fun onChaptersDeleted() {
|
||||
dismissDeletingDialog()
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
fun onChaptersDeletedError(error: Throwable) {
|
||||
dismissDeletingDialog()
|
||||
Timber.e(error)
|
||||
}
|
||||
|
||||
private fun dismissDeletingDialog() {
|
||||
router.popControllerWithTag(DeletingChaptersDialog.TAG)
|
||||
}
|
||||
|
||||
// OVERFLOW MENU DIALOGS
|
||||
|
||||
private fun showDisplayModeDialog() {
|
||||
val preselected = if (presenter.manga.displayMode == Manga.DISPLAY_NAME) 0 else 1
|
||||
SetDisplayModeDialog(this, preselected).showDialog(router)
|
||||
}
|
||||
|
||||
override fun setDisplayMode(id: Int) {
|
||||
presenter.setDisplayMode(id)
|
||||
adapter?.notifyDataSetChanged()
|
||||
}
|
||||
|
||||
private fun showSortingDialog() {
|
||||
val preselected = if (presenter.manga.sorting == Manga.SORTING_SOURCE) 0 else 1
|
||||
SetSortingDialog(this, preselected).showDialog(router)
|
||||
}
|
||||
|
||||
override fun setSorting(id: Int) {
|
||||
presenter.setSorting(id)
|
||||
}
|
||||
|
||||
private fun showDownloadDialog() {
|
||||
DownloadChaptersDialog(this).showDialog(router)
|
||||
}
|
||||
|
||||
private fun getUnreadChaptersSorted() = presenter.chapters
|
||||
.filter { !it.read && it.status == Download.NOT_DOWNLOADED }
|
||||
.distinctBy { it.name }
|
||||
.sortedByDescending { it.source_order }
|
||||
|
||||
override fun downloadCustomChapters(amount: Int) {
|
||||
val chaptersToDownload = getUnreadChaptersSorted().take(amount)
|
||||
if (chaptersToDownload.isNotEmpty()) {
|
||||
downloadChapters(chaptersToDownload)
|
||||
}
|
||||
}
|
||||
|
||||
private fun showCustomDownloadDialog() {
|
||||
DownloadCustomChaptersDialog(this, presenter.chapters.size).showDialog(router)
|
||||
}
|
||||
|
||||
|
||||
override fun downloadChapters(choice: Int) {
|
||||
// i = 0: Download 1
|
||||
// i = 1: Download 5
|
||||
// i = 2: Download 10
|
||||
// i = 3: Download x
|
||||
// i = 4: Download unread
|
||||
// i = 5: Download all
|
||||
val chaptersToDownload = when (choice) {
|
||||
0 -> getUnreadChaptersSorted().take(1)
|
||||
1 -> getUnreadChaptersSorted().take(5)
|
||||
2 -> getUnreadChaptersSorted().take(10)
|
||||
3 -> {
|
||||
showCustomDownloadDialog()
|
||||
return
|
||||
}
|
||||
4 -> presenter.chapters.filter { !it.read }
|
||||
5 -> presenter.chapters
|
||||
else -> emptyList()
|
||||
}
|
||||
if (chaptersToDownload.isNotEmpty()) {
|
||||
downloadChapters(chaptersToDownload)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,418 +1,418 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
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.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Presenter of [ChaptersController].
|
||||
*/
|
||||
class ChaptersPresenter(
|
||||
val manga: Manga,
|
||||
val source: Source,
|
||||
private val chapterCountRelay: BehaviorRelay<Float>,
|
||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
||||
val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get()
|
||||
) : BasePresenter<ChaptersController>() {
|
||||
|
||||
/**
|
||||
* List of chapters of the manga. It's always unfiltered and unsorted.
|
||||
*/
|
||||
var chapters: List<ChapterItem> = emptyList()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subject of list of chapters to allow updating the view without going to DB.
|
||||
*/
|
||||
val chaptersRelay: PublishRelay<List<ChapterItem>>
|
||||
by lazy { PublishRelay.create<List<ChapterItem>>() }
|
||||
|
||||
/**
|
||||
* Whether the chapter list has been requested to the source.
|
||||
*/
|
||||
var hasRequested = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subscription to retrieve the new list of chapters from the source.
|
||||
*/
|
||||
private var fetchChaptersSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription to observe download status changes.
|
||||
*/
|
||||
private var observeDownloadsSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
// Prepare the relay.
|
||||
chaptersRelay.flatMap { applyChapterFilters(it) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(ChaptersController::onNextChapters,
|
||||
{ _, error -> Timber.e(error) })
|
||||
|
||||
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
||||
// changes, and sends the list of chapters to the relay.
|
||||
add(db.getChapters(manga).asRxObservable()
|
||||
.map { chapters ->
|
||||
// Convert every chapter to a model.
|
||||
chapters.map { it.toModel() }
|
||||
}
|
||||
.doOnNext { chapters ->
|
||||
// Find downloaded chapters
|
||||
setDownloadedChapters(chapters)
|
||||
|
||||
// Store the last emission
|
||||
this.chapters = chapters
|
||||
|
||||
// Listen for download status changes
|
||||
observeDownloads()
|
||||
|
||||
// Emit the number of chapters to the info tab.
|
||||
chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number
|
||||
?: 0f)
|
||||
|
||||
// Emit the upload date of the most recent chapter
|
||||
lastUpdateRelay.call(Date(chapters.maxBy { it.date_upload }?.date_upload
|
||||
?: 0))
|
||||
|
||||
}
|
||||
.subscribe { chaptersRelay.call(it) })
|
||||
}
|
||||
|
||||
private fun observeDownloads() {
|
||||
observeDownloadsSubscription?.let { remove(it) }
|
||||
observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter { download -> download.manga.id == manga.id }
|
||||
.doOnNext { onDownloadStatusChange(it) }
|
||||
.subscribeLatestCache(ChaptersController::onChapterStatusChange,
|
||||
{ _, error -> Timber.e(error) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a chapter from the database to an extended model, allowing to store new fields.
|
||||
*/
|
||||
private fun Chapter.toModel(): ChapterItem {
|
||||
// Create the model object.
|
||||
val model = ChapterItem(this, manga)
|
||||
|
||||
// Find an active download for this chapter.
|
||||
val download = downloadManager.queue.find { it.chapter.id == id }
|
||||
|
||||
if (download != null) {
|
||||
// If there's an active download, assign it.
|
||||
model.download = download
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and assigns the list of downloaded chapters.
|
||||
*
|
||||
* @param chapters the list of chapter from the database.
|
||||
*/
|
||||
private fun setDownloadedChapters(chapters: List<ChapterItem>) {
|
||||
for (chapter in chapters) {
|
||||
if (downloadManager.isChapterDownloaded(chapter, manga)) {
|
||||
chapter.status = Download.DOWNLOADED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an updated list of chapters from the source.
|
||||
*/
|
||||
fun fetchChaptersFromSource() {
|
||||
hasRequested = true
|
||||
|
||||
if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
|
||||
fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onFetchChaptersDone()
|
||||
}, ChaptersController::onFetchChaptersError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI after applying the filters.
|
||||
*/
|
||||
private fun refreshChapters() {
|
||||
chaptersRelay.call(chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the view filters to the list of chapters obtained from the database.
|
||||
* @param chapters the list of chapters from the database
|
||||
* @return an observable of the list of chapters filtered and sorted.
|
||||
*/
|
||||
private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
|
||||
var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
|
||||
if (onlyUnread()) {
|
||||
observable = observable.filter { !it.read }
|
||||
}
|
||||
else if (onlyRead()) {
|
||||
observable = observable.filter { it.read }
|
||||
}
|
||||
if (onlyDownloaded()) {
|
||||
observable = observable.filter { it.isDownloaded || it.manga.source == LocalSource.ID }
|
||||
}
|
||||
if (onlyBookmarked()) {
|
||||
observable = observable.filter { it.bookmark }
|
||||
}
|
||||
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
|
||||
Manga.SORTING_SOURCE -> when (sortDescending()) {
|
||||
true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
|
||||
false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||
}
|
||||
Manga.SORTING_NUMBER -> when (sortDescending()) {
|
||||
true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
|
||||
false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
||||
}
|
||||
else -> throw NotImplementedError("Unimplemented sorting method")
|
||||
}
|
||||
return observable.toSortedList(sortFunction)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a download for the active manga changes status.
|
||||
* @param download the download whose status changed.
|
||||
*/
|
||||
fun onDownloadStatusChange(download: Download) {
|
||||
// Assign the download to the model object.
|
||||
if (download.status == Download.QUEUE) {
|
||||
chapters.find { it.id == download.chapter.id }?.let {
|
||||
if (it.download == null) {
|
||||
it.download = download
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force UI update if downloaded filter active and download finished.
|
||||
if (onlyDownloaded() && download.status == Download.DOWNLOADED)
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next unread chapter or null if everything is read.
|
||||
*/
|
||||
fun getNextUnreadChapter(): ChapterItem? {
|
||||
return chapters.sortedByDescending { it.source_order }.find { !it.read }
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the selected chapter list as read/unread.
|
||||
* @param selectedChapters the list of selected chapters.
|
||||
* @param read whether to mark chapters as read or unread.
|
||||
*/
|
||||
fun markChaptersRead(selectedChapters: List<ChapterItem>, read: Boolean) {
|
||||
Observable.from(selectedChapters)
|
||||
.doOnNext { chapter ->
|
||||
chapter.read = read
|
||||
if (!read) {
|
||||
chapter.last_page_read = 0
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
.flatMap { db.updateChaptersProgress(it).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the given list of chapters with the manager.
|
||||
* @param chapters the list of chapters to download.
|
||||
*/
|
||||
fun downloadChapters(chapters: List<ChapterItem>) {
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookmarks the given list of chapters.
|
||||
* @param selectedChapters the list of chapters to bookmark.
|
||||
*/
|
||||
fun bookmarkChapters(selectedChapters: List<ChapterItem>, bookmarked: Boolean) {
|
||||
Observable.from(selectedChapters)
|
||||
.doOnNext { chapter ->
|
||||
chapter.bookmark = bookmarked
|
||||
}
|
||||
.toList()
|
||||
.flatMap { db.updateChaptersProgress(it).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given list of chapter.
|
||||
* @param chapters the list of chapters to delete.
|
||||
*/
|
||||
fun deleteChapters(chapters: List<ChapterItem>) {
|
||||
Observable.just(chapters)
|
||||
.doOnNext { deleteChaptersInternal(chapters) }
|
||||
.doOnNext { if (onlyDownloaded()) refreshChapters() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onChaptersDeleted()
|
||||
}, ChaptersController::onChaptersDeletedError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a list of chapters from disk. This method is called in a background thread.
|
||||
* @param chapters the chapters to delete.
|
||||
*/
|
||||
private fun deleteChaptersInternal(chapters: List<ChapterItem>) {
|
||||
downloadManager.deleteChapters(chapters, manga, source)
|
||||
chapters.forEach {
|
||||
it.status = Download.NOT_DOWNLOADED
|
||||
it.download = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the sorting and requests an UI update.
|
||||
*/
|
||||
fun revertSortOrder() {
|
||||
manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC)
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the read filter and requests an UI update.
|
||||
* @param onlyUnread whether to display only unread chapters or all chapters.
|
||||
*/
|
||||
fun setUnreadFilter(onlyUnread: Boolean) {
|
||||
manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the read filter and requests an UI update.
|
||||
* @param onlyRead whether to display only read chapters or all chapters.
|
||||
*/
|
||||
fun setReadFilter(onlyRead: Boolean) {
|
||||
manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the download filter and requests an UI update.
|
||||
* @param onlyDownloaded whether to display only downloaded chapters or all chapters.
|
||||
*/
|
||||
fun setDownloadedFilter(onlyDownloaded: Boolean) {
|
||||
manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bookmark filter and requests an UI update.
|
||||
* @param onlyBookmarked whether to display only bookmarked chapters or all chapters.
|
||||
*/
|
||||
fun setBookmarkedFilter(onlyBookmarked: Boolean) {
|
||||
manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all filters and requests an UI update.
|
||||
*/
|
||||
fun removeFilters() {
|
||||
manga.readFilter = Manga.SHOW_ALL
|
||||
manga.downloadedFilter = Manga.SHOW_ALL
|
||||
manga.bookmarkedFilter = Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds manga to library
|
||||
*/
|
||||
fun addToLibrary() {
|
||||
mangaFavoriteRelay.call(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active display mode.
|
||||
* @param mode the mode to set.
|
||||
*/
|
||||
fun setDisplayMode(mode: Int) {
|
||||
manga.displayMode = mode
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sorting method and requests an UI update.
|
||||
* @param sort the sorting mode.
|
||||
*/
|
||||
fun setSorting(sort: Int) {
|
||||
manga.sorting = sort
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only downloaded filter is enabled.
|
||||
*/
|
||||
fun onlyDownloaded(): Boolean {
|
||||
return manga.downloadedFilter == Manga.SHOW_DOWNLOADED
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only downloaded filter is enabled.
|
||||
*/
|
||||
fun onlyBookmarked(): Boolean {
|
||||
return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only unread filter is enabled.
|
||||
*/
|
||||
fun onlyUnread(): Boolean {
|
||||
return manga.readFilter == Manga.SHOW_UNREAD
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only read filter is enabled.
|
||||
*/
|
||||
fun onlyRead(): Boolean {
|
||||
return manga.readFilter == Manga.SHOW_READ
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the sorting method is descending or ascending.
|
||||
*/
|
||||
fun sortDescending(): Boolean {
|
||||
return manga.sortDescending()
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
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.source.LocalSource
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import eu.kanade.tachiyomi.util.syncChaptersWithSource
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import timber.log.Timber
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Presenter of [ChaptersController].
|
||||
*/
|
||||
class ChaptersPresenter(
|
||||
val manga: Manga,
|
||||
val source: Source,
|
||||
private val chapterCountRelay: BehaviorRelay<Float>,
|
||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
||||
val preferences: PreferencesHelper = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get()
|
||||
) : BasePresenter<ChaptersController>() {
|
||||
|
||||
/**
|
||||
* List of chapters of the manga. It's always unfiltered and unsorted.
|
||||
*/
|
||||
var chapters: List<ChapterItem> = emptyList()
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subject of list of chapters to allow updating the view without going to DB.
|
||||
*/
|
||||
val chaptersRelay: PublishRelay<List<ChapterItem>>
|
||||
by lazy { PublishRelay.create<List<ChapterItem>>() }
|
||||
|
||||
/**
|
||||
* Whether the chapter list has been requested to the source.
|
||||
*/
|
||||
var hasRequested = false
|
||||
private set
|
||||
|
||||
/**
|
||||
* Subscription to retrieve the new list of chapters from the source.
|
||||
*/
|
||||
private var fetchChaptersSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription to observe download status changes.
|
||||
*/
|
||||
private var observeDownloadsSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
|
||||
// Prepare the relay.
|
||||
chaptersRelay.flatMap { applyChapterFilters(it) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(ChaptersController::onNextChapters,
|
||||
{ _, error -> Timber.e(error) })
|
||||
|
||||
// Add the subscription that retrieves the chapters from the database, keeps subscribed to
|
||||
// changes, and sends the list of chapters to the relay.
|
||||
add(db.getChapters(manga).asRxObservable()
|
||||
.map { chapters ->
|
||||
// Convert every chapter to a model.
|
||||
chapters.map { it.toModel() }
|
||||
}
|
||||
.doOnNext { chapters ->
|
||||
// Find downloaded chapters
|
||||
setDownloadedChapters(chapters)
|
||||
|
||||
// Store the last emission
|
||||
this.chapters = chapters
|
||||
|
||||
// Listen for download status changes
|
||||
observeDownloads()
|
||||
|
||||
// Emit the number of chapters to the info tab.
|
||||
chapterCountRelay.call(chapters.maxBy { it.chapter_number }?.chapter_number
|
||||
?: 0f)
|
||||
|
||||
// Emit the upload date of the most recent chapter
|
||||
lastUpdateRelay.call(Date(chapters.maxBy { it.date_upload }?.date_upload
|
||||
?: 0))
|
||||
|
||||
}
|
||||
.subscribe { chaptersRelay.call(it) })
|
||||
}
|
||||
|
||||
private fun observeDownloads() {
|
||||
observeDownloadsSubscription?.let { remove(it) }
|
||||
observeDownloadsSubscription = downloadManager.queue.getStatusObservable()
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.filter { download -> download.manga.id == manga.id }
|
||||
.doOnNext { onDownloadStatusChange(it) }
|
||||
.subscribeLatestCache(ChaptersController::onChapterStatusChange,
|
||||
{ _, error -> Timber.e(error) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts a chapter from the database to an extended model, allowing to store new fields.
|
||||
*/
|
||||
private fun Chapter.toModel(): ChapterItem {
|
||||
// Create the model object.
|
||||
val model = ChapterItem(this, manga)
|
||||
|
||||
// Find an active download for this chapter.
|
||||
val download = downloadManager.queue.find { it.chapter.id == id }
|
||||
|
||||
if (download != null) {
|
||||
// If there's an active download, assign it.
|
||||
model.download = download
|
||||
}
|
||||
return model
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds and assigns the list of downloaded chapters.
|
||||
*
|
||||
* @param chapters the list of chapter from the database.
|
||||
*/
|
||||
private fun setDownloadedChapters(chapters: List<ChapterItem>) {
|
||||
for (chapter in chapters) {
|
||||
if (downloadManager.isChapterDownloaded(chapter, manga)) {
|
||||
chapter.status = Download.DOWNLOADED
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Requests an updated list of chapters from the source.
|
||||
*/
|
||||
fun fetchChaptersFromSource() {
|
||||
hasRequested = true
|
||||
|
||||
if (!fetchChaptersSubscription.isNullOrUnsubscribed()) return
|
||||
fetchChaptersSubscription = Observable.defer { source.fetchChapterList(manga) }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.map { syncChaptersWithSource(db, it, manga, source) }
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onFetchChaptersDone()
|
||||
}, ChaptersController::onFetchChaptersError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the UI after applying the filters.
|
||||
*/
|
||||
private fun refreshChapters() {
|
||||
chaptersRelay.call(chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the view filters to the list of chapters obtained from the database.
|
||||
* @param chapters the list of chapters from the database
|
||||
* @return an observable of the list of chapters filtered and sorted.
|
||||
*/
|
||||
private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
|
||||
var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
|
||||
if (onlyUnread()) {
|
||||
observable = observable.filter { !it.read }
|
||||
}
|
||||
else if (onlyRead()) {
|
||||
observable = observable.filter { it.read }
|
||||
}
|
||||
if (onlyDownloaded()) {
|
||||
observable = observable.filter { it.isDownloaded || it.manga.source == LocalSource.ID }
|
||||
}
|
||||
if (onlyBookmarked()) {
|
||||
observable = observable.filter { it.bookmark }
|
||||
}
|
||||
val sortFunction: (Chapter, Chapter) -> Int = when (manga.sorting) {
|
||||
Manga.SORTING_SOURCE -> when (sortDescending()) {
|
||||
true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
|
||||
false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
|
||||
}
|
||||
Manga.SORTING_NUMBER -> when (sortDescending()) {
|
||||
true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
|
||||
false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
|
||||
}
|
||||
else -> throw NotImplementedError("Unimplemented sorting method")
|
||||
}
|
||||
return observable.toSortedList(sortFunction)
|
||||
}
|
||||
|
||||
/**
|
||||
* Called when a download for the active manga changes status.
|
||||
* @param download the download whose status changed.
|
||||
*/
|
||||
fun onDownloadStatusChange(download: Download) {
|
||||
// Assign the download to the model object.
|
||||
if (download.status == Download.QUEUE) {
|
||||
chapters.find { it.id == download.chapter.id }?.let {
|
||||
if (it.download == null) {
|
||||
it.download = download
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Force UI update if downloaded filter active and download finished.
|
||||
if (onlyDownloaded() && download.status == Download.DOWNLOADED)
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the next unread chapter or null if everything is read.
|
||||
*/
|
||||
fun getNextUnreadChapter(): ChapterItem? {
|
||||
return chapters.sortedByDescending { it.source_order }.find { !it.read }
|
||||
}
|
||||
|
||||
/**
|
||||
* Mark the selected chapter list as read/unread.
|
||||
* @param selectedChapters the list of selected chapters.
|
||||
* @param read whether to mark chapters as read or unread.
|
||||
*/
|
||||
fun markChaptersRead(selectedChapters: List<ChapterItem>, read: Boolean) {
|
||||
Observable.from(selectedChapters)
|
||||
.doOnNext { chapter ->
|
||||
chapter.read = read
|
||||
if (!read) {
|
||||
chapter.last_page_read = 0
|
||||
}
|
||||
}
|
||||
.toList()
|
||||
.flatMap { db.updateChaptersProgress(it).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Downloads the given list of chapters with the manager.
|
||||
* @param chapters the list of chapters to download.
|
||||
*/
|
||||
fun downloadChapters(chapters: List<ChapterItem>) {
|
||||
downloadManager.downloadChapters(manga, chapters)
|
||||
}
|
||||
|
||||
/**
|
||||
* Bookmarks the given list of chapters.
|
||||
* @param selectedChapters the list of chapters to bookmark.
|
||||
*/
|
||||
fun bookmarkChapters(selectedChapters: List<ChapterItem>, bookmarked: Boolean) {
|
||||
Observable.from(selectedChapters)
|
||||
.doOnNext { chapter ->
|
||||
chapter.bookmark = bookmarked
|
||||
}
|
||||
.toList()
|
||||
.flatMap { db.updateChaptersProgress(it).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.subscribe()
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes the given list of chapter.
|
||||
* @param chapters the list of chapters to delete.
|
||||
*/
|
||||
fun deleteChapters(chapters: List<ChapterItem>) {
|
||||
Observable.just(chapters)
|
||||
.doOnNext { deleteChaptersInternal(chapters) }
|
||||
.doOnNext { if (onlyDownloaded()) refreshChapters() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onChaptersDeleted()
|
||||
}, ChaptersController::onChaptersDeletedError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a list of chapters from disk. This method is called in a background thread.
|
||||
* @param chapters the chapters to delete.
|
||||
*/
|
||||
private fun deleteChaptersInternal(chapters: List<ChapterItem>) {
|
||||
downloadManager.deleteChapters(chapters, manga, source)
|
||||
chapters.forEach {
|
||||
it.status = Download.NOT_DOWNLOADED
|
||||
it.download = null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverses the sorting and requests an UI update.
|
||||
*/
|
||||
fun revertSortOrder() {
|
||||
manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC)
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the read filter and requests an UI update.
|
||||
* @param onlyUnread whether to display only unread chapters or all chapters.
|
||||
*/
|
||||
fun setUnreadFilter(onlyUnread: Boolean) {
|
||||
manga.readFilter = if (onlyUnread) Manga.SHOW_UNREAD else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the read filter and requests an UI update.
|
||||
* @param onlyRead whether to display only read chapters or all chapters.
|
||||
*/
|
||||
fun setReadFilter(onlyRead: Boolean) {
|
||||
manga.readFilter = if (onlyRead) Manga.SHOW_READ else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the download filter and requests an UI update.
|
||||
* @param onlyDownloaded whether to display only downloaded chapters or all chapters.
|
||||
*/
|
||||
fun setDownloadedFilter(onlyDownloaded: Boolean) {
|
||||
manga.downloadedFilter = if (onlyDownloaded) Manga.SHOW_DOWNLOADED else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the bookmark filter and requests an UI update.
|
||||
* @param onlyBookmarked whether to display only bookmarked chapters or all chapters.
|
||||
*/
|
||||
fun setBookmarkedFilter(onlyBookmarked: Boolean) {
|
||||
manga.bookmarkedFilter = if (onlyBookmarked) Manga.SHOW_BOOKMARKED else Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes all filters and requests an UI update.
|
||||
*/
|
||||
fun removeFilters() {
|
||||
manga.readFilter = Manga.SHOW_ALL
|
||||
manga.downloadedFilter = Manga.SHOW_ALL
|
||||
manga.bookmarkedFilter = Manga.SHOW_ALL
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds manga to library
|
||||
*/
|
||||
fun addToLibrary() {
|
||||
mangaFavoriteRelay.call(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the active display mode.
|
||||
* @param mode the mode to set.
|
||||
*/
|
||||
fun setDisplayMode(mode: Int) {
|
||||
manga.displayMode = mode
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the sorting method and requests an UI update.
|
||||
* @param sort the sorting mode.
|
||||
*/
|
||||
fun setSorting(sort: Int) {
|
||||
manga.sorting = sort
|
||||
db.updateFlags(manga).executeAsBlocking()
|
||||
refreshChapters()
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only downloaded filter is enabled.
|
||||
*/
|
||||
fun onlyDownloaded(): Boolean {
|
||||
return manga.downloadedFilter == Manga.SHOW_DOWNLOADED
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only downloaded filter is enabled.
|
||||
*/
|
||||
fun onlyBookmarked(): Boolean {
|
||||
return manga.bookmarkedFilter == Manga.SHOW_BOOKMARKED
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only unread filter is enabled.
|
||||
*/
|
||||
fun onlyUnread(): Boolean {
|
||||
return manga.readFilter == Manga.SHOW_UNREAD
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the display only read filter is enabled.
|
||||
*/
|
||||
fun onlyRead(): Boolean {
|
||||
return manga.readFilter == Manga.SHOW_READ
|
||||
}
|
||||
|
||||
/**
|
||||
* Whether the sorting method is descending or ascending.
|
||||
*/
|
||||
fun sortDescending(): Boolean {
|
||||
return manga.sortDescending()
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,32 +1,32 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class DeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : DeleteChaptersDialog.Listener {
|
||||
|
||||
constructor(target: T) : this() {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.content(R.string.confirm_delete_chapters)
|
||||
.positiveText(android.R.string.yes)
|
||||
.negativeText(android.R.string.no)
|
||||
.onPositive { _, _ ->
|
||||
(targetController as? Listener)?.deleteChapters()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun deleteChapters()
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class DeleteChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : DeleteChaptersDialog.Listener {
|
||||
|
||||
constructor(target: T) : this() {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.content(R.string.confirm_delete_chapters)
|
||||
.positiveText(android.R.string.yes)
|
||||
.negativeText(android.R.string.no)
|
||||
.onPositive { _, _ ->
|
||||
(targetController as? Listener)?.deleteChapters()
|
||||
}
|
||||
.show()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun deleteChapters()
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,27 +1,27 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "deleting_dialog"
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.progress(true, 0)
|
||||
.content(R.string.deleting)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun showDialog(router: Router) {
|
||||
showDialog(router, TAG)
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Router
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class DeletingChaptersDialog(bundle: Bundle? = null) : DialogController(bundle) {
|
||||
|
||||
companion object {
|
||||
const val TAG = "deleting_dialog"
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.progress(true, 0)
|
||||
.content(R.string.deleting)
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun showDialog(router: Router) {
|
||||
showDialog(router, TAG)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,42 +1,42 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class DownloadChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : DownloadChaptersDialog.Listener {
|
||||
|
||||
constructor(target: T) : this() {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val activity = activity!!
|
||||
|
||||
val choices = intArrayOf(
|
||||
R.string.download_1,
|
||||
R.string.download_5,
|
||||
R.string.download_10,
|
||||
R.string.download_custom,
|
||||
R.string.download_unread,
|
||||
R.string.download_all
|
||||
).map { activity.getString(it) }
|
||||
|
||||
return MaterialDialog.Builder(activity)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.items(choices)
|
||||
.itemsCallback { _, _, position, _ ->
|
||||
(targetController as? Listener)?.downloadChapters(position)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun downloadChapters(choice: Int)
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class DownloadChaptersDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : DownloadChaptersDialog.Listener {
|
||||
|
||||
constructor(target: T) : this() {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val activity = activity!!
|
||||
|
||||
val choices = intArrayOf(
|
||||
R.string.download_1,
|
||||
R.string.download_5,
|
||||
R.string.download_10,
|
||||
R.string.download_custom,
|
||||
R.string.download_unread,
|
||||
R.string.download_all
|
||||
).map { activity.getString(it) }
|
||||
|
||||
return MaterialDialog.Builder(activity)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.items(choices)
|
||||
.itemsCallback { _, _, position, _ ->
|
||||
(targetController as? Listener)?.downloadChapters(position)
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun downloadChapters(choice: Int)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +1,43 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class SetDisplayModeDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : SetDisplayModeDialog.Listener {
|
||||
|
||||
private val selectedIndex = args.getInt("selected", -1)
|
||||
|
||||
constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply {
|
||||
putInt("selected", selectedIndex)
|
||||
}) {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val activity = activity!!
|
||||
val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER)
|
||||
val choices = intArrayOf(R.string.show_title, R.string.show_chapter_number)
|
||||
.map { activity.getString(it) }
|
||||
|
||||
return MaterialDialog.Builder(activity)
|
||||
.title(R.string.action_display_mode)
|
||||
.items(choices)
|
||||
.itemsIds(ids)
|
||||
.itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
|
||||
(targetController as? Listener)?.setDisplayMode(itemView.id)
|
||||
true
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun setDisplayMode(id: Int)
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class SetDisplayModeDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : SetDisplayModeDialog.Listener {
|
||||
|
||||
private val selectedIndex = args.getInt("selected", -1)
|
||||
|
||||
constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply {
|
||||
putInt("selected", selectedIndex)
|
||||
}) {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val activity = activity!!
|
||||
val ids = intArrayOf(Manga.DISPLAY_NAME, Manga.DISPLAY_NUMBER)
|
||||
val choices = intArrayOf(R.string.show_title, R.string.show_chapter_number)
|
||||
.map { activity.getString(it) }
|
||||
|
||||
return MaterialDialog.Builder(activity)
|
||||
.title(R.string.action_display_mode)
|
||||
.items(choices)
|
||||
.itemsIds(ids)
|
||||
.itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
|
||||
(targetController as? Listener)?.setDisplayMode(itemView.id)
|
||||
true
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun setDisplayMode(id: Int)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,43 +1,43 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class SetSortingDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : SetSortingDialog.Listener {
|
||||
|
||||
private val selectedIndex = args.getInt("selected", -1)
|
||||
|
||||
constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply {
|
||||
putInt("selected", selectedIndex)
|
||||
}) {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val activity = activity!!
|
||||
val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER)
|
||||
val choices = intArrayOf(R.string.sort_by_source, R.string.sort_by_number)
|
||||
.map { activity.getString(it) }
|
||||
|
||||
return MaterialDialog.Builder(activity)
|
||||
.title(R.string.sorting_mode)
|
||||
.items(choices)
|
||||
.itemsIds(ids)
|
||||
.itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
|
||||
(targetController as? Listener)?.setSorting(itemView.id)
|
||||
true
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun setSorting(id: Int)
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.chapter
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
|
||||
class SetSortingDialog<T>(bundle: Bundle? = null) : DialogController(bundle)
|
||||
where T : Controller, T : SetSortingDialog.Listener {
|
||||
|
||||
private val selectedIndex = args.getInt("selected", -1)
|
||||
|
||||
constructor(target: T, selectedIndex: Int = -1) : this(Bundle().apply {
|
||||
putInt("selected", selectedIndex)
|
||||
}) {
|
||||
targetController = target
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val activity = activity!!
|
||||
val ids = intArrayOf(Manga.SORTING_SOURCE, Manga.SORTING_NUMBER)
|
||||
val choices = intArrayOf(R.string.sort_by_source, R.string.sort_by_number)
|
||||
.map { activity.getString(it) }
|
||||
|
||||
return MaterialDialog.Builder(activity)
|
||||
.title(R.string.sorting_mode)
|
||||
.items(choices)
|
||||
.itemsIds(ids)
|
||||
.itemsCallbackSingleChoice(selectedIndex) { _, itemView, _, _ ->
|
||||
(targetController as? Listener)?.setSorting(itemView.id)
|
||||
true
|
||||
}
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun setSorting(id: Int)
|
||||
}
|
||||
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,173 +1,173 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.info
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Presenter of MangaInfoFragment.
|
||||
* Contains information and data for fragment.
|
||||
* Observable updates should be called from here.
|
||||
*/
|
||||
class MangaInfoPresenter(
|
||||
val manga: Manga,
|
||||
val source: Source,
|
||||
private val chapterCountRelay: BehaviorRelay<Float>,
|
||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get()
|
||||
) : BasePresenter<MangaInfoController>() {
|
||||
|
||||
/**
|
||||
* Subscription to send the manga to the view.
|
||||
*/
|
||||
private var viewMangaSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription to update the manga from the source.
|
||||
*/
|
||||
private var fetchMangaSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
sendMangaToView()
|
||||
|
||||
// Update chapter count
|
||||
chapterCountRelay.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(MangaInfoController::setChapterCount)
|
||||
|
||||
// Update favorite status
|
||||
mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { setFavorite(it) }
|
||||
.apply { add(this) }
|
||||
|
||||
//update last update date
|
||||
lastUpdateRelay.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(MangaInfoController::setLastUpdateDate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the active manga to the view.
|
||||
*/
|
||||
fun sendMangaToView() {
|
||||
viewMangaSubscription?.let { remove(it) }
|
||||
viewMangaSubscription = Observable.just(manga)
|
||||
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch manga information from source.
|
||||
*/
|
||||
fun fetchMangaFromSource() {
|
||||
if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
|
||||
fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
|
||||
.map { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
manga
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { sendMangaToView() }
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onFetchMangaDone()
|
||||
}, MangaInfoController::onFetchMangaError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update favorite status of manga, (removes / adds) manga (to / from) library.
|
||||
*
|
||||
* @return the new status of the manga.
|
||||
*/
|
||||
fun toggleFavorite(): Boolean {
|
||||
manga.favorite = !manga.favorite
|
||||
if (!manga.favorite) {
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
}
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
sendMangaToView()
|
||||
return manga.favorite
|
||||
}
|
||||
|
||||
private fun setFavorite(favorite: Boolean) {
|
||||
if (manga.favorite == favorite) {
|
||||
return
|
||||
}
|
||||
toggleFavorite()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the manga has any downloads.
|
||||
*/
|
||||
fun hasDownloads(): Boolean {
|
||||
return downloadManager.getDownloadCount(manga) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all the downloads for the manga.
|
||||
*/
|
||||
fun deleteDownloads() {
|
||||
downloadManager.deleteManga(manga, source)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user categories.
|
||||
*
|
||||
* @return List of categories, not including the default category
|
||||
*/
|
||||
fun getCategories(): List<Category> {
|
||||
return db.getCategories().executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||
*
|
||||
* @param manga the manga to get categories from.
|
||||
* @return Array of category ids the manga is in, if none returns default id
|
||||
*/
|
||||
fun getMangaCategoryIds(manga: Manga): Array<Int> {
|
||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
return categories.mapNotNull { it.id }.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to categories.
|
||||
*
|
||||
* @param manga the manga to move.
|
||||
* @param categories the selected categories.
|
||||
*/
|
||||
fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
||||
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
||||
db.setMangaCategories(mc, listOf(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to the category.
|
||||
*
|
||||
* @param manga the manga to move.
|
||||
* @param category the selected category, or null for default category.
|
||||
*/
|
||||
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
||||
moveMangaToCategories(manga, listOfNotNull(category))
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.info
|
||||
|
||||
import android.os.Bundle
|
||||
import com.jakewharton.rxrelay.BehaviorRelay
|
||||
import com.jakewharton.rxrelay.PublishRelay
|
||||
import eu.kanade.tachiyomi.data.cache.CoverCache
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Category
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||
import eu.kanade.tachiyomi.data.download.DownloadManager
|
||||
import eu.kanade.tachiyomi.source.Source
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.isNullOrUnsubscribed
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.*
|
||||
|
||||
/**
|
||||
* Presenter of MangaInfoFragment.
|
||||
* Contains information and data for fragment.
|
||||
* Observable updates should be called from here.
|
||||
*/
|
||||
class MangaInfoPresenter(
|
||||
val manga: Manga,
|
||||
val source: Source,
|
||||
private val chapterCountRelay: BehaviorRelay<Float>,
|
||||
private val lastUpdateRelay: BehaviorRelay<Date>,
|
||||
private val mangaFavoriteRelay: PublishRelay<Boolean>,
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val downloadManager: DownloadManager = Injekt.get(),
|
||||
private val coverCache: CoverCache = Injekt.get()
|
||||
) : BasePresenter<MangaInfoController>() {
|
||||
|
||||
/**
|
||||
* Subscription to send the manga to the view.
|
||||
*/
|
||||
private var viewMangaSubscription: Subscription? = null
|
||||
|
||||
/**
|
||||
* Subscription to update the manga from the source.
|
||||
*/
|
||||
private var fetchMangaSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
sendMangaToView()
|
||||
|
||||
// Update chapter count
|
||||
chapterCountRelay.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(MangaInfoController::setChapterCount)
|
||||
|
||||
// Update favorite status
|
||||
mangaFavoriteRelay.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe { setFavorite(it) }
|
||||
.apply { add(this) }
|
||||
|
||||
//update last update date
|
||||
lastUpdateRelay.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(MangaInfoController::setLastUpdateDate)
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends the active manga to the view.
|
||||
*/
|
||||
fun sendMangaToView() {
|
||||
viewMangaSubscription?.let { remove(it) }
|
||||
viewMangaSubscription = Observable.just(manga)
|
||||
.subscribeLatestCache({ view, manga -> view.onNextManga(manga, source) })
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch manga information from source.
|
||||
*/
|
||||
fun fetchMangaFromSource() {
|
||||
if (!fetchMangaSubscription.isNullOrUnsubscribed()) return
|
||||
fetchMangaSubscription = Observable.defer { source.fetchMangaDetails(manga) }
|
||||
.map { networkManga ->
|
||||
manga.copyFrom(networkManga)
|
||||
manga.initialized = true
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
manga
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { sendMangaToView() }
|
||||
.subscribeFirst({ view, _ ->
|
||||
view.onFetchMangaDone()
|
||||
}, MangaInfoController::onFetchMangaError)
|
||||
}
|
||||
|
||||
/**
|
||||
* Update favorite status of manga, (removes / adds) manga (to / from) library.
|
||||
*
|
||||
* @return the new status of the manga.
|
||||
*/
|
||||
fun toggleFavorite(): Boolean {
|
||||
manga.favorite = !manga.favorite
|
||||
if (!manga.favorite) {
|
||||
coverCache.deleteFromCache(manga.thumbnail_url)
|
||||
}
|
||||
db.insertManga(manga).executeAsBlocking()
|
||||
sendMangaToView()
|
||||
return manga.favorite
|
||||
}
|
||||
|
||||
private fun setFavorite(favorite: Boolean) {
|
||||
if (manga.favorite == favorite) {
|
||||
return
|
||||
}
|
||||
toggleFavorite()
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if the manga has any downloads.
|
||||
*/
|
||||
fun hasDownloads(): Boolean {
|
||||
return downloadManager.getDownloadCount(manga) > 0
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes all the downloads for the manga.
|
||||
*/
|
||||
fun deleteDownloads() {
|
||||
downloadManager.deleteManga(manga, source)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user categories.
|
||||
*
|
||||
* @return List of categories, not including the default category
|
||||
*/
|
||||
fun getCategories(): List<Category> {
|
||||
return db.getCategories().executeAsBlocking()
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
|
||||
*
|
||||
* @param manga the manga to get categories from.
|
||||
* @return Array of category ids the manga is in, if none returns default id
|
||||
*/
|
||||
fun getMangaCategoryIds(manga: Manga): Array<Int> {
|
||||
val categories = db.getCategoriesForManga(manga).executeAsBlocking()
|
||||
return categories.mapNotNull { it.id }.toTypedArray()
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to categories.
|
||||
*
|
||||
* @param manga the manga to move.
|
||||
* @param categories the selected categories.
|
||||
*/
|
||||
fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
|
||||
val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
|
||||
db.setMangaCategories(mc, listOf(manga))
|
||||
}
|
||||
|
||||
/**
|
||||
* Move the given manga to the category.
|
||||
*
|
||||
* @param manga the manga to move.
|
||||
* @param category the selected category, or null for default category.
|
||||
*/
|
||||
fun moveMangaToCategory(manga: Manga, category: Category?) {
|
||||
moveMangaToCategories(manga, listOfNotNull(category))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,74 +1,74 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.widget.NumberPicker
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SetTrackChaptersDialog<T> : DialogController
|
||||
where T : Controller, T : SetTrackChaptersDialog.Listener {
|
||||
|
||||
private val item: TrackItem
|
||||
|
||||
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
||||
putSerializable(KEY_ITEM_TRACK, item.track)
|
||||
}) {
|
||||
targetController = target
|
||||
this.item = item
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : super(bundle) {
|
||||
val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
|
||||
val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
|
||||
item = TrackItem(track, service)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val item = item
|
||||
|
||||
val dialog = MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.chapters)
|
||||
.customView(R.layout.track_chapters_dialog, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { dialog, _ ->
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
// Remove focus to update selected number
|
||||
val np: NumberPicker = view.findViewById(R.id.chapters_picker)
|
||||
np.clearFocus()
|
||||
|
||||
(targetController as? Listener)?.setChaptersRead(item, np.value)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
val np: NumberPicker = view.findViewById(R.id.chapters_picker)
|
||||
// Set initial value
|
||||
np.value = item.track?.last_chapter_read ?: 0
|
||||
// Don't allow to go from 0 to 9999
|
||||
np.wrapSelectorWheel = false
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun setChaptersRead(item: TrackItem, chaptersRead: Int)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY_ITEM_TRACK = "SetTrackChaptersDialog.item.track"
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.widget.NumberPicker
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SetTrackChaptersDialog<T> : DialogController
|
||||
where T : Controller, T : SetTrackChaptersDialog.Listener {
|
||||
|
||||
private val item: TrackItem
|
||||
|
||||
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
||||
putSerializable(KEY_ITEM_TRACK, item.track)
|
||||
}) {
|
||||
targetController = target
|
||||
this.item = item
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : super(bundle) {
|
||||
val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
|
||||
val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
|
||||
item = TrackItem(track, service)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val item = item
|
||||
|
||||
val dialog = MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.chapters)
|
||||
.customView(R.layout.track_chapters_dialog, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { dialog, _ ->
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
// Remove focus to update selected number
|
||||
val np: NumberPicker = view.findViewById(R.id.chapters_picker)
|
||||
np.clearFocus()
|
||||
|
||||
(targetController as? Listener)?.setChaptersRead(item, np.value)
|
||||
}
|
||||
}
|
||||
.build()
|
||||
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
val np: NumberPicker = view.findViewById(R.id.chapters_picker)
|
||||
// Set initial value
|
||||
np.value = item.track?.last_chapter_read ?: 0
|
||||
// Don't allow to go from 0 to 9999
|
||||
np.wrapSelectorWheel = false
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun setChaptersRead(item: TrackItem, chaptersRead: Int)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY_ITEM_TRACK = "SetTrackChaptersDialog.item.track"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,80 +1,80 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.widget.NumberPicker
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SetTrackScoreDialog<T> : DialogController
|
||||
where T : Controller, T : SetTrackScoreDialog.Listener {
|
||||
|
||||
private val item: TrackItem
|
||||
|
||||
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
||||
putSerializable(KEY_ITEM_TRACK, item.track)
|
||||
}) {
|
||||
targetController = target
|
||||
this.item = item
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : super(bundle) {
|
||||
val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
|
||||
val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
|
||||
item = TrackItem(track, service)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val item = item
|
||||
|
||||
val dialog = MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.score)
|
||||
.customView(R.layout.track_score_dialog, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { dialog, _ ->
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
// Remove focus to update selected number
|
||||
val np: NumberPicker = view.findViewById(R.id.score_picker)
|
||||
np.clearFocus()
|
||||
|
||||
(targetController as? Listener)?.setScore(item, np.value)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
val np: NumberPicker = view.findViewById(R.id.score_picker)
|
||||
val scores = item.service.getScoreList().toTypedArray()
|
||||
np.maxValue = scores.size - 1
|
||||
np.displayedValues = scores
|
||||
|
||||
// Set initial value
|
||||
val displayedScore = item.service.displayScore(item.track!!)
|
||||
if (displayedScore != "-") {
|
||||
val index = scores.indexOf(displayedScore)
|
||||
np.value = if (index != -1) index else 0
|
||||
}
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun setScore(item: TrackItem, score: Int)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY_ITEM_TRACK = "SetTrackScoreDialog.item.track"
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.widget.NumberPicker
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SetTrackScoreDialog<T> : DialogController
|
||||
where T : Controller, T : SetTrackScoreDialog.Listener {
|
||||
|
||||
private val item: TrackItem
|
||||
|
||||
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
||||
putSerializable(KEY_ITEM_TRACK, item.track)
|
||||
}) {
|
||||
targetController = target
|
||||
this.item = item
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : super(bundle) {
|
||||
val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
|
||||
val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
|
||||
item = TrackItem(track, service)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val item = item
|
||||
|
||||
val dialog = MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.score)
|
||||
.customView(R.layout.track_score_dialog, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { dialog, _ ->
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
// Remove focus to update selected number
|
||||
val np: NumberPicker = view.findViewById(R.id.score_picker)
|
||||
np.clearFocus()
|
||||
|
||||
(targetController as? Listener)?.setScore(item, np.value)
|
||||
}
|
||||
}
|
||||
.show()
|
||||
|
||||
val view = dialog.customView
|
||||
if (view != null) {
|
||||
val np: NumberPicker = view.findViewById(R.id.score_picker)
|
||||
val scores = item.service.getScoreList().toTypedArray()
|
||||
np.maxValue = scores.size - 1
|
||||
np.displayedValues = scores
|
||||
|
||||
// Set initial value
|
||||
val displayedScore = item.service.displayScore(item.track!!)
|
||||
if (displayedScore != "-") {
|
||||
val index = scores.indexOf(displayedScore)
|
||||
np.value = if (index != -1) index else 0
|
||||
}
|
||||
}
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun setScore(item: TrackItem, score: Int)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY_ITEM_TRACK = "SetTrackScoreDialog.item.track"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,58 +1,58 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SetTrackStatusDialog<T> : DialogController
|
||||
where T : Controller, T : SetTrackStatusDialog.Listener {
|
||||
|
||||
private val item: TrackItem
|
||||
|
||||
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
||||
putSerializable(KEY_ITEM_TRACK, item.track)
|
||||
}) {
|
||||
targetController = target
|
||||
this.item = item
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : super(bundle) {
|
||||
val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
|
||||
val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
|
||||
item = TrackItem(track, service)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val item = item
|
||||
val statusList = item.service.getStatusList().orEmpty()
|
||||
val statusString = statusList.mapNotNull { item.service.getStatus(it) }
|
||||
val selectedIndex = statusList.indexOf(item.track?.status)
|
||||
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.status)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.items(statusString)
|
||||
.itemsCallbackSingleChoice(selectedIndex, { _, _, i, _ ->
|
||||
(targetController as? Listener)?.setStatus(item, i)
|
||||
true
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun setStatus(item: TrackItem, selection: Int)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY_ITEM_TRACK = "SetTrackStatusDialog.item.track"
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.bluelinelabs.conductor.Controller
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
class SetTrackStatusDialog<T> : DialogController
|
||||
where T : Controller, T : SetTrackStatusDialog.Listener {
|
||||
|
||||
private val item: TrackItem
|
||||
|
||||
constructor(target: T, item: TrackItem) : super(Bundle().apply {
|
||||
putSerializable(KEY_ITEM_TRACK, item.track)
|
||||
}) {
|
||||
targetController = target
|
||||
this.item = item
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : super(bundle) {
|
||||
val track = bundle.getSerializable(KEY_ITEM_TRACK) as Track
|
||||
val service = Injekt.get<TrackManager>().getService(track.sync_id)!!
|
||||
item = TrackItem(track, service)
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedViewState: Bundle?): Dialog {
|
||||
val item = item
|
||||
val statusList = item.service.getStatusList().orEmpty()
|
||||
val statusString = statusList.mapNotNull { item.service.getStatus(it) }
|
||||
val selectedIndex = statusList.indexOf(item.track?.status)
|
||||
|
||||
return MaterialDialog.Builder(activity!!)
|
||||
.title(R.string.status)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.items(statusString)
|
||||
.itemsCallbackSingleChoice(selectedIndex, { _, _, i, _ ->
|
||||
(targetController as? Listener)?.setStatus(item, i)
|
||||
true
|
||||
})
|
||||
.build()
|
||||
}
|
||||
|
||||
interface Listener {
|
||||
fun setStatus(item: TrackItem, selection: Int)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY_ITEM_TRACK = "SetTrackStatusDialog.item.track"
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,45 +1,45 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
|
||||
class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHolder>() {
|
||||
|
||||
var items = emptyList<TrackItem>()
|
||||
set(value) {
|
||||
if (field !== value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
val rowClickListener: OnClickListener = controller
|
||||
|
||||
fun getItem(index: Int): TrackItem? {
|
||||
return items.getOrNull(index)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
|
||||
val view = parent.inflate(R.layout.track_item)
|
||||
return TrackHolder(view, this)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TrackHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
interface OnClickListener {
|
||||
fun onLogoClick(position: Int)
|
||||
fun onTitleClick(position: Int)
|
||||
fun onStatusClick(position: Int)
|
||||
fun onChaptersClick(position: Int)
|
||||
fun onScoreClick(position: Int)
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import androidx.recyclerview.widget.RecyclerView
|
||||
import android.view.ViewGroup
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
|
||||
class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHolder>() {
|
||||
|
||||
var items = emptyList<TrackItem>()
|
||||
set(value) {
|
||||
if (field !== value) {
|
||||
field = value
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
}
|
||||
|
||||
val rowClickListener: OnClickListener = controller
|
||||
|
||||
fun getItem(index: Int): TrackItem? {
|
||||
return items.getOrNull(index)
|
||||
}
|
||||
|
||||
override fun getItemCount(): Int {
|
||||
return items.size
|
||||
}
|
||||
|
||||
override fun onCreateViewHolder(parent: ViewGroup, viewType: Int): TrackHolder {
|
||||
val view = parent.inflate(R.layout.track_item)
|
||||
return TrackHolder(view, this)
|
||||
}
|
||||
|
||||
override fun onBindViewHolder(holder: TrackHolder, position: Int) {
|
||||
holder.bind(items[position])
|
||||
}
|
||||
|
||||
interface OnClickListener {
|
||||
fun onLogoClick(position: Int)
|
||||
fun onTitleClick(position: Int)
|
||||
fun onStatusClick(position: Int)
|
||||
fun onChaptersClick(position: Int)
|
||||
fun onScoreClick(position: Int)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,142 +1,142 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.track_controller.*
|
||||
import timber.log.Timber
|
||||
|
||||
class TrackController : NucleusController<TrackPresenter>(),
|
||||
TrackAdapter.OnClickListener,
|
||||
SetTrackStatusDialog.Listener,
|
||||
SetTrackChaptersDialog.Listener,
|
||||
SetTrackScoreDialog.Listener {
|
||||
|
||||
private var adapter: TrackAdapter? = null
|
||||
|
||||
init {
|
||||
// There's no menu, but this avoids a bug when coming from the catalogue, where the menu
|
||||
// disappears if the searchview is expanded
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun createPresenter(): TrackPresenter {
|
||||
return TrackPresenter((parentController as MangaController).manga!!)
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.track_controller, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
adapter = TrackAdapter(this)
|
||||
with(view) {
|
||||
track_recycler.layoutManager = LinearLayoutManager(context)
|
||||
track_recycler.adapter = adapter
|
||||
swipe_refresh.isEnabled = false
|
||||
swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
fun onNextTrackings(trackings: List<TrackItem>) {
|
||||
val atLeastOneLink = trackings.any { it.track != null }
|
||||
adapter?.items = trackings
|
||||
swipe_refresh?.isEnabled = atLeastOneLink
|
||||
(parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
|
||||
}
|
||||
|
||||
fun onSearchResults(results: List<TrackSearch>) {
|
||||
getSearchDialog()?.onSearchResults(results)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onSearchResultsError(error: Throwable) {
|
||||
Timber.e(error)
|
||||
getSearchDialog()?.onSearchResultsError()
|
||||
}
|
||||
|
||||
private fun getSearchDialog(): TrackSearchDialog? {
|
||||
return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog
|
||||
}
|
||||
|
||||
fun onRefreshDone() {
|
||||
swipe_refresh?.isRefreshing = false
|
||||
}
|
||||
|
||||
fun onRefreshError(error: Throwable) {
|
||||
swipe_refresh?.isRefreshing = false
|
||||
activity?.toast(error.message)
|
||||
}
|
||||
|
||||
override fun onLogoClick(position: Int) {
|
||||
val track = adapter?.getItem(position)?.track ?: return
|
||||
|
||||
if (track.tracking_url.isNullOrBlank()) {
|
||||
activity?.toast(R.string.url_not_set)
|
||||
} else {
|
||||
activity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url)))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTitleClick(position: Int) {
|
||||
val item = adapter?.getItem(position) ?: return
|
||||
TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER)
|
||||
}
|
||||
|
||||
override fun onStatusClick(position: Int) {
|
||||
val item = adapter?.getItem(position) ?: return
|
||||
if (item.track == null) return
|
||||
|
||||
SetTrackStatusDialog(this, item).showDialog(router)
|
||||
}
|
||||
|
||||
override fun onChaptersClick(position: Int) {
|
||||
val item = adapter?.getItem(position) ?: return
|
||||
if (item.track == null) return
|
||||
|
||||
SetTrackChaptersDialog(this, item).showDialog(router)
|
||||
}
|
||||
|
||||
override fun onScoreClick(position: Int) {
|
||||
val item = adapter?.getItem(position) ?: return
|
||||
if (item.track == null) return
|
||||
|
||||
SetTrackScoreDialog(this, item).showDialog(router)
|
||||
}
|
||||
|
||||
override fun setStatus(item: TrackItem, selection: Int) {
|
||||
presenter.setStatus(item, selection)
|
||||
swipe_refresh?.isRefreshing = true
|
||||
}
|
||||
|
||||
override fun setScore(item: TrackItem, score: Int) {
|
||||
presenter.setScore(item, score)
|
||||
swipe_refresh?.isRefreshing = true
|
||||
}
|
||||
|
||||
override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
|
||||
presenter.setLastChapterRead(item, chaptersRead)
|
||||
swipe_refresh?.isRefreshing = true
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG_SEARCH_CONTROLLER = "track_search_controller"
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import androidx.recyclerview.widget.LinearLayoutManager
|
||||
import android.view.LayoutInflater
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import com.jakewharton.rxbinding.support.v4.widget.refreshes
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
|
||||
import eu.kanade.tachiyomi.ui.manga.MangaController
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import kotlinx.android.synthetic.main.track_controller.*
|
||||
import timber.log.Timber
|
||||
|
||||
class TrackController : NucleusController<TrackPresenter>(),
|
||||
TrackAdapter.OnClickListener,
|
||||
SetTrackStatusDialog.Listener,
|
||||
SetTrackChaptersDialog.Listener,
|
||||
SetTrackScoreDialog.Listener {
|
||||
|
||||
private var adapter: TrackAdapter? = null
|
||||
|
||||
init {
|
||||
// There's no menu, but this avoids a bug when coming from the catalogue, where the menu
|
||||
// disappears if the searchview is expanded
|
||||
setHasOptionsMenu(true)
|
||||
}
|
||||
|
||||
override fun createPresenter(): TrackPresenter {
|
||||
return TrackPresenter((parentController as MangaController).manga!!)
|
||||
}
|
||||
|
||||
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
|
||||
return inflater.inflate(R.layout.track_controller, container, false)
|
||||
}
|
||||
|
||||
override fun onViewCreated(view: View) {
|
||||
super.onViewCreated(view)
|
||||
|
||||
adapter = TrackAdapter(this)
|
||||
with(view) {
|
||||
track_recycler.layoutManager = LinearLayoutManager(context)
|
||||
track_recycler.adapter = adapter
|
||||
swipe_refresh.isEnabled = false
|
||||
swipe_refresh.refreshes().subscribeUntilDestroy { presenter.refresh() }
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
adapter = null
|
||||
super.onDestroyView(view)
|
||||
}
|
||||
|
||||
fun onNextTrackings(trackings: List<TrackItem>) {
|
||||
val atLeastOneLink = trackings.any { it.track != null }
|
||||
adapter?.items = trackings
|
||||
swipe_refresh?.isEnabled = atLeastOneLink
|
||||
(parentController as? MangaController)?.setTrackingIcon(atLeastOneLink)
|
||||
}
|
||||
|
||||
fun onSearchResults(results: List<TrackSearch>) {
|
||||
getSearchDialog()?.onSearchResults(results)
|
||||
}
|
||||
|
||||
@Suppress("UNUSED_PARAMETER")
|
||||
fun onSearchResultsError(error: Throwable) {
|
||||
Timber.e(error)
|
||||
getSearchDialog()?.onSearchResultsError()
|
||||
}
|
||||
|
||||
private fun getSearchDialog(): TrackSearchDialog? {
|
||||
return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog
|
||||
}
|
||||
|
||||
fun onRefreshDone() {
|
||||
swipe_refresh?.isRefreshing = false
|
||||
}
|
||||
|
||||
fun onRefreshError(error: Throwable) {
|
||||
swipe_refresh?.isRefreshing = false
|
||||
activity?.toast(error.message)
|
||||
}
|
||||
|
||||
override fun onLogoClick(position: Int) {
|
||||
val track = adapter?.getItem(position)?.track ?: return
|
||||
|
||||
if (track.tracking_url.isNullOrBlank()) {
|
||||
activity?.toast(R.string.url_not_set)
|
||||
} else {
|
||||
activity?.startActivity(Intent(Intent.ACTION_VIEW, Uri.parse(track.tracking_url)))
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTitleClick(position: Int) {
|
||||
val item = adapter?.getItem(position) ?: return
|
||||
TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER)
|
||||
}
|
||||
|
||||
override fun onStatusClick(position: Int) {
|
||||
val item = adapter?.getItem(position) ?: return
|
||||
if (item.track == null) return
|
||||
|
||||
SetTrackStatusDialog(this, item).showDialog(router)
|
||||
}
|
||||
|
||||
override fun onChaptersClick(position: Int) {
|
||||
val item = adapter?.getItem(position) ?: return
|
||||
if (item.track == null) return
|
||||
|
||||
SetTrackChaptersDialog(this, item).showDialog(router)
|
||||
}
|
||||
|
||||
override fun onScoreClick(position: Int) {
|
||||
val item = adapter?.getItem(position) ?: return
|
||||
if (item.track == null) return
|
||||
|
||||
SetTrackScoreDialog(this, item).showDialog(router)
|
||||
}
|
||||
|
||||
override fun setStatus(item: TrackItem, selection: Int) {
|
||||
presenter.setStatus(item, selection)
|
||||
swipe_refresh?.isRefreshing = true
|
||||
}
|
||||
|
||||
override fun setScore(item: TrackItem, score: Int) {
|
||||
presenter.setScore(item, score)
|
||||
swipe_refresh?.isRefreshing = true
|
||||
}
|
||||
|
||||
override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
|
||||
presenter.setLastChapterRead(item, chaptersRead)
|
||||
swipe_refresh?.isRefreshing = true
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val TAG_SEARCH_CONTROLLER = "track_search_controller"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,42 +1,42 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
|
||||
import kotlinx.android.synthetic.main.track_item.*
|
||||
|
||||
class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
|
||||
|
||||
init {
|
||||
val listener = adapter.rowClickListener
|
||||
logo_container.setOnClickListener { listener.onLogoClick(adapterPosition) }
|
||||
title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
|
||||
status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
|
||||
chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
|
||||
score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Suppress("DEPRECATION")
|
||||
fun bind(item: TrackItem) {
|
||||
val track = item.track
|
||||
track_logo.setImageResource(item.service.getLogo())
|
||||
logo_container.setBackgroundColor(item.service.getLogoColor())
|
||||
if (track != null) {
|
||||
track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary)
|
||||
track_title.setAllCaps(false)
|
||||
track_title.text = track.title
|
||||
track_chapters.text = "${track.last_chapter_read}/" +
|
||||
if (track.total_chapters > 0) track.total_chapters else "-"
|
||||
track_status.text = item.service.getStatus(track.status)
|
||||
track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
|
||||
} else {
|
||||
track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button)
|
||||
track_title.setText(R.string.action_edit)
|
||||
track_chapters.text = ""
|
||||
track_score.text = ""
|
||||
track_status.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.view.View
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.ui.base.holder.BaseViewHolder
|
||||
import kotlinx.android.synthetic.main.track_item.*
|
||||
|
||||
class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
|
||||
|
||||
init {
|
||||
val listener = adapter.rowClickListener
|
||||
logo_container.setOnClickListener { listener.onLogoClick(adapterPosition) }
|
||||
title_container.setOnClickListener { listener.onTitleClick(adapterPosition) }
|
||||
status_container.setOnClickListener { listener.onStatusClick(adapterPosition) }
|
||||
chapters_container.setOnClickListener { listener.onChaptersClick(adapterPosition) }
|
||||
score_container.setOnClickListener { listener.onScoreClick(adapterPosition) }
|
||||
}
|
||||
|
||||
@SuppressLint("SetTextI18n")
|
||||
@Suppress("DEPRECATION")
|
||||
fun bind(item: TrackItem) {
|
||||
val track = item.track
|
||||
track_logo.setImageResource(item.service.getLogo())
|
||||
logo_container.setBackgroundColor(item.service.getLogoColor())
|
||||
if (track != null) {
|
||||
track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Regular_Body1_Secondary)
|
||||
track_title.setAllCaps(false)
|
||||
track_title.text = track.title
|
||||
track_chapters.text = "${track.last_chapter_read}/" +
|
||||
if (track.total_chapters > 0) track.total_chapters else "-"
|
||||
track_status.text = item.service.getStatus(track.status)
|
||||
track_score.text = if (track.score == 0f) "-" else item.service.displayScore(track)
|
||||
} else {
|
||||
track_title.setTextAppearance(itemView.context, R.style.TextAppearance_Medium_Button)
|
||||
track_title.setText(R.string.action_edit)
|
||||
track_chapters.text = ""
|
||||
track_score.text = ""
|
||||
track_status.text = ""
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
|
||||
data class TrackItem(val track: Track?, val service: TrackService)
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
|
||||
data class TrackItem(val track: Track?, val service: TrackService)
|
||||
|
||||
@@ -1,130 +1,130 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
||||
class TrackPresenter(
|
||||
val manga: Manga,
|
||||
preferences: PreferencesHelper = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val trackManager: TrackManager = Injekt.get()
|
||||
) : BasePresenter<TrackController>() {
|
||||
|
||||
private val context = preferences.context
|
||||
|
||||
private var trackList: List<TrackItem> = emptyList()
|
||||
|
||||
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
|
||||
private var trackSubscription: Subscription? = null
|
||||
|
||||
private var searchSubscription: Subscription? = null
|
||||
|
||||
private var refreshSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
fetchTrackings()
|
||||
}
|
||||
|
||||
fun fetchTrackings() {
|
||||
trackSubscription?.let { remove(it) }
|
||||
trackSubscription = db.getTracks(manga)
|
||||
.asRxObservable()
|
||||
.map { tracks ->
|
||||
loggedServices.map { service ->
|
||||
TrackItem(tracks.find { it.sync_id == service.id }, service)
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { trackList = it }
|
||||
.subscribeLatestCache(TrackController::onNextTrackings)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
refreshSubscription?.let { remove(it) }
|
||||
refreshSubscription = Observable.from(trackList)
|
||||
.filter { it.track != null }
|
||||
.concatMap { item ->
|
||||
item.service.refresh(item.track!!)
|
||||
.flatMap { db.insertTrack(it).asRxObservable() }
|
||||
.map { item }
|
||||
.onErrorReturn { item }
|
||||
}
|
||||
.toList()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ -> view.onRefreshDone() },
|
||||
TrackController::onRefreshError)
|
||||
}
|
||||
|
||||
fun search(query: String, service: TrackService) {
|
||||
searchSubscription?.let { remove(it) }
|
||||
searchSubscription = service.search(query)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(TrackController::onSearchResults,
|
||||
TrackController::onSearchResultsError)
|
||||
}
|
||||
|
||||
fun registerTracking(item: Track?, service: TrackService) {
|
||||
if (item != null) {
|
||||
item.manga_id = manga.id!!
|
||||
add(service.bind(item)
|
||||
.flatMap { db.insertTrack(item).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ },
|
||||
{ error -> context.toast(error.message) }))
|
||||
} else {
|
||||
db.deleteTrackForManga(manga, service).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRemote(track: Track, service: TrackService) {
|
||||
service.update(track)
|
||||
.flatMap { db.insertTrack(track).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ -> view.onRefreshDone() },
|
||||
{ view, error ->
|
||||
view.onRefreshError(error)
|
||||
|
||||
// Restart on error to set old values
|
||||
fetchTrackings()
|
||||
})
|
||||
}
|
||||
|
||||
fun setStatus(item: TrackItem, index: Int) {
|
||||
val track = item.track!!
|
||||
track.status = item.service.getStatusList()[index]
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
fun setScore(item: TrackItem, index: Int) {
|
||||
val track = item.track!!
|
||||
track.score = item.service.indexToScore(index)
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
|
||||
val track = item.track!!
|
||||
track.last_chapter_read = chapterNumber
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.os.Bundle
|
||||
import eu.kanade.tachiyomi.data.database.DatabaseHelper
|
||||
import eu.kanade.tachiyomi.data.database.models.Manga
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.preference.PreferencesHelper
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
|
||||
import eu.kanade.tachiyomi.util.toast
|
||||
import rx.Observable
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.schedulers.Schedulers
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
|
||||
|
||||
class TrackPresenter(
|
||||
val manga: Manga,
|
||||
preferences: PreferencesHelper = Injekt.get(),
|
||||
private val db: DatabaseHelper = Injekt.get(),
|
||||
private val trackManager: TrackManager = Injekt.get()
|
||||
) : BasePresenter<TrackController>() {
|
||||
|
||||
private val context = preferences.context
|
||||
|
||||
private var trackList: List<TrackItem> = emptyList()
|
||||
|
||||
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
|
||||
|
||||
private var trackSubscription: Subscription? = null
|
||||
|
||||
private var searchSubscription: Subscription? = null
|
||||
|
||||
private var refreshSubscription: Subscription? = null
|
||||
|
||||
override fun onCreate(savedState: Bundle?) {
|
||||
super.onCreate(savedState)
|
||||
fetchTrackings()
|
||||
}
|
||||
|
||||
fun fetchTrackings() {
|
||||
trackSubscription?.let { remove(it) }
|
||||
trackSubscription = db.getTracks(manga)
|
||||
.asRxObservable()
|
||||
.map { tracks ->
|
||||
loggedServices.map { service ->
|
||||
TrackItem(tracks.find { it.sync_id == service.id }, service)
|
||||
}
|
||||
}
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.doOnNext { trackList = it }
|
||||
.subscribeLatestCache(TrackController::onNextTrackings)
|
||||
}
|
||||
|
||||
fun refresh() {
|
||||
refreshSubscription?.let { remove(it) }
|
||||
refreshSubscription = Observable.from(trackList)
|
||||
.filter { it.track != null }
|
||||
.concatMap { item ->
|
||||
item.service.refresh(item.track!!)
|
||||
.flatMap { db.insertTrack(it).asRxObservable() }
|
||||
.map { item }
|
||||
.onErrorReturn { item }
|
||||
}
|
||||
.toList()
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ -> view.onRefreshDone() },
|
||||
TrackController::onRefreshError)
|
||||
}
|
||||
|
||||
fun search(query: String, service: TrackService) {
|
||||
searchSubscription?.let { remove(it) }
|
||||
searchSubscription = service.search(query)
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeLatestCache(TrackController::onSearchResults,
|
||||
TrackController::onSearchResultsError)
|
||||
}
|
||||
|
||||
fun registerTracking(item: Track?, service: TrackService) {
|
||||
if (item != null) {
|
||||
item.manga_id = manga.id!!
|
||||
add(service.bind(item)
|
||||
.flatMap { db.insertTrack(item).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ },
|
||||
{ error -> context.toast(error.message) }))
|
||||
} else {
|
||||
db.deleteTrackForManga(manga, service).executeAsBlocking()
|
||||
}
|
||||
}
|
||||
|
||||
private fun updateRemote(track: Track, service: TrackService) {
|
||||
service.update(track)
|
||||
.flatMap { db.insertTrack(track).asRxObservable() }
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribeFirst({ view, _ -> view.onRefreshDone() },
|
||||
{ view, error ->
|
||||
view.onRefreshError(error)
|
||||
|
||||
// Restart on error to set old values
|
||||
fetchTrackings()
|
||||
})
|
||||
}
|
||||
|
||||
fun setStatus(item: TrackItem, index: Int) {
|
||||
val track = item.track!!
|
||||
track.status = item.service.getStatusList()[index]
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
fun setScore(item: TrackItem, index: Int) {
|
||||
val track = item.track!!
|
||||
track.score = item.service.indexToScore(index)
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
fun setLastChapterRead(item: TrackItem, chapterNumber: Int) {
|
||||
val track = item.track!!
|
||||
track.last_chapter_read = chapterNumber
|
||||
updateRemote(track, item.service)
|
||||
}
|
||||
|
||||
}
|
||||
@@ -1,79 +1,79 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.util.gone
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import kotlinx.android.synthetic.main.track_search_item.view.*
|
||||
import java.util.*
|
||||
|
||||
class TrackSearchAdapter(context: Context)
|
||||
: ArrayAdapter<TrackSearch>(context, R.layout.track_search_item, ArrayList<TrackSearch>()) {
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
var v = view
|
||||
// Get the data item for this position
|
||||
val track = getItem(position)
|
||||
// Check if an existing view is being reused, otherwise inflate the view
|
||||
val holder: TrackSearchHolder // view lookup cache stored in tag
|
||||
if (v == null) {
|
||||
v = parent.inflate(R.layout.track_search_item)
|
||||
holder = TrackSearchHolder(v)
|
||||
v.tag = holder
|
||||
} else {
|
||||
holder = v.tag as TrackSearchHolder
|
||||
}
|
||||
holder.onSetValues(track)
|
||||
return v
|
||||
}
|
||||
|
||||
fun setItems(syncs: List<TrackSearch>) {
|
||||
setNotifyOnChange(false)
|
||||
clear()
|
||||
addAll(syncs)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class TrackSearchHolder(private val view: View) {
|
||||
|
||||
fun onSetValues(track: TrackSearch) {
|
||||
view.track_search_title.text = track.title
|
||||
view.track_search_summary.text = track.summary
|
||||
GlideApp.with(view.context).clear(view.track_search_cover)
|
||||
if (!track.cover_url.isNullOrEmpty()) {
|
||||
GlideApp.with(view.context)
|
||||
.load(track.cover_url)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.into(view.track_search_cover)
|
||||
}
|
||||
|
||||
if (track.publishing_status.isNullOrBlank()) {
|
||||
view.track_search_status.gone()
|
||||
view.track_search_status_result.gone()
|
||||
} else {
|
||||
view.track_search_status_result.text = track.publishing_status.capitalize()
|
||||
}
|
||||
|
||||
if (track.publishing_type.isNullOrBlank()) {
|
||||
view.track_search_type.gone()
|
||||
view.track_search_type_result.gone()
|
||||
} else {
|
||||
view.track_search_type_result.text = track.publishing_type.capitalize()
|
||||
}
|
||||
|
||||
if (track.start_date.isNullOrBlank()) {
|
||||
view.track_search_start.gone()
|
||||
view.track_search_start_result.gone()
|
||||
} else {
|
||||
view.track_search_start_result.text = track.start_date
|
||||
}
|
||||
}
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.content.Context
|
||||
import android.view.View
|
||||
import android.view.ViewGroup
|
||||
import android.widget.ArrayAdapter
|
||||
import com.bumptech.glide.load.engine.DiskCacheStrategy
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.glide.GlideApp
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.util.gone
|
||||
import eu.kanade.tachiyomi.util.inflate
|
||||
import kotlinx.android.synthetic.main.track_search_item.view.*
|
||||
import java.util.*
|
||||
|
||||
class TrackSearchAdapter(context: Context)
|
||||
: ArrayAdapter<TrackSearch>(context, R.layout.track_search_item, ArrayList<TrackSearch>()) {
|
||||
|
||||
override fun getView(position: Int, view: View?, parent: ViewGroup): View {
|
||||
var v = view
|
||||
// Get the data item for this position
|
||||
val track = getItem(position)
|
||||
// Check if an existing view is being reused, otherwise inflate the view
|
||||
val holder: TrackSearchHolder // view lookup cache stored in tag
|
||||
if (v == null) {
|
||||
v = parent.inflate(R.layout.track_search_item)
|
||||
holder = TrackSearchHolder(v)
|
||||
v.tag = holder
|
||||
} else {
|
||||
holder = v.tag as TrackSearchHolder
|
||||
}
|
||||
holder.onSetValues(track)
|
||||
return v
|
||||
}
|
||||
|
||||
fun setItems(syncs: List<TrackSearch>) {
|
||||
setNotifyOnChange(false)
|
||||
clear()
|
||||
addAll(syncs)
|
||||
notifyDataSetChanged()
|
||||
}
|
||||
|
||||
class TrackSearchHolder(private val view: View) {
|
||||
|
||||
fun onSetValues(track: TrackSearch) {
|
||||
view.track_search_title.text = track.title
|
||||
view.track_search_summary.text = track.summary
|
||||
GlideApp.with(view.context).clear(view.track_search_cover)
|
||||
if (!track.cover_url.isNullOrEmpty()) {
|
||||
GlideApp.with(view.context)
|
||||
.load(track.cover_url)
|
||||
.diskCacheStrategy(DiskCacheStrategy.RESOURCE)
|
||||
.centerCrop()
|
||||
.into(view.track_search_cover)
|
||||
}
|
||||
|
||||
if (track.publishing_status.isNullOrBlank()) {
|
||||
view.track_search_status.gone()
|
||||
view.track_search_status_result.gone()
|
||||
} else {
|
||||
view.track_search_status_result.text = track.publishing_status.capitalize()
|
||||
}
|
||||
|
||||
if (track.publishing_type.isNullOrBlank()) {
|
||||
view.track_search_type.gone()
|
||||
view.track_search_type_result.gone()
|
||||
} else {
|
||||
view.track_search_type_result.text = track.publishing_type.capitalize()
|
||||
}
|
||||
|
||||
if (track.start_date.isNullOrBlank()) {
|
||||
view.track_search_start.gone()
|
||||
view.track_search_start_result.gone()
|
||||
} else {
|
||||
view.track_search_start_result.text = track.start_date
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,144 +1,144 @@
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.jakewharton.rxbinding.widget.itemClicks
|
||||
import com.jakewharton.rxbinding.widget.textChanges
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import kotlinx.android.synthetic.main.track_search_dialog.view.*
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class TrackSearchDialog : DialogController {
|
||||
|
||||
private var dialogView: View? = null
|
||||
|
||||
private var adapter: TrackSearchAdapter? = null
|
||||
|
||||
private var selectedItem: Track? = null
|
||||
|
||||
private val service: TrackService
|
||||
|
||||
private var subscriptions = CompositeSubscription()
|
||||
|
||||
private var searchTextSubscription: Subscription? = null
|
||||
|
||||
private val trackController
|
||||
get() = targetController as TrackController
|
||||
|
||||
constructor(target: TrackController, service: TrackService) : super(Bundle().apply {
|
||||
putInt(KEY_SERVICE, service.id)
|
||||
}) {
|
||||
targetController = target
|
||||
this.service = service
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : super(bundle) {
|
||||
service = Injekt.get<TrackManager>().getService(bundle.getInt(KEY_SERVICE))!!
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
||||
val dialog = MaterialDialog.Builder(activity!!)
|
||||
.customView(R.layout.track_search_dialog, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { _, _ -> onPositiveButtonClick() }
|
||||
.build()
|
||||
|
||||
if (subscriptions.isUnsubscribed) {
|
||||
subscriptions = CompositeSubscription()
|
||||
}
|
||||
|
||||
dialogView = dialog.view
|
||||
onViewCreated(dialog.view, savedState)
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
// Create adapter
|
||||
val adapter = TrackSearchAdapter(view.context)
|
||||
this.adapter = adapter
|
||||
view.track_search_list.adapter = adapter
|
||||
|
||||
// Set listeners
|
||||
selectedItem = null
|
||||
|
||||
subscriptions += view.track_search_list.itemClicks().subscribe { position ->
|
||||
selectedItem = adapter.getItem(position)
|
||||
}
|
||||
|
||||
// Do an initial search based on the manga's title
|
||||
if (savedState == null) {
|
||||
val title = trackController.presenter.manga.title
|
||||
view.track_search.append(title)
|
||||
search(title)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
subscriptions.unsubscribe()
|
||||
dialogView = null
|
||||
adapter = null
|
||||
}
|
||||
|
||||
override fun onAttach(view: View) {
|
||||
super.onAttach(view)
|
||||
searchTextSubscription = dialogView!!.track_search.textChanges()
|
||||
.skip(1)
|
||||
.debounce(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
|
||||
.map { it.toString() }
|
||||
.filter(String::isNotBlank)
|
||||
.subscribe { search(it) }
|
||||
}
|
||||
|
||||
override fun onDetach(view: View) {
|
||||
super.onDetach(view)
|
||||
searchTextSubscription?.unsubscribe()
|
||||
}
|
||||
|
||||
private fun search(query: String) {
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.VISIBLE
|
||||
view.track_search_list.visibility = View.INVISIBLE
|
||||
trackController.presenter.search(query, service)
|
||||
}
|
||||
|
||||
fun onSearchResults(results: List<TrackSearch>) {
|
||||
selectedItem = null
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.INVISIBLE
|
||||
view.track_search_list.visibility = View.VISIBLE
|
||||
adapter?.setItems(results)
|
||||
}
|
||||
|
||||
fun onSearchResultsError() {
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.VISIBLE
|
||||
view.track_search_list.visibility = View.INVISIBLE
|
||||
adapter?.setItems(emptyList())
|
||||
}
|
||||
|
||||
private fun onPositiveButtonClick() {
|
||||
trackController.presenter.registerTracking(selectedItem, service)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY_SERVICE = "service_id"
|
||||
}
|
||||
|
||||
}
|
||||
package eu.kanade.tachiyomi.ui.manga.track
|
||||
|
||||
import android.app.Dialog
|
||||
import android.os.Bundle
|
||||
import android.view.View
|
||||
import com.afollestad.materialdialogs.MaterialDialog
|
||||
import com.jakewharton.rxbinding.widget.itemClicks
|
||||
import com.jakewharton.rxbinding.widget.textChanges
|
||||
import eu.kanade.tachiyomi.R
|
||||
import eu.kanade.tachiyomi.data.database.models.Track
|
||||
import eu.kanade.tachiyomi.data.track.TrackManager
|
||||
import eu.kanade.tachiyomi.data.track.TrackService
|
||||
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||
import eu.kanade.tachiyomi.ui.base.controller.DialogController
|
||||
import eu.kanade.tachiyomi.util.plusAssign
|
||||
import kotlinx.android.synthetic.main.track_search_dialog.view.*
|
||||
import rx.Subscription
|
||||
import rx.android.schedulers.AndroidSchedulers
|
||||
import rx.subscriptions.CompositeSubscription
|
||||
import uy.kohesive.injekt.Injekt
|
||||
import uy.kohesive.injekt.api.get
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class TrackSearchDialog : DialogController {
|
||||
|
||||
private var dialogView: View? = null
|
||||
|
||||
private var adapter: TrackSearchAdapter? = null
|
||||
|
||||
private var selectedItem: Track? = null
|
||||
|
||||
private val service: TrackService
|
||||
|
||||
private var subscriptions = CompositeSubscription()
|
||||
|
||||
private var searchTextSubscription: Subscription? = null
|
||||
|
||||
private val trackController
|
||||
get() = targetController as TrackController
|
||||
|
||||
constructor(target: TrackController, service: TrackService) : super(Bundle().apply {
|
||||
putInt(KEY_SERVICE, service.id)
|
||||
}) {
|
||||
targetController = target
|
||||
this.service = service
|
||||
}
|
||||
|
||||
@Suppress("unused")
|
||||
constructor(bundle: Bundle) : super(bundle) {
|
||||
service = Injekt.get<TrackManager>().getService(bundle.getInt(KEY_SERVICE))!!
|
||||
}
|
||||
|
||||
override fun onCreateDialog(savedState: Bundle?): Dialog {
|
||||
val dialog = MaterialDialog.Builder(activity!!)
|
||||
.customView(R.layout.track_search_dialog, false)
|
||||
.positiveText(android.R.string.ok)
|
||||
.negativeText(android.R.string.cancel)
|
||||
.onPositive { _, _ -> onPositiveButtonClick() }
|
||||
.build()
|
||||
|
||||
if (subscriptions.isUnsubscribed) {
|
||||
subscriptions = CompositeSubscription()
|
||||
}
|
||||
|
||||
dialogView = dialog.view
|
||||
onViewCreated(dialog.view, savedState)
|
||||
|
||||
return dialog
|
||||
}
|
||||
|
||||
fun onViewCreated(view: View, savedState: Bundle?) {
|
||||
// Create adapter
|
||||
val adapter = TrackSearchAdapter(view.context)
|
||||
this.adapter = adapter
|
||||
view.track_search_list.adapter = adapter
|
||||
|
||||
// Set listeners
|
||||
selectedItem = null
|
||||
|
||||
subscriptions += view.track_search_list.itemClicks().subscribe { position ->
|
||||
selectedItem = adapter.getItem(position)
|
||||
}
|
||||
|
||||
// Do an initial search based on the manga's title
|
||||
if (savedState == null) {
|
||||
val title = trackController.presenter.manga.title
|
||||
view.track_search.append(title)
|
||||
search(title)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDestroyView(view: View) {
|
||||
super.onDestroyView(view)
|
||||
subscriptions.unsubscribe()
|
||||
dialogView = null
|
||||
adapter = null
|
||||
}
|
||||
|
||||
override fun onAttach(view: View) {
|
||||
super.onAttach(view)
|
||||
searchTextSubscription = dialogView!!.track_search.textChanges()
|
||||
.skip(1)
|
||||
.debounce(1, TimeUnit.SECONDS, AndroidSchedulers.mainThread())
|
||||
.map { it.toString() }
|
||||
.filter(String::isNotBlank)
|
||||
.subscribe { search(it) }
|
||||
}
|
||||
|
||||
override fun onDetach(view: View) {
|
||||
super.onDetach(view)
|
||||
searchTextSubscription?.unsubscribe()
|
||||
}
|
||||
|
||||
private fun search(query: String) {
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.VISIBLE
|
||||
view.track_search_list.visibility = View.INVISIBLE
|
||||
trackController.presenter.search(query, service)
|
||||
}
|
||||
|
||||
fun onSearchResults(results: List<TrackSearch>) {
|
||||
selectedItem = null
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.INVISIBLE
|
||||
view.track_search_list.visibility = View.VISIBLE
|
||||
adapter?.setItems(results)
|
||||
}
|
||||
|
||||
fun onSearchResultsError() {
|
||||
val view = dialogView ?: return
|
||||
view.progress.visibility = View.VISIBLE
|
||||
view.track_search_list.visibility = View.INVISIBLE
|
||||
adapter?.setItems(emptyList())
|
||||
}
|
||||
|
||||
private fun onPositiveButtonClick() {
|
||||
trackController.presenter.registerTracking(selectedItem, service)
|
||||
}
|
||||
|
||||
private companion object {
|
||||
const val KEY_SERVICE = "service_id"
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user