Move tracking to a bottom sheet (#4364)

* Move tracking to a bottom sheet

* Give methods better names and remove unnecessary annotation
This commit is contained in:
Andreas 2021-01-31 20:43:43 +01:00 committed by GitHub
parent c34b548a3e
commit 535abcbb8b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 357 additions and 396 deletions

View File

@ -37,6 +37,7 @@ import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.download.DownloadService import eu.kanade.tachiyomi.data.download.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.databinding.MangaControllerBinding import eu.kanade.tachiyomi.databinding.MangaControllerBinding
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
@ -62,7 +63,9 @@ import eu.kanade.tachiyomi.ui.manga.chapter.DownloadCustomChaptersDialog
import eu.kanade.tachiyomi.ui.manga.chapter.MangaChaptersHeaderAdapter import eu.kanade.tachiyomi.ui.manga.chapter.MangaChaptersHeaderAdapter
import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter import eu.kanade.tachiyomi.ui.manga.chapter.base.BaseChaptersAdapter
import eu.kanade.tachiyomi.ui.manga.info.MangaInfoHeaderAdapter import eu.kanade.tachiyomi.ui.manga.info.MangaInfoHeaderAdapter
import eu.kanade.tachiyomi.ui.manga.track.TrackController import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.ui.manga.track.TrackSearchDialog
import eu.kanade.tachiyomi.ui.manga.track.TrackSheet
import eu.kanade.tachiyomi.ui.reader.ReaderActivity import eu.kanade.tachiyomi.ui.reader.ReaderActivity
import eu.kanade.tachiyomi.ui.recent.history.HistoryController import eu.kanade.tachiyomi.ui.recent.history.HistoryController
import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
@ -160,6 +163,8 @@ class MangaController :
private var isRefreshingInfo = false private var isRefreshingInfo = false
private var isRefreshingChapters = false private var isRefreshingChapters = false
private var trackSheet: TrackSheet? = null
init { init {
setHasOptionsMenu(true) setHasOptionsMenu(true)
} }
@ -246,6 +251,8 @@ class MangaController :
} }
} }
trackSheet = TrackSheet(this, manga!!)
updateFilterIconState() updateFilterIconState()
} }
@ -461,7 +468,7 @@ class MangaController :
} }
fun onTrackingClick() { fun onTrackingClick() {
router.pushController(TrackController(manga).withFadeTransaction()) trackSheet?.show()
} }
private fun addToLibrary(manga: Manga) { private fun addToLibrary(manga: Manga) {
@ -1030,6 +1037,35 @@ class MangaController :
// Chapters list - end // Chapters list - end
// Tracker sheet - start
fun onNextTrackers(trackers: List<TrackItem>) {
trackSheet?.onNextTrackers(trackers)
}
fun onTrackingRefreshDone() {
}
fun onTrackingRefreshError(error: Throwable) {
Timber.e(error)
activity?.toast(error.message)
}
fun onTrackingSearchResults(results: List<TrackSearch>) {
getTrackingSearchDialog()?.onSearchResults(results)
}
fun onTrackingSearchResultsError(error: Throwable) {
Timber.e(error)
activity?.toast(error.message)
getTrackingSearchDialog()?.onSearchResultsError()
}
private fun getTrackingSearchDialog(): TrackSearchDialog? {
return trackSheet?.getSearchDialog()
}
// Tracker sheet - end
companion object { companion object {
const val FROM_SOURCE_EXTRA = "from_source" const val FROM_SOURCE_EXTRA = "from_source"
const val MANGA_EXTRA = "manga" const val MANGA_EXTRA = "manga"

View File

@ -10,17 +10,20 @@ import eu.kanade.tachiyomi.data.database.models.Category
import eu.kanade.tachiyomi.data.database.models.Chapter import eu.kanade.tachiyomi.data.database.models.Chapter
import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.MangaCategory
import eu.kanade.tachiyomi.data.database.models.Track
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.download.DownloadManager import eu.kanade.tachiyomi.data.download.DownloadManager
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.toSChapter import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
import eu.kanade.tachiyomi.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.isLocal
@ -29,9 +32,13 @@ import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.prepUpdateCover
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.util.updateCoverLastModified import eu.kanade.tachiyomi.util.updateCoverLastModified
import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import rx.Observable import rx.Observable
import rx.Subscription import rx.Subscription
import rx.android.schedulers.AndroidSchedulers import rx.android.schedulers.AndroidSchedulers
@ -86,6 +93,15 @@ class MangaPresenter(
private var observeDownloadsStatusSubscription: Subscription? = null private var observeDownloadsStatusSubscription: Subscription? = null
private var observeDownloadsPageSubscription: Subscription? = null private var observeDownloadsPageSubscription: Subscription? = null
private var _trackList: List<TrackItem> = emptyList()
val trackList get() = _trackList
private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
private var trackSubscription: Subscription? = null
private var searchJob: Job? = null
private var refreshJob: Job? = null
override fun onCreate(savedState: Bundle?) { override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState) super.onCreate(savedState)
@ -134,6 +150,8 @@ class MangaPresenter(
) )
// Chapters list - end // Chapters list - end
fetchTrackers()
} }
// Manga info - start // Manga info - start
@ -645,4 +663,128 @@ class MangaPresenter(
} }
// Chapters list - end // Chapters list - end
// Track sheet - start
private fun fetchTrackers() {
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(MangaController::onNextTrackers)
}
fun trackingRefresh() {
refreshJob?.cancel()
refreshJob = launchIO {
supervisorScope {
try {
trackList
.filter { it.track != null }
.map {
async {
val track = it.service.refresh(it.track!!)
db.insertTrack(track).executeAsBlocking()
}
}
.awaitAll()
withUIContext { view?.onTrackingRefreshDone() }
} catch (e: Throwable) {
withUIContext { view?.onTrackingRefreshError(e) }
}
}
}
}
fun trackingSearch(query: String, service: TrackService) {
searchJob?.cancel()
searchJob = launchIO {
try {
val results = service.search(query)
withUIContext { view?.onTrackingSearchResults(results) }
} catch (e: Throwable) {
withUIContext { view?.onTrackingSearchResultsError(e) }
}
}
}
fun registerTracking(item: Track?, service: TrackService) {
if (item != null) {
item.manga_id = manga.id!!
launchIO {
try {
service.bind(item)
db.insertTrack(item).executeAsBlocking()
} catch (e: Throwable) {
withUIContext { view?.applicationContext?.toast(e.message) }
}
}
} else {
unregisterTracking(service)
}
}
fun unregisterTracking(service: TrackService) {
db.deleteTrackForManga(manga, service).executeAsBlocking()
}
private fun updateRemote(track: Track, service: TrackService) {
launchIO {
try {
service.update(track)
db.insertTrack(track).executeAsBlocking()
withUIContext { view?.onTrackingRefreshDone() }
} catch (e: Throwable) {
withUIContext { view?.onTrackingRefreshError(e) }
// Restart on error to set old values
fetchTrackers()
}
}
}
fun setTrackerStatus(item: TrackItem, index: Int) {
val track = item.track!!
track.status = item.service.getStatusList()[index]
if (track.status == item.service.getCompletionStatus() && track.total_chapters != 0) {
track.last_chapter_read = track.total_chapters
}
updateRemote(track, item.service)
}
fun setTrackerScore(item: TrackItem, index: Int) {
val track = item.track!!
track.score = item.service.indexToScore(index)
updateRemote(track, item.service)
}
fun setTrackerLastChapterRead(item: TrackItem, chapterNumber: Int) {
val track = item.track!!
track.last_chapter_read = chapterNumber
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = item.service.getCompletionStatus()
}
updateRemote(track, item.service)
}
fun setTrackerStartDate(item: TrackItem, date: Long) {
val track = item.track!!
track.started_reading_date = date
updateRemote(track, item.service)
}
fun setTrackerFinishDate(item: TrackItem, date: Long) {
val track = item.track!!
track.finished_reading_date = date
updateRemote(track, item.service)
}
// Track sheet - end
} }

View File

@ -16,14 +16,17 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class SetTrackChaptersDialog<T> : DialogController class SetTrackChaptersDialog<T> : DialogController
where T : Controller, T : SetTrackChaptersDialog.Listener { where T : Controller {
private val item: TrackItem private val item: TrackItem
constructor(target: T, item: TrackItem) : super( private lateinit var listener: Listener
constructor(target: T, listener: Listener, item: TrackItem) : super(
bundleOf(KEY_ITEM_TRACK to item.track) bundleOf(KEY_ITEM_TRACK to item.track)
) { ) {
targetController = target targetController = target
this.listener = listener
this.item = item this.item = item
} }
@ -46,7 +49,7 @@ class SetTrackChaptersDialog<T> : DialogController
val np: NumberPicker = view.findViewById(R.id.chapters_picker) val np: NumberPicker = view.findViewById(R.id.chapters_picker)
np.clearFocus() np.clearFocus()
(targetController as? Listener)?.setChaptersRead(item, np.value) listener.setChaptersRead(item, np.value)
} }
.negativeButton(android.R.string.cancel) .negativeButton(android.R.string.cancel)

View File

@ -15,16 +15,19 @@ import uy.kohesive.injekt.api.get
import java.util.Calendar import java.util.Calendar
class SetTrackReadingDatesDialog<T> : DialogController class SetTrackReadingDatesDialog<T> : DialogController
where T : Controller, T : SetTrackReadingDatesDialog.Listener { where T : Controller {
private val item: TrackItem private val item: TrackItem
private val dateToUpdate: ReadingDate private val dateToUpdate: ReadingDate
constructor(target: T, dateToUpdate: ReadingDate, item: TrackItem) : super( private lateinit var listener: Listener
constructor(target: T, listener: Listener, dateToUpdate: ReadingDate, item: TrackItem) : super(
bundleOf(KEY_ITEM_TRACK to item.track) bundleOf(KEY_ITEM_TRACK to item.track)
) { ) {
targetController = target targetController = target
this.listener = listener
this.item = item this.item = item
this.dateToUpdate = dateToUpdate this.dateToUpdate = dateToUpdate
} }
@ -38,8 +41,6 @@ class SetTrackReadingDatesDialog<T> : DialogController
} }
override fun onCreateDialog(savedViewState: Bundle?): Dialog { override fun onCreateDialog(savedViewState: Bundle?): Dialog {
val listener = (targetController as? Listener)
return MaterialDialog(activity!!) return MaterialDialog(activity!!)
.title( .title(
when (dateToUpdate) { when (dateToUpdate) {

View File

@ -16,14 +16,17 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class SetTrackScoreDialog<T> : DialogController class SetTrackScoreDialog<T> : DialogController
where T : Controller, T : SetTrackScoreDialog.Listener { where T : Controller {
private val item: TrackItem private val item: TrackItem
constructor(target: T, item: TrackItem) : super( private lateinit var listener: Listener
constructor(target: T, listener: Listener, item: TrackItem) : super(
bundleOf(KEY_ITEM_TRACK to item.track) bundleOf(KEY_ITEM_TRACK to item.track)
) { ) {
targetController = target targetController = target
this.listener = listener
this.item = item this.item = item
} }
@ -46,7 +49,7 @@ class SetTrackScoreDialog<T> : DialogController
val np: NumberPicker = view.findViewById(R.id.score_picker) val np: NumberPicker = view.findViewById(R.id.score_picker)
np.clearFocus() np.clearFocus()
(targetController as? Listener)?.setScore(item, np.value) listener.setScore(item, np.value)
} }
.negativeButton(android.R.string.cancel) .negativeButton(android.R.string.cancel)

View File

@ -14,14 +14,17 @@ import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get import uy.kohesive.injekt.api.get
class SetTrackStatusDialog<T> : DialogController class SetTrackStatusDialog<T> : DialogController
where T : Controller, T : SetTrackStatusDialog.Listener { where T : Controller {
private val item: TrackItem private val item: TrackItem
constructor(target: T, item: TrackItem) : super( private lateinit var listener: Listener
constructor(target: T, listener: Listener, item: TrackItem) : super(
bundleOf(KEY_ITEM_TRACK to item.track) bundleOf(KEY_ITEM_TRACK to item.track)
) { ) {
targetController = target targetController = target
this.listener = listener
this.item = item this.item = item
} }
@ -46,7 +49,7 @@ class SetTrackStatusDialog<T> : DialogController
initialSelection = selectedIndex, initialSelection = selectedIndex,
waitForPositiveButton = false waitForPositiveButton = false
) { dialog, position, _ -> ) { dialog, position, _ ->
(targetController as? Listener)?.setStatus(item, position) listener.setStatus(item, position)
dialog.dismiss() dialog.dismiss()
} }
} }

View File

@ -5,7 +5,7 @@ import android.view.ViewGroup
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.databinding.TrackItemBinding import eu.kanade.tachiyomi.databinding.TrackItemBinding
class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHolder>() { class TrackAdapter(listener: OnClickListener) : RecyclerView.Adapter<TrackHolder>() {
private lateinit var binding: TrackItemBinding private lateinit var binding: TrackItemBinding
@ -17,7 +17,7 @@ class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHold
} }
} }
val rowClickListener: OnClickListener = controller val rowClickListener: OnClickListener = listener
fun getItem(index: Int): TrackItem? { fun getItem(index: Int): TrackItem? {
return items.getOrNull(index) return items.getOrNull(index)

View File

@ -1,200 +0,0 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.content.Intent
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.core.os.bundleOf
import androidx.recyclerview.widget.LinearLayoutManager
import eu.kanade.tachiyomi.data.database.DatabaseHelper
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
import eu.kanade.tachiyomi.ui.base.controller.NucleusController
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import reactivecircus.flowbinding.swiperefreshlayout.refreshes
import timber.log.Timber
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class TrackController :
NucleusController<TrackControllerBinding, TrackPresenter>,
TrackAdapter.OnClickListener,
SetTrackStatusDialog.Listener,
SetTrackChaptersDialog.Listener,
SetTrackScoreDialog.Listener,
SetTrackReadingDatesDialog.Listener {
constructor(manga: Manga?) : super(
bundleOf(MANGA_EXTRA to (manga?.id ?: 0))
) {
this.manga = manga
}
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
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 getTitle(): String? {
return manga?.title
}
override fun createPresenter(): TrackPresenter {
return TrackPresenter(manga!!)
}
override fun inflateView(inflater: LayoutInflater, container: ViewGroup): View {
binding = TrackControllerBinding.inflate(inflater)
return binding.root
}
override fun onViewCreated(view: View) {
super.onViewCreated(view)
if (manga == null) return
adapter = TrackAdapter(this)
binding.trackRecycler.layoutManager = LinearLayoutManager(view.context)
binding.trackRecycler.adapter = adapter
binding.swipeRefresh.isEnabled = false
binding.swipeRefresh.refreshes()
.onEach { presenter.refresh() }
.launchIn(viewScope)
}
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
binding.swipeRefresh.isEnabled = atLeastOneLink
}
fun onSearchResults(results: List<TrackSearch>) {
getSearchDialog()?.onSearchResults(results)
}
@Suppress("UNUSED_PARAMETER")
fun onSearchResultsError(error: Throwable) {
Timber.e(error)
activity?.toast(error.message)
getSearchDialog()?.onSearchResultsError()
}
private fun getSearchDialog(): TrackSearchDialog? {
return router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog
}
fun onRefreshDone() {
binding.swipeRefresh.isRefreshing = false
}
fun onRefreshError(error: Throwable) {
binding.swipeRefresh.isRefreshing = false
activity?.toast(error.message)
}
override fun onLogoClick(position: Int) {
val track = adapter?.getItem(position)?.track ?: return
if (track.tracking_url.isNotBlank()) {
activity?.startActivity(Intent(Intent.ACTION_VIEW, track.tracking_url.toUri()))
}
}
override fun onSetClick(position: Int) {
val item = adapter?.getItem(position) ?: return
TrackSearchDialog(this, item.service).showDialog(router, TAG_SEARCH_CONTROLLER)
}
override fun onTitleLongClick(position: Int) {
adapter?.getItem(position)?.track?.title?.let {
activity?.copyToClipboard(it, it)
}
}
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 onStartDateClick(position: Int) {
val item = adapter?.getItem(position) ?: return
if (item.track == null) return
SetTrackReadingDatesDialog(this, SetTrackReadingDatesDialog.ReadingDate.Start, item).showDialog(router)
}
override fun onFinishDateClick(position: Int) {
val item = adapter?.getItem(position) ?: return
if (item.track == null) return
SetTrackReadingDatesDialog(this, SetTrackReadingDatesDialog.ReadingDate.Finish, item).showDialog(router)
}
override fun setStatus(item: TrackItem, selection: Int) {
presenter.setStatus(item, selection)
binding.swipeRefresh.isRefreshing = true
}
override fun setScore(item: TrackItem, score: Int) {
presenter.setScore(item, score)
binding.swipeRefresh.isRefreshing = true
}
override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
presenter.setLastChapterRead(item, chaptersRead)
binding.swipeRefresh.isRefreshing = true
}
override fun setReadingDate(item: TrackItem, type: SetTrackReadingDatesDialog.ReadingDate, date: Long) {
when (type) {
SetTrackReadingDatesDialog.ReadingDate.Start -> presenter.setStartDate(item, date)
SetTrackReadingDatesDialog.ReadingDate.Finish -> presenter.setFinishDate(item, date)
}
binding.swipeRefresh.isRefreshing = true
}
private companion object {
const val MANGA_EXTRA = "manga"
const val TAG_SEARCH_CONTROLLER = "track_search_controller"
}
}

View File

@ -1,164 +0,0 @@
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.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.system.toast
import kotlinx.coroutines.Job
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.supervisorScope
import rx.Subscription
import rx.android.schedulers.AndroidSchedulers
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 searchJob: Job? = null
private var refreshJob: Job? = null
override fun onCreate(savedState: Bundle?) {
super.onCreate(savedState)
fetchTrackings()
}
private 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() {
refreshJob?.cancel()
refreshJob = launchIO {
supervisorScope {
try {
trackList
.filter { it.track != null }
.map {
async {
val track = it.service.refresh(it.track!!)
db.insertTrack(track).executeAsBlocking()
}
}
.awaitAll()
withUIContext { view?.onRefreshDone() }
} catch (e: Throwable) {
withUIContext { view?.onRefreshError(e) }
}
}
}
}
fun search(query: String, service: TrackService) {
searchJob?.cancel()
searchJob = launchIO {
try {
val results = service.search(query)
withUIContext { view?.onSearchResults(results) }
} catch (e: Throwable) {
withUIContext { view?.onSearchResultsError(e) }
}
}
}
fun registerTracking(item: Track?, service: TrackService) {
if (item != null) {
item.manga_id = manga.id!!
launchIO {
try {
service.bind(item)
db.insertTrack(item).executeAsBlocking()
} catch (e: Throwable) {
withUIContext { context.toast(e.message) }
}
}
} else {
unregisterTracking(service)
}
}
fun unregisterTracking(service: TrackService) {
db.deleteTrackForManga(manga, service).executeAsBlocking()
}
private fun updateRemote(track: Track, service: TrackService) {
launchIO {
try {
service.update(track)
db.insertTrack(track).executeAsBlocking()
withUIContext { view?.onRefreshDone() }
} catch (e: Throwable) {
withUIContext { view?.onRefreshError(e) }
// Restart on error to set old values
fetchTrackings()
}
}
}
fun setStatus(item: TrackItem, index: Int) {
val track = item.track!!
track.status = item.service.getStatusList()[index]
if (track.status == item.service.getCompletionStatus() && track.total_chapters != 0) {
track.last_chapter_read = track.total_chapters
}
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
if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
track.status = item.service.getCompletionStatus()
}
updateRemote(track, item.service)
}
fun setStartDate(item: TrackItem, date: Long) {
val track = item.track!!
track.started_reading_date = date
updateRemote(track, item.service)
}
fun setFinishDate(item: TrackItem, date: Long) {
val track = item.track!!
track.finished_reading_date = date
updateRemote(track, item.service)
}
}

View File

@ -15,6 +15,7 @@ import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.data.track.model.TrackSearch
import eu.kanade.tachiyomi.databinding.TrackSearchDialogBinding import eu.kanade.tachiyomi.databinding.TrackSearchDialogBinding
import eu.kanade.tachiyomi.ui.base.controller.DialogController import eu.kanade.tachiyomi.ui.base.controller.DialogController
import eu.kanade.tachiyomi.ui.manga.MangaController
import kotlinx.coroutines.flow.debounce import kotlinx.coroutines.flow.debounce
import kotlinx.coroutines.flow.filter import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.launchIn import kotlinx.coroutines.flow.launchIn
@ -36,9 +37,9 @@ class TrackSearchDialog : DialogController {
private val service: TrackService private val service: TrackService
private val trackController private val trackController
get() = targetController as TrackController get() = targetController as MangaController
constructor(target: TrackController, service: TrackService) : super( constructor(target: MangaController, service: TrackService) : super(
bundleOf(KEY_SERVICE to service.id) bundleOf(KEY_SERVICE to service.id)
) { ) {
targetController = target targetController = target
@ -105,7 +106,7 @@ class TrackSearchDialog : DialogController {
val binding = binding ?: return val binding = binding ?: return
binding.progress.isVisible = true binding.progress.isVisible = true
binding.trackSearchList.isVisible = false binding.trackSearchList.isVisible = false
trackController.presenter.search(query, service) trackController.presenter.trackingSearch(query, service)
} }
fun onSearchResults(results: List<TrackSearch>) { fun onSearchResults(results: List<TrackSearch>) {

View File

@ -0,0 +1,144 @@
package eu.kanade.tachiyomi.ui.manga.track
import android.content.Intent
import android.os.Bundle
import android.view.ViewGroup
import androidx.core.net.toUri
import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior
import eu.kanade.tachiyomi.data.database.models.Manga
import eu.kanade.tachiyomi.databinding.TrackControllerBinding
import eu.kanade.tachiyomi.ui.manga.MangaController
import eu.kanade.tachiyomi.util.system.copyToClipboard
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
class TrackSheet(
val controller: MangaController,
val manga: Manga
) : BaseBottomSheetDialog(controller.activity!!),
TrackAdapter.OnClickListener,
SetTrackStatusDialog.Listener,
SetTrackChaptersDialog.Listener,
SetTrackScoreDialog.Listener,
SetTrackReadingDatesDialog.Listener {
private lateinit var binding: TrackControllerBinding
private lateinit var sheetBehavior: BottomSheetBehavior<*>
private lateinit var adapter: TrackAdapter
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
binding = TrackControllerBinding.inflate(layoutInflater)
setContentView(binding.root)
adapter = TrackAdapter(this)
binding.trackRecycler.layoutManager = LinearLayoutManager(context)
binding.trackRecycler.adapter = adapter
sheetBehavior = BottomSheetBehavior.from(binding.root.parent as ViewGroup)
adapter.items = controller.presenter.trackList
}
override fun onStart() {
super.onStart()
sheetBehavior.skipCollapsed = true
}
override fun show() {
super.show()
controller.presenter.trackingRefresh()
sheetBehavior.state = BottomSheetBehavior.STATE_EXPANDED
}
fun onNextTrackers(trackers: List<TrackItem>) {
if (this::adapter.isInitialized) {
adapter.items = trackers
adapter.notifyDataSetChanged()
}
}
override fun onLogoClick(position: Int) {
val track = adapter.getItem(position)?.track ?: return
if (track.tracking_url.isNotBlank()) {
controller.activity?.startActivity(Intent(Intent.ACTION_VIEW, track.tracking_url.toUri()))
}
}
override fun onSetClick(position: Int) {
val item = adapter.getItem(position) ?: return
TrackSearchDialog(controller, item.service).showDialog(controller.router, TAG_SEARCH_CONTROLLER)
}
override fun onTitleLongClick(position: Int) {
adapter.getItem(position)?.track?.title?.let {
controller.activity?.copyToClipboard(it, it)
}
}
override fun onStatusClick(position: Int) {
val item = adapter.getItem(position) ?: return
if (item.track == null) return
SetTrackStatusDialog(controller, this, item).showDialog(controller.router)
}
override fun onChaptersClick(position: Int) {
val item = adapter.getItem(position) ?: return
if (item.track == null) return
SetTrackChaptersDialog(controller, this, item).showDialog(controller.router)
}
override fun onScoreClick(position: Int) {
val item = adapter.getItem(position) ?: return
if (item.track == null) return
SetTrackScoreDialog(controller, this, item).showDialog(controller.router)
}
override fun onStartDateClick(position: Int) {
val item = adapter.getItem(position) ?: return
if (item.track == null) return
SetTrackReadingDatesDialog(controller, this, SetTrackReadingDatesDialog.ReadingDate.Start, item).showDialog(controller.router)
}
override fun onFinishDateClick(position: Int) {
val item = adapter.getItem(position) ?: return
if (item.track == null) return
SetTrackReadingDatesDialog(controller, this, SetTrackReadingDatesDialog.ReadingDate.Finish, item).showDialog(controller.router)
}
override fun setStatus(item: TrackItem, selection: Int) {
controller.presenter.setTrackerStatus(item, selection)
}
override fun setChaptersRead(item: TrackItem, chaptersRead: Int) {
controller.presenter.setTrackerLastChapterRead(item, chaptersRead)
}
override fun setScore(item: TrackItem, score: Int) {
controller.presenter.setTrackerScore(item, score)
}
override fun setReadingDate(item: TrackItem, type: SetTrackReadingDatesDialog.ReadingDate, date: Long) {
when (type) {
SetTrackReadingDatesDialog.ReadingDate.Start -> controller.presenter.setTrackerStartDate(item, date)
SetTrackReadingDatesDialog.ReadingDate.Finish -> controller.presenter.setTrackerFinishDate(item, date)
}
}
fun getSearchDialog(): TrackSearchDialog? {
return controller.router.getControllerWithTag(TAG_SEARCH_CONTROLLER) as? TrackSearchDialog
}
private companion object {
const val TAG_SEARCH_CONTROLLER = "track_search_controller"
}
}

View File

@ -5,20 +5,12 @@
android:layout_height="match_parent" android:layout_height="match_parent"
android:orientation="vertical"> android:orientation="vertical">
<eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout
android:id="@+id/swipe_refresh"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_marginTop="4dp"
android:layout_marginBottom="4dp"
android:orientation="vertical">
<androidx.recyclerview.widget.RecyclerView <androidx.recyclerview.widget.RecyclerView
android:id="@+id/track_recycler" android:id="@+id/track_recycler"
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent" android:layout_height="match_parent"
android:layout_marginTop="4dp"
android:clipToPadding="false"
tools:listitem="@layout/track_item" /> tools:listitem="@layout/track_item" />
</eu.kanade.tachiyomi.widget.ThemedSwipeRefreshLayout>
</LinearLayout> </LinearLayout>