From 535abcbb8b2197faef77a6b0ba56aa4309067d4c Mon Sep 17 00:00:00 2001 From: Andreas Date: Sun, 31 Jan 2021 20:43:43 +0100 Subject: [PATCH] Move tracking to a bottom sheet (#4364) * Move tracking to a bottom sheet * Give methods better names and remove unnecessary annotation --- .../tachiyomi/ui/manga/MangaController.kt | 40 +++- .../tachiyomi/ui/manga/MangaPresenter.kt | 142 +++++++++++++ .../ui/manga/track/SetTrackChaptersDialog.kt | 9 +- .../manga/track/SetTrackReadingDatesDialog.kt | 9 +- .../ui/manga/track/SetTrackScoreDialog.kt | 9 +- .../ui/manga/track/SetTrackStatusDialog.kt | 9 +- .../tachiyomi/ui/manga/track/TrackAdapter.kt | 4 +- .../ui/manga/track/TrackController.kt | 200 ------------------ .../ui/manga/track/TrackPresenter.kt | 164 -------------- .../ui/manga/track/TrackSearchDialog.kt | 7 +- .../tachiyomi/ui/manga/track/TrackSheet.kt | 144 +++++++++++++ app/src/main/res/layout/track_controller.xml | 16 +- 12 files changed, 357 insertions(+), 396 deletions(-) delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackController.kt delete mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSheet.kt diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt index aa8897e4ac..1c43079eeb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaController.kt +++ b/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) { + trackSheet?.onNextTrackers(trackers) + } + + fun onTrackingRefreshDone() { + } + + fun onTrackingRefreshError(error: Throwable) { + Timber.e(error) + activity?.toast(error.message) + } + + fun onTrackingSearchResults(results: List) { + 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" diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt index 1df42162ee..ea36087f54 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaPresenter.kt +++ b/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 = 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 } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt index c5999f5c11..f12e6d6ddf 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackChaptersDialog.kt +++ b/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 : 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 : 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackReadingDatesDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackReadingDatesDialog.kt index f1e0ee5ffe..a0331c952f 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackReadingDatesDialog.kt +++ b/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 : 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 : DialogController } override fun onCreateDialog(savedViewState: Bundle?): Dialog { - val listener = (targetController as? Listener) - return MaterialDialog(activity!!) .title( when (dateToUpdate) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt index edff52a28f..8277a58657 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackScoreDialog.kt +++ b/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 : 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 : 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) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt index 7adad27355..4753a41729 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/SetTrackStatusDialog.kt +++ b/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 : 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 : DialogController initialSelection = selectedIndex, waitForPositiveButton = false ) { dialog, position, _ -> - (targetController as? Listener)?.setStatus(item, position) + listener.setStatus(item, position) dialog.dismiss() } } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt index 221212ef7f..c85aec7901 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackAdapter.kt +++ b/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() { +class TrackAdapter(listener: OnClickListener) : RecyclerView.Adapter() { private lateinit var binding: TrackItemBinding @@ -17,7 +17,7 @@ class TrackAdapter(controller: TrackController) : RecyclerView.Adapter, - 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().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) { - val atLeastOneLink = trackings.any { it.track != null } - adapter?.items = trackings - binding.swipeRefresh.isEnabled = atLeastOneLink - } - - fun onSearchResults(results: List) { - 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" - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt deleted file mode 100644 index 4e12028176..0000000000 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackPresenter.kt +++ /dev/null @@ -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() { - - private val context = preferences.context - - private var trackList: List = 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) - } -} diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt index e2328c5800..d02e4015c3 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSearchDialog.kt +++ b/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) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSheet.kt new file mode 100644 index 0000000000..1355e919cf --- /dev/null +++ b/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) { + 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" + } +} diff --git a/app/src/main/res/layout/track_controller.xml b/app/src/main/res/layout/track_controller.xml index c55e959ad0..07c707a77e 100644 --- a/app/src/main/res/layout/track_controller.xml +++ b/app/src/main/res/layout/track_controller.xml @@ -5,20 +5,12 @@ android:layout_height="match_parent" android:orientation="vertical"> - - - - - + android:clipToPadding="false" + tools:listitem="@layout/track_item" />