MangaPresenter.kt 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801
  1. package eu.kanade.tachiyomi.ui.manga
  2. import android.content.Context
  3. import android.net.Uri
  4. import android.os.Bundle
  5. import com.jakewharton.rxrelay.PublishRelay
  6. import eu.kanade.tachiyomi.data.cache.CoverCache
  7. import eu.kanade.tachiyomi.data.database.DatabaseHelper
  8. import eu.kanade.tachiyomi.data.database.models.Category
  9. import eu.kanade.tachiyomi.data.database.models.Chapter
  10. import eu.kanade.tachiyomi.data.database.models.Manga
  11. import eu.kanade.tachiyomi.data.database.models.MangaCategory
  12. import eu.kanade.tachiyomi.data.database.models.Track
  13. import eu.kanade.tachiyomi.data.database.models.toMangaInfo
  14. import eu.kanade.tachiyomi.data.download.DownloadManager
  15. import eu.kanade.tachiyomi.data.download.model.Download
  16. import eu.kanade.tachiyomi.data.preference.PreferencesHelper
  17. import eu.kanade.tachiyomi.data.track.TrackManager
  18. import eu.kanade.tachiyomi.data.track.TrackService
  19. import eu.kanade.tachiyomi.source.LocalSource
  20. import eu.kanade.tachiyomi.source.Source
  21. import eu.kanade.tachiyomi.source.model.toSChapter
  22. import eu.kanade.tachiyomi.source.model.toSManga
  23. import eu.kanade.tachiyomi.ui.base.presenter.BasePresenter
  24. import eu.kanade.tachiyomi.ui.manga.chapter.ChapterItem
  25. import eu.kanade.tachiyomi.ui.manga.track.TrackItem
  26. import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
  27. import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource
  28. import eu.kanade.tachiyomi.util.isLocal
  29. import eu.kanade.tachiyomi.util.lang.launchIO
  30. import eu.kanade.tachiyomi.util.lang.withUIContext
  31. import eu.kanade.tachiyomi.util.prepUpdateCover
  32. import eu.kanade.tachiyomi.util.removeCovers
  33. import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
  34. import eu.kanade.tachiyomi.util.system.toast
  35. import eu.kanade.tachiyomi.util.updateCoverLastModified
  36. import eu.kanade.tachiyomi.widget.ExtendedNavigationView.Item.TriStateGroup.State
  37. import kotlinx.coroutines.Job
  38. import kotlinx.coroutines.async
  39. import kotlinx.coroutines.awaitAll
  40. import kotlinx.coroutines.supervisorScope
  41. import rx.Observable
  42. import rx.Subscription
  43. import rx.android.schedulers.AndroidSchedulers
  44. import rx.schedulers.Schedulers
  45. import timber.log.Timber
  46. import uy.kohesive.injekt.Injekt
  47. import uy.kohesive.injekt.api.get
  48. import java.util.Date
  49. class MangaPresenter(
  50. val manga: Manga,
  51. val source: Source,
  52. val preferences: PreferencesHelper = Injekt.get(),
  53. private val db: DatabaseHelper = Injekt.get(),
  54. private val trackManager: TrackManager = Injekt.get(),
  55. private val downloadManager: DownloadManager = Injekt.get(),
  56. private val coverCache: CoverCache = Injekt.get()
  57. ) : BasePresenter<MangaController>() {
  58. /**
  59. * Subscription to update the manga from the source.
  60. */
  61. private var fetchMangaJob: Job? = null
  62. /**
  63. * List of chapters of the manga. It's always unfiltered and unsorted.
  64. */
  65. var chapters: List<ChapterItem> = emptyList()
  66. private set
  67. /**
  68. * Subject of list of chapters to allow updating the view without going to DB.
  69. */
  70. private val chaptersRelay: PublishRelay<List<ChapterItem>> by lazy {
  71. PublishRelay.create<List<ChapterItem>>()
  72. }
  73. /**
  74. * Whether the chapter list has been requested to the source.
  75. */
  76. var hasRequested = false
  77. private set
  78. /**
  79. * Subscription to retrieve the new list of chapters from the source.
  80. */
  81. private var fetchChaptersJob: Job? = null
  82. /**
  83. * Subscription to observe download status changes.
  84. */
  85. private var observeDownloadsStatusSubscription: Subscription? = null
  86. private var observeDownloadsPageSubscription: Subscription? = null
  87. private var _trackList: List<TrackItem> = emptyList()
  88. val trackList get() = _trackList
  89. private val loggedServices by lazy { trackManager.services.filter { it.isLogged } }
  90. private var trackSubscription: Subscription? = null
  91. private var searchTrackerJob: Job? = null
  92. private var refreshTrackersJob: Job? = null
  93. override fun onCreate(savedState: Bundle?) {
  94. super.onCreate(savedState)
  95. if (!manga.favorite) {
  96. ChapterSettingsHelper.applySettingDefaults(manga)
  97. }
  98. // Manga info - start
  99. getMangaObservable()
  100. .observeOn(AndroidSchedulers.mainThread())
  101. .subscribeLatestCache({ view, manga -> view.onNextMangaInfo(manga, source) })
  102. getTrackingObservable()
  103. .observeOn(AndroidSchedulers.mainThread())
  104. .subscribeLatestCache(MangaController::onTrackingCount) { _, error -> Timber.e(error) }
  105. // Prepare the relay.
  106. chaptersRelay.flatMap { applyChapterFilters(it) }
  107. .observeOn(AndroidSchedulers.mainThread())
  108. .subscribeLatestCache(MangaController::onNextChapters) { _, error -> Timber.e(error) }
  109. // Manga info - end
  110. // Chapters list - start
  111. // Add the subscription that retrieves the chapters from the database, keeps subscribed to
  112. // changes, and sends the list of chapters to the relay.
  113. add(
  114. db.getChapters(manga).asRxObservable()
  115. .map { chapters ->
  116. // Convert every chapter to a model.
  117. chapters.map { it.toModel() }
  118. }
  119. .doOnNext { chapters ->
  120. // Find downloaded chapters
  121. setDownloadedChapters(chapters)
  122. // Store the last emission
  123. this.chapters = chapters
  124. // Listen for download status changes
  125. observeDownloads()
  126. }
  127. .subscribe { chaptersRelay.call(it) }
  128. )
  129. // Chapters list - end
  130. fetchTrackers()
  131. }
  132. // Manga info - start
  133. private fun getMangaObservable(): Observable<Manga> {
  134. return db.getManga(manga.url, manga.source).asRxObservable()
  135. }
  136. private fun getTrackingObservable(): Observable<Int> {
  137. if (!trackManager.hasLoggedServices()) {
  138. return Observable.just(0)
  139. }
  140. return db.getTracks(manga).asRxObservable()
  141. .map { tracks ->
  142. val loggedServices = trackManager.services.filter { it.isLogged }.map { it.id }
  143. tracks.filter { it.sync_id in loggedServices }
  144. }
  145. .map { it.size }
  146. }
  147. /**
  148. * Fetch manga information from source.
  149. */
  150. fun fetchMangaFromSource(manualFetch: Boolean = false) {
  151. if (fetchMangaJob?.isActive == true) return
  152. fetchMangaJob = presenterScope.launchIO {
  153. try {
  154. val networkManga = source.getMangaDetails(manga.toMangaInfo())
  155. val sManga = networkManga.toSManga()
  156. manga.prepUpdateCover(coverCache, sManga, manualFetch)
  157. manga.copyFrom(sManga)
  158. manga.initialized = true
  159. db.insertManga(manga).executeAsBlocking()
  160. withUIContext { view?.onFetchMangaInfoDone() }
  161. } catch (e: Throwable) {
  162. withUIContext { view?.onFetchMangaInfoError(e) }
  163. }
  164. }
  165. }
  166. /**
  167. * Update favorite status of manga, (removes / adds) manga (to / from) library.
  168. *
  169. * @return the new status of the manga.
  170. */
  171. fun toggleFavorite(): Boolean {
  172. manga.favorite = !manga.favorite
  173. manga.date_added = when (manga.favorite) {
  174. true -> Date().time
  175. false -> 0
  176. }
  177. if (!manga.favorite) {
  178. manga.removeCovers(coverCache)
  179. }
  180. db.insertManga(manga).executeAsBlocking()
  181. return manga.favorite
  182. }
  183. /**
  184. * Returns true if the manga has any downloads.
  185. */
  186. fun hasDownloads(): Boolean {
  187. return downloadManager.getDownloadCount(manga) > 0
  188. }
  189. /**
  190. * Deletes all the downloads for the manga.
  191. */
  192. fun deleteDownloads() {
  193. downloadManager.deleteManga(manga, source)
  194. }
  195. /**
  196. * Get user categories.
  197. *
  198. * @return List of categories, not including the default category
  199. */
  200. fun getCategories(): List<Category> {
  201. return db.getCategories().executeAsBlocking()
  202. }
  203. /**
  204. * Gets the category id's the manga is in, if the manga is not in a category, returns the default id.
  205. *
  206. * @param manga the manga to get categories from.
  207. * @return Array of category ids the manga is in, if none returns default id
  208. */
  209. fun getMangaCategoryIds(manga: Manga): Array<Int> {
  210. val categories = db.getCategoriesForManga(manga).executeAsBlocking()
  211. return categories.mapNotNull { it.id }.toTypedArray()
  212. }
  213. /**
  214. * Move the given manga to categories.
  215. *
  216. * @param manga the manga to move.
  217. * @param categories the selected categories.
  218. */
  219. fun moveMangaToCategories(manga: Manga, categories: List<Category>) {
  220. val mc = categories.filter { it.id != 0 }.map { MangaCategory.create(manga, it) }
  221. db.setMangaCategories(mc, listOf(manga))
  222. }
  223. /**
  224. * Move the given manga to the category.
  225. *
  226. * @param manga the manga to move.
  227. * @param category the selected category, or null for default category.
  228. */
  229. fun moveMangaToCategory(manga: Manga, category: Category?) {
  230. moveMangaToCategories(manga, listOfNotNull(category))
  231. }
  232. /**
  233. * Update cover with local file.
  234. *
  235. * @param manga the manga edited.
  236. * @param context Context.
  237. * @param data uri of the cover resource.
  238. */
  239. fun editCover(manga: Manga, context: Context, data: Uri) {
  240. Observable
  241. .fromCallable {
  242. context.contentResolver.openInputStream(data)?.use {
  243. if (manga.isLocal()) {
  244. LocalSource.updateCover(context, manga, it)
  245. manga.updateCoverLastModified(db)
  246. } else if (manga.favorite) {
  247. coverCache.setCustomCoverToCache(manga, it)
  248. manga.updateCoverLastModified(db)
  249. }
  250. }
  251. }
  252. .subscribeOn(Schedulers.io())
  253. .observeOn(AndroidSchedulers.mainThread())
  254. .subscribeFirst(
  255. { view, _ -> view.onSetCoverSuccess() },
  256. { view, e -> view.onSetCoverError(e) }
  257. )
  258. }
  259. fun deleteCustomCover(manga: Manga) {
  260. Observable
  261. .fromCallable {
  262. coverCache.deleteCustomCover(manga)
  263. manga.updateCoverLastModified(db)
  264. }
  265. .subscribeOn(Schedulers.io())
  266. .observeOn(AndroidSchedulers.mainThread())
  267. .subscribeFirst(
  268. { view, _ -> view.onSetCoverSuccess() },
  269. { view, e -> view.onSetCoverError(e) }
  270. )
  271. }
  272. // Manga info - end
  273. // Chapters list - start
  274. private fun observeDownloads() {
  275. observeDownloadsStatusSubscription?.let { remove(it) }
  276. observeDownloadsStatusSubscription = downloadManager.queue.getStatusObservable()
  277. .observeOn(Schedulers.io())
  278. .onBackpressureLatest()
  279. .filter { download -> download.manga.id == manga.id }
  280. .observeOn(AndroidSchedulers.mainThread())
  281. .subscribeLatestCache(
  282. { view, it ->
  283. onDownloadStatusChange(it)
  284. view.onChapterDownloadUpdate(it)
  285. },
  286. { _, error ->
  287. Timber.e(error)
  288. }
  289. )
  290. observeDownloadsPageSubscription?.let { remove(it) }
  291. observeDownloadsPageSubscription = downloadManager.queue.getProgressObservable()
  292. .observeOn(Schedulers.io())
  293. .onBackpressureLatest()
  294. .filter { download -> download.manga.id == manga.id }
  295. .observeOn(AndroidSchedulers.mainThread())
  296. .subscribeLatestCache(MangaController::onChapterDownloadUpdate) { _, error ->
  297. Timber.e(error)
  298. }
  299. }
  300. /**
  301. * Converts a chapter from the database to an extended model, allowing to store new fields.
  302. */
  303. private fun Chapter.toModel(): ChapterItem {
  304. // Create the model object.
  305. val model = ChapterItem(this, manga)
  306. // Find an active download for this chapter.
  307. val download = downloadManager.queue.find { it.chapter.id == id }
  308. if (download != null) {
  309. // If there's an active download, assign it.
  310. model.download = download
  311. }
  312. return model
  313. }
  314. /**
  315. * Finds and assigns the list of downloaded chapters.
  316. *
  317. * @param chapters the list of chapter from the database.
  318. */
  319. private fun setDownloadedChapters(chapters: List<ChapterItem>) {
  320. chapters
  321. .filter { downloadManager.isChapterDownloaded(it, manga) }
  322. .forEach { it.status = Download.State.DOWNLOADED }
  323. }
  324. /**
  325. * Requests an updated list of chapters from the source.
  326. */
  327. fun fetchChaptersFromSource(manualFetch: Boolean = false) {
  328. hasRequested = true
  329. if (fetchChaptersJob?.isActive == true) return
  330. fetchChaptersJob = presenterScope.launchIO {
  331. try {
  332. val chapters = source.getChapterList(manga.toMangaInfo())
  333. .map { it.toSChapter() }
  334. val (newChapters, _) = syncChaptersWithSource(db, chapters, manga, source)
  335. if (manualFetch) {
  336. downloadNewChapters(newChapters)
  337. }
  338. withUIContext { view?.onFetchChaptersDone() }
  339. } catch (e: Throwable) {
  340. withUIContext { view?.onFetchChaptersError(e) }
  341. }
  342. }
  343. }
  344. /**
  345. * Updates the UI after applying the filters.
  346. */
  347. private fun refreshChapters() {
  348. chaptersRelay.call(chapters)
  349. }
  350. /**
  351. * Applies the view filters to the list of chapters obtained from the database.
  352. * @param chapters the list of chapters from the database
  353. * @return an observable of the list of chapters filtered and sorted.
  354. */
  355. private fun applyChapterFilters(chapters: List<ChapterItem>): Observable<List<ChapterItem>> {
  356. var observable = Observable.from(chapters).subscribeOn(Schedulers.io())
  357. val unreadFilter = onlyUnread()
  358. if (unreadFilter == State.INCLUDE) {
  359. observable = observable.filter { !it.read }
  360. } else if (unreadFilter == State.EXCLUDE) {
  361. observable = observable.filter { it.read }
  362. }
  363. val downloadedFilter = onlyDownloaded()
  364. if (downloadedFilter == State.INCLUDE) {
  365. observable = observable.filter { it.isDownloaded || it.manga.isLocal() }
  366. } else if (downloadedFilter == State.EXCLUDE) {
  367. observable = observable.filter { !it.isDownloaded && !it.manga.isLocal() }
  368. }
  369. val bookmarkedFilter = onlyBookmarked()
  370. if (bookmarkedFilter == State.INCLUDE) {
  371. observable = observable.filter { it.bookmark }
  372. } else if (bookmarkedFilter == State.EXCLUDE) {
  373. observable = observable.filter { !it.bookmark }
  374. }
  375. return observable.toSortedList(getChapterSort())
  376. }
  377. fun getChapterSort(): (Chapter, Chapter) -> Int {
  378. return when (manga.sorting) {
  379. Manga.SORTING_SOURCE -> when (sortDescending()) {
  380. true -> { c1, c2 -> c1.source_order.compareTo(c2.source_order) }
  381. false -> { c1, c2 -> c2.source_order.compareTo(c1.source_order) }
  382. }
  383. Manga.SORTING_NUMBER -> when (sortDescending()) {
  384. true -> { c1, c2 -> c2.chapter_number.compareTo(c1.chapter_number) }
  385. false -> { c1, c2 -> c1.chapter_number.compareTo(c2.chapter_number) }
  386. }
  387. Manga.SORTING_UPLOAD_DATE -> when (sortDescending()) {
  388. true -> { c1, c2 -> c2.date_upload.compareTo(c1.date_upload) }
  389. false -> { c1, c2 -> c1.date_upload.compareTo(c2.date_upload) }
  390. }
  391. else -> throw NotImplementedError("Unimplemented sorting method")
  392. }
  393. }
  394. /**
  395. * Called when a download for the active manga changes status.
  396. * @param download the download whose status changed.
  397. */
  398. private fun onDownloadStatusChange(download: Download) {
  399. // Assign the download to the model object.
  400. if (download.status == Download.State.QUEUE) {
  401. chapters.find { it.id == download.chapter.id }?.let {
  402. if (it.download == null) {
  403. it.download = download
  404. }
  405. }
  406. }
  407. // Force UI update if downloaded filter active and download finished.
  408. if (onlyDownloaded() != State.IGNORE && download.status == Download.State.DOWNLOADED) {
  409. refreshChapters()
  410. }
  411. }
  412. /**
  413. * Returns the next unread chapter or null if everything is read.
  414. */
  415. fun getNextUnreadChapter(): ChapterItem? {
  416. return chapters.sortedWith(getChapterSort()).findLast { !it.read }
  417. }
  418. /**
  419. * Mark the selected chapter list as read/unread.
  420. * @param selectedChapters the list of selected chapters.
  421. * @param read whether to mark chapters as read or unread.
  422. */
  423. fun markChaptersRead(selectedChapters: List<ChapterItem>, read: Boolean) {
  424. val chapters = selectedChapters.map { chapter ->
  425. chapter.read = read
  426. if (!read) {
  427. chapter.last_page_read = 0
  428. }
  429. chapter
  430. }
  431. launchIO {
  432. db.updateChaptersProgress(chapters).executeAsBlocking()
  433. if (preferences.removeAfterMarkedAsRead()) {
  434. deleteChapters(chapters.filter { it.read })
  435. }
  436. }
  437. }
  438. /**
  439. * Downloads the given list of chapters with the manager.
  440. * @param chapters the list of chapters to download.
  441. */
  442. fun downloadChapters(chapters: List<Chapter>) {
  443. downloadManager.downloadChapters(manga, chapters)
  444. }
  445. /**
  446. * Bookmarks the given list of chapters.
  447. * @param selectedChapters the list of chapters to bookmark.
  448. */
  449. fun bookmarkChapters(selectedChapters: List<ChapterItem>, bookmarked: Boolean) {
  450. launchIO {
  451. selectedChapters
  452. .forEach {
  453. it.bookmark = bookmarked
  454. db.updateChapterProgress(it).executeAsBlocking()
  455. }
  456. }
  457. }
  458. /**
  459. * Deletes the given list of chapter.
  460. * @param chapters the list of chapters to delete.
  461. */
  462. fun deleteChapters(chapters: List<ChapterItem>) {
  463. launchIO {
  464. try {
  465. downloadManager.deleteChapters(chapters, manga, source).forEach {
  466. if (it is ChapterItem) {
  467. it.status = Download.State.NOT_DOWNLOADED
  468. it.download = null
  469. }
  470. }
  471. if (onlyDownloaded() != State.IGNORE) {
  472. refreshChapters()
  473. }
  474. view?.onChaptersDeleted(chapters)
  475. } catch (e: Throwable) {
  476. view?.onChaptersDeletedError(e)
  477. }
  478. }
  479. }
  480. private fun downloadNewChapters(chapters: List<Chapter>) {
  481. if (chapters.isEmpty() || !manga.shouldDownloadNewChapters(db, preferences)) return
  482. downloadChapters(chapters)
  483. }
  484. /**
  485. * Reverses the sorting and requests an UI update.
  486. */
  487. fun reverseSortOrder() {
  488. manga.setChapterOrder(if (sortDescending()) Manga.SORT_ASC else Manga.SORT_DESC)
  489. db.updateFlags(manga).executeAsBlocking()
  490. refreshChapters()
  491. }
  492. /**
  493. * Sets the read filter and requests an UI update.
  494. * @param state whether to display only unread chapters or all chapters.
  495. */
  496. fun setUnreadFilter(state: State) {
  497. manga.readFilter = when (state) {
  498. State.IGNORE -> Manga.SHOW_ALL
  499. State.INCLUDE -> Manga.SHOW_UNREAD
  500. State.EXCLUDE -> Manga.SHOW_READ
  501. }
  502. db.updateFlags(manga).executeAsBlocking()
  503. refreshChapters()
  504. }
  505. /**
  506. * Sets the download filter and requests an UI update.
  507. * @param state whether to display only downloaded chapters or all chapters.
  508. */
  509. fun setDownloadedFilter(state: State) {
  510. manga.downloadedFilter = when (state) {
  511. State.IGNORE -> Manga.SHOW_ALL
  512. State.INCLUDE -> Manga.SHOW_DOWNLOADED
  513. State.EXCLUDE -> Manga.SHOW_NOT_DOWNLOADED
  514. }
  515. db.updateFlags(manga).executeAsBlocking()
  516. refreshChapters()
  517. }
  518. /**
  519. * Sets the bookmark filter and requests an UI update.
  520. * @param state whether to display only bookmarked chapters or all chapters.
  521. */
  522. fun setBookmarkedFilter(state: State) {
  523. manga.bookmarkedFilter = when (state) {
  524. State.IGNORE -> Manga.SHOW_ALL
  525. State.INCLUDE -> Manga.SHOW_BOOKMARKED
  526. State.EXCLUDE -> Manga.SHOW_NOT_BOOKMARKED
  527. }
  528. db.updateFlags(manga).executeAsBlocking()
  529. refreshChapters()
  530. }
  531. /**
  532. * Sets the active display mode.
  533. * @param mode the mode to set.
  534. */
  535. fun setDisplayMode(mode: Int) {
  536. manga.displayMode = mode
  537. db.updateFlags(manga).executeAsBlocking()
  538. }
  539. /**
  540. * Sets the sorting method and requests an UI update.
  541. * @param sort the sorting mode.
  542. */
  543. fun setSorting(sort: Int) {
  544. manga.sorting = sort
  545. db.updateFlags(manga).executeAsBlocking()
  546. refreshChapters()
  547. }
  548. /**
  549. * Whether downloaded only mode is enabled.
  550. */
  551. fun forceDownloaded(): Boolean {
  552. return manga.favorite && preferences.downloadedOnly().get()
  553. }
  554. /**
  555. * Whether the display only downloaded filter is enabled.
  556. */
  557. fun onlyDownloaded(): State {
  558. if (forceDownloaded()) {
  559. return State.INCLUDE
  560. }
  561. return when (manga.downloadedFilter) {
  562. Manga.SHOW_DOWNLOADED -> State.INCLUDE
  563. Manga.SHOW_NOT_DOWNLOADED -> State.EXCLUDE
  564. else -> State.IGNORE
  565. }
  566. }
  567. /**
  568. * Whether the display only downloaded filter is enabled.
  569. */
  570. fun onlyBookmarked(): State {
  571. return when (manga.bookmarkedFilter) {
  572. Manga.SHOW_BOOKMARKED -> State.INCLUDE
  573. Manga.SHOW_NOT_BOOKMARKED -> State.EXCLUDE
  574. else -> State.IGNORE
  575. }
  576. }
  577. /**
  578. * Whether the display only unread filter is enabled.
  579. */
  580. fun onlyUnread(): State {
  581. return when (manga.readFilter) {
  582. Manga.SHOW_UNREAD -> State.INCLUDE
  583. Manga.SHOW_READ -> State.EXCLUDE
  584. else -> State.IGNORE
  585. }
  586. }
  587. /**
  588. * Whether the sorting method is descending or ascending.
  589. */
  590. fun sortDescending(): Boolean {
  591. return manga.sortDescending()
  592. }
  593. // Chapters list - end
  594. // Track sheet - start
  595. private fun fetchTrackers() {
  596. trackSubscription?.let { remove(it) }
  597. trackSubscription = db.getTracks(manga)
  598. .asRxObservable()
  599. .map { tracks ->
  600. loggedServices.map { service ->
  601. TrackItem(tracks.find { it.sync_id == service.id }, service)
  602. }
  603. }
  604. .observeOn(AndroidSchedulers.mainThread())
  605. .doOnNext { _trackList = it }
  606. .subscribeLatestCache(MangaController::onNextTrackers)
  607. }
  608. fun refreshTrackers() {
  609. refreshTrackersJob?.cancel()
  610. refreshTrackersJob = launchIO {
  611. supervisorScope {
  612. try {
  613. trackList
  614. .filter { it.track != null }
  615. .map {
  616. async {
  617. val track = it.service.refresh(it.track!!)
  618. db.insertTrack(track).executeAsBlocking()
  619. }
  620. }
  621. .awaitAll()
  622. withUIContext { view?.onTrackingRefreshDone() }
  623. } catch (e: Throwable) {
  624. withUIContext { view?.onTrackingRefreshError(e) }
  625. }
  626. }
  627. }
  628. }
  629. fun trackingSearch(query: String, service: TrackService) {
  630. searchTrackerJob?.cancel()
  631. searchTrackerJob = launchIO {
  632. try {
  633. val results = service.search(query)
  634. withUIContext { view?.onTrackingSearchResults(results) }
  635. } catch (e: Throwable) {
  636. withUIContext { view?.onTrackingSearchResultsError(e) }
  637. }
  638. }
  639. }
  640. fun registerTracking(item: Track?, service: TrackService) {
  641. if (item != null) {
  642. item.manga_id = manga.id!!
  643. launchIO {
  644. try {
  645. service.bind(item)
  646. db.insertTrack(item).executeAsBlocking()
  647. } catch (e: Throwable) {
  648. withUIContext { view?.applicationContext?.toast(e.message) }
  649. }
  650. }
  651. } else {
  652. unregisterTracking(service)
  653. }
  654. }
  655. fun unregisterTracking(service: TrackService) {
  656. db.deleteTrackForManga(manga, service).executeAsBlocking()
  657. }
  658. private fun updateRemote(track: Track, service: TrackService) {
  659. launchIO {
  660. try {
  661. service.update(track)
  662. db.insertTrack(track).executeAsBlocking()
  663. withUIContext { view?.onTrackingRefreshDone() }
  664. } catch (e: Throwable) {
  665. withUIContext { view?.onTrackingRefreshError(e) }
  666. // Restart on error to set old values
  667. fetchTrackers()
  668. }
  669. }
  670. }
  671. fun setTrackerStatus(item: TrackItem, index: Int) {
  672. val track = item.track!!
  673. track.status = item.service.getStatusList()[index]
  674. if (track.status == item.service.getCompletionStatus() && track.total_chapters != 0) {
  675. track.last_chapter_read = track.total_chapters
  676. }
  677. updateRemote(track, item.service)
  678. }
  679. fun setTrackerScore(item: TrackItem, index: Int) {
  680. val track = item.track!!
  681. track.score = item.service.indexToScore(index)
  682. updateRemote(track, item.service)
  683. }
  684. fun setTrackerLastChapterRead(item: TrackItem, chapterNumber: Int) {
  685. val track = item.track!!
  686. track.last_chapter_read = chapterNumber
  687. if (track.total_chapters != 0 && track.last_chapter_read == track.total_chapters) {
  688. track.status = item.service.getCompletionStatus()
  689. }
  690. updateRemote(track, item.service)
  691. }
  692. fun setTrackerStartDate(item: TrackItem, date: Long) {
  693. val track = item.track!!
  694. track.started_reading_date = date
  695. updateRemote(track, item.service)
  696. }
  697. fun setTrackerFinishDate(item: TrackItem, date: Long) {
  698. val track = item.track!!
  699. track.finished_reading_date = date
  700. updateRemote(track, item.service)
  701. }
  702. // Track sheet - end
  703. }