Browse Source

Move tracking to a bottom sheet (#4364)

* Move tracking to a bottom sheet

* Give methods better names and remove unnecessary annotation
Andreas 4 years ago
parent
commit
535abcbb8b

+ 38 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt

@@ -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.model.Download
 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.source.LocalSource
 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.base.BaseChaptersAdapter
 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.recent.history.HistoryController
 import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController
@@ -160,6 +163,8 @@ class MangaController :
     private var isRefreshingInfo = false
     private var isRefreshingChapters = false
 
+    private var trackSheet: TrackSheet? = null
+
     init {
         setHasOptionsMenu(true)
     }
@@ -246,6 +251,8 @@ class MangaController :
             }
         }
 
+        trackSheet = TrackSheet(this, manga!!)
+
         updateFilterIconState()
     }
 
@@ -461,7 +468,7 @@ class MangaController :
     }
 
     fun onTrackingClick() {
-        router.pushController(TrackController(manga).withFadeTransaction())
+        trackSheet?.show()
     }
 
     private fun addToLibrary(manga: Manga) {
@@ -1030,6 +1037,35 @@ class MangaController :
 
     // 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 {
         const val FROM_SOURCE_EXTRA = "from_source"
         const val MANGA_EXTRA = "manga"

+ 142 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt

@@ -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.Manga
 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.download.DownloadManager
 import eu.kanade.tachiyomi.data.download.model.Download
 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.source.LocalSource
 import eu.kanade.tachiyomi.source.Source
 import eu.kanade.tachiyomi.source.model.toSChapter
 import eu.kanade.tachiyomi.source.model.toSManga
 import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
 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.syncChaptersWithSource
 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.removeCovers
 import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
+import eu.kanade.tachiyomi.util.system.toast
 import eu.kanade.tachiyomi.util.updateCoverLastModified
 import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
 import kotlinx.coroutines.Job
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.supervisorScope
 import rx.Observable
 import rx.Subscription
 import rx.android.schedulers.AndroidSchedulers
@@ -86,6 +93,15 @@ class MangaPresenter(
     private var observeDownloadsStatusSubscription: 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?) {
         super.onCreate(savedState)
 
@@ -134,6 +150,8 @@ class MangaPresenter(
         )
 
         // Chapters list - end
+
+        fetchTrackers()
     }
 
     // Manga info - start
@@ -645,4 +663,128 @@ class MangaPresenter(
     }
 
     // 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
 }

+ 6 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt

@@ -16,14 +16,17 @@ import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
 class SetTrackChaptersDialog<T> : DialogController
-        where T : Controller, T : SetTrackChaptersDialog.Listener {
+        where T : Controller {
 
     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)
     ) {
         targetController = target
+        this.listener = listener
         this.item = item
     }
 
@@ -46,7 +49,7 @@ class SetTrackChaptersDialog<T> : DialogController
                 val np: NumberPicker = view.findViewById(R.id.chapters_picker)
                 np.clearFocus()
 
-                (targetController as? Listener)?.setChaptersRead(item, np.value)
+                listener.setChaptersRead(item, np.value)
             }
             .negativeButton(android.R.string.cancel)
 

+ 5 - 4
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackReadingDatesDialog.kt

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

+ 6 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt

@@ -16,14 +16,17 @@ import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
 class SetTrackScoreDialog<T> : DialogController
-        where T : Controller, T : SetTrackScoreDialog.Listener {
+        where T : Controller {
 
     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)
     ) {
         targetController = target
+        this.listener = listener
         this.item = item
     }
 
@@ -46,7 +49,7 @@ class SetTrackScoreDialog<T> : DialogController
                 val np: NumberPicker = view.findViewById(R.id.score_picker)
                 np.clearFocus()
 
-                (targetController as? Listener)?.setScore(item, np.value)
+                listener.setScore(item, np.value)
             }
             .negativeButton(android.R.string.cancel)
 

+ 6 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt

@@ -14,14 +14,17 @@ import uy.kohesive.injekt.Injekt
 import uy.kohesive.injekt.api.get
 
 class SetTrackStatusDialog<T> : DialogController
-        where T : Controller, T : SetTrackStatusDialog.Listener {
+        where T : Controller {
 
     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)
     ) {
         targetController = target
+        this.listener = listener
         this.item = item
     }
 
@@ -46,7 +49,7 @@ class SetTrackStatusDialog<T> : DialogController
                 initialSelection = selectedIndex,
                 waitForPositiveButton = false
             ) { dialog, position, _ ->
-                (targetController as? Listener)?.setStatus(item, position)
+                listener.setStatus(item, position)
                 dialog.dismiss()
             }
     }

+ 2 - 2
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt

@@ -5,7 +5,7 @@ import android.view.ViewGroup
 import androidx.recyclerview.widget.RecyclerView
 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
 
@@ -17,7 +17,7 @@ class TrackAdapter(controller: TrackController) : RecyclerView.Adapter<TrackHold
             }
         }
 
-    val rowClickListener: OnClickListener = controller
+    val rowClickListener: OnClickListener = listener
 
     fun getItem(index: Int): TrackItem? {
         return items.getOrNull(index)

+ 0 - 200
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt

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

+ 0 - 164
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt

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

+ 4 - 3
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt

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

+ 144 - 0
app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSheet.kt

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

+ 4 - 12
app/src/main/res/layout/track_controller.xml

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