From 6717f7dd3b323a62e44cb0bebd600b6593fb0ad5 Mon Sep 17 00:00:00 2001 From: Jays2Kings Date: Sat, 3 Jul 2021 14:26:04 -0400 Subject: [PATCH] Add Komga as an unattended track service Co-Authored-By: arkon <4098258+arkon@users.noreply.github.com> Co-Authored-By: Gauthier <2139133+gotson@users.noreply.github.com> --- .../data/library/LibraryUpdateService.kt | 6 + .../data/preference/PreferenceKeys.kt | 2 + .../data/preference/PreferencesHelper.kt | 2 + .../data/track/NoLoginTrackService.kt | 8 ++ .../tachiyomi/data/track/TrackManager.kt | 6 +- .../data/track/UnattendedTrackService.kt | 21 ++++ .../tachiyomi/data/track/komga/Komga.kt | 116 ++++++++++++++++++ .../tachiyomi/data/track/komga/KomgaApi.kt | 84 +++++++++++++ .../tachiyomi/data/track/komga/KomgaModels.kt | 83 +++++++++++++ .../ui/manga/MangaDetailsController.kt | 7 +- .../ui/manga/MangaDetailsPresenter.kt | 13 +- .../tachiyomi/ui/manga/track/TrackHolder.kt | 11 +- .../ui/manga/track/TrackingBottomSheet.kt | 36 +++++- .../ui/setting/SettingsTrackingController.kt | 22 +++- .../kanade/tachiyomi/util/MangaExtensions.kt | 45 +++++++ .../util/chapter/ChapterTrackSync.kt | 42 +++++++ .../main/res/drawable/ic_tracker_komga.webp | Bin 0 -> 15834 bytes app/src/main/res/layout/track_item.xml | 3 +- app/src/main/res/values/strings.xml | 5 + 19 files changed, 499 insertions(+), 13 deletions(-) create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/NoLoginTrackService.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/UnattendedTrackService.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaModels.kt create mode 100644 app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterTrackSync.kt create mode 100644 app/src/main/res/drawable/ic_tracker_komga.webp diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt index 52074b97c1..d5c856e36e 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/library/LibraryUpdateService.kt @@ -26,12 +26,14 @@ import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.UnattendedTrackService import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.toSChapter import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.online.HttpSource import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource +import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.manga.MangaShortcutManager import eu.kanade.tachiyomi.util.shouldDownloadNewChapters @@ -496,6 +498,10 @@ class LibraryUpdateService( try { val newTrack = service.refresh(track) db.insertTrack(newTrack).executeAsBlocking() + + if (service is UnattendedTrackService) { + syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service) + } } catch (e: Exception) { Timber.e(e) } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt index 57dff5b593..f2d3a9fd82 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferenceKeys.kt @@ -88,6 +88,8 @@ object PreferenceKeys { const val autoUpdateTrack = "pref_auto_update_manga_sync_key" + const val autoAddTrack = "pref_auto_add_track_key" + const val lastUsedCatalogueSource = "last_catalogue_source" const val lastUsedCategory = "last_used_category" diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt index 2bbb5f6f99..d01a5fab74 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/preference/PreferencesHelper.kt @@ -192,6 +192,8 @@ class PreferencesHelper(val context: Context) { fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true) + fun autoAddTrack() = prefs.getBoolean(Keys.autoAddTrack, true) + fun lastUsedCatalogueSource() = flowPrefs.getLong(Keys.lastUsedCatalogueSource, -1) fun lastUsedCategory() = rxPrefs.getInteger(Keys.lastUsedCategory, 0) diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/NoLoginTrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/NoLoginTrackService.kt new file mode 100644 index 0000000000..7e5f02fc8f --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/NoLoginTrackService.kt @@ -0,0 +1,8 @@ +package eu.kanade.tachiyomi.data.track + +/** + * A TrackService that doesn't need explicit login. + */ +interface NoLoginTrackService { + fun loginNoop() +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt index 173a6ffd6f..05065920c2 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/TrackManager.kt @@ -4,6 +4,7 @@ import android.content.Context import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.bangumi.Bangumi import eu.kanade.tachiyomi.data.track.kitsu.Kitsu +import eu.kanade.tachiyomi.data.track.komga.Komga import eu.kanade.tachiyomi.data.track.myanimelist.MyAnimeList import eu.kanade.tachiyomi.data.track.shikimori.Shikimori @@ -15,6 +16,7 @@ class TrackManager(context: Context) { const val KITSU = 3 const val SHIKIMORI = 4 const val BANGUMI = 5 + const val KOMGA = 6 } val myAnimeList = MyAnimeList(context, MYANIMELIST) @@ -27,7 +29,9 @@ class TrackManager(context: Context) { val bangumi = Bangumi(context, BANGUMI) - val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi) + val komga = Komga(context, KOMGA) + + val services = listOf(myAnimeList, aniList, kitsu, shikimori, bangumi, komga) fun getService(id: Int) = services.find { it.id == id } diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/UnattendedTrackService.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/UnattendedTrackService.kt new file mode 100644 index 0000000000..fbbed65eaf --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/UnattendedTrackService.kt @@ -0,0 +1,21 @@ +package eu.kanade.tachiyomi.data.track + +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.source.Source + +/** + * An Unattended Track Service will never prompt the user to match a manga with the remote. + * It is expected that such Track Sercice can only work with specific sources and unique IDs. + */ +interface UnattendedTrackService { + /** + * This TrackService will only work with the sources that are accepted by this filter function. + */ + fun accept(source: Source): Boolean + + /** + * match is similar to TrackService.search, but only return zero or one match. + */ + suspend fun match(manga: Manga): TrackSearch? +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt new file mode 100644 index 0000000000..7ccaf63f3b --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt @@ -0,0 +1,116 @@ +package eu.kanade.tachiyomi.data.track.komga + +import android.content.Context +import android.graphics.Color +import androidx.annotation.StringRes +import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.NoLoginTrackService +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.UnattendedTrackService +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.data.track.updateNewTrackInfo +import eu.kanade.tachiyomi.source.Source +import okhttp3.Dns +import okhttp3.OkHttpClient + +class Komga(private val context: Context, id: Int) : TrackService(id), UnattendedTrackService, NoLoginTrackService { + + companion object { + const val UNREAD = 1 + const val READING = 2 + const val COMPLETED = 3 + + const val ACCEPTED_SOURCE = "eu.kanade.tachiyomi.extension.all.komga.Komga" + } + + override val client: OkHttpClient = + networkService.client.newBuilder() + .dns(Dns.SYSTEM) // don't use DNS over HTTPS as it breaks IP addressing + .build() + + val api by lazy { KomgaApi(client) } + + @StringRes + override fun nameRes() = R.string.komga + + override fun getLogo() = R.drawable.ic_tracker_komga + + override fun getLogoColor() = Color.rgb(51, 37, 50) + + override fun getStatusList() = listOf(UNREAD, READING, COMPLETED) + + override fun isCompletedStatus(index: Int): Boolean = getStatusList()[index] == COMPLETED + + override fun getStatus(status: Int): String = with(context) { + when (status) { + UNREAD -> getString(R.string.unread) + READING -> getString(R.string.currently_reading) + COMPLETED -> getString(R.string.completed) + else -> "" + } + } + + override fun getGlobalStatus(status: Int): String = with(context) { + when (status) { + UNREAD -> getString(R.string.plan_to_read) + READING -> getString(R.string.reading) + COMPLETED -> getString(R.string.completed) + else -> "" + } + } + + override fun completedStatus(): Int = COMPLETED + + override fun getScoreList(): List = emptyList() + + override fun displayScore(track: Track): String = "" + override suspend fun add(track: Track): Track { + track.status = READING + updateNewTrackInfo(track, UNREAD) + return api.updateProgress(track) + } + + override suspend fun update(track: Track, setToReadStatus: Boolean): Track { + if (setToReadStatus && track.status == UNREAD && track.last_chapter_read != 0) { + track.status = READING + } + return api.updateProgress(track) + } + + override suspend fun bind(track: Track): Track { + return track + } + + override suspend fun search(query: String): List { + TODO("Not yet implemented: search") + } + + override suspend fun refresh(track: Track): Track { + val remoteTrack = api.getTrackSearch(track.tracking_url) + track.copyPersonalFrom(remoteTrack) + track.total_chapters = remoteTrack.total_chapters + return track + } + + override suspend fun login(username: String, password: String): Boolean { + saveCredentials("user", "pass") + return true + } + + // TrackService.isLogged works by checking that credentials are saved. + // By saving dummy, unused credentials, we can activate the tracker simply by login/logout + override fun loginNoop() { + saveCredentials("user", "pass") + } + + override fun accept(source: Source): Boolean = source::class.qualifiedName == ACCEPTED_SOURCE + + override suspend fun match(manga: Manga): TrackSearch? = + try { + api.getTrackSearch(manga.url) + } catch (e: Exception) { + null + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt new file mode 100644 index 0000000000..4a6abe29fc --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaApi.kt @@ -0,0 +1,84 @@ +package eu.kanade.tachiyomi.data.track.komga + +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.model.TrackSearch +import eu.kanade.tachiyomi.network.GET +import eu.kanade.tachiyomi.network.await +import eu.kanade.tachiyomi.network.parseAs +import eu.kanade.tachiyomi.util.system.withIOContext +import kotlinx.serialization.encodeToString +import kotlinx.serialization.json.Json +import okhttp3.MediaType.Companion.toMediaType +import okhttp3.OkHttpClient +import okhttp3.Request +import okhttp3.RequestBody.Companion.toRequestBody +import timber.log.Timber +import uy.kohesive.injekt.injectLazy + +const val READLIST_API = "/api/v1/readlists" + +class KomgaApi(private val client: OkHttpClient) { + + private val json: Json by injectLazy() + + suspend fun getTrackSearch(url: String): TrackSearch = + withIOContext { + try { + val track = if (url.contains(READLIST_API)) { + client.newCall(GET(url)) + .await() + .parseAs() + .toTrack() + } else { + client.newCall(GET(url)) + .await() + .parseAs() + .toTrack() + } + + val progress = client + .newCall(GET("$url/read-progress/tachiyomi")) + .await() + .parseAs() + + track.apply { + cover_url = "$url/thumbnail" + tracking_url = url + total_chapters = progress.booksCount + status = when (progress.booksCount) { + progress.booksUnreadCount -> Komga.UNREAD + progress.booksReadCount -> Komga.COMPLETED + else -> Komga.READING + } + last_chapter_read = progress.lastReadContinuousIndex + } + } catch (e: Exception) { + Timber.w(e, "Could not get item: $url") + throw e + } + } + + suspend fun updateProgress(track: Track): Track { + val progress = ReadProgressUpdateDto(track.last_chapter_read) + val payload = json.encodeToString(progress) + client.newCall( + Request.Builder() + .url("${track.tracking_url}/read-progress/tachiyomi") + .put(payload.toRequestBody("application/json".toMediaType())) + .build() + ) + .await() + return getTrackSearch(track.tracking_url) + } + + private fun SeriesDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also { + it.title = metadata.title + it.summary = metadata.summary + it.publishing_status = metadata.status + } + + private fun ReadListDto.toTrack(): TrackSearch = TrackSearch.create(TrackManager.KOMGA).also { + it.title = name + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaModels.kt b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaModels.kt new file mode 100644 index 0000000000..d27af7f16d --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/KomgaModels.kt @@ -0,0 +1,83 @@ +package eu.kanade.tachiyomi.data.track.komga + +import kotlinx.serialization.Serializable + +@Serializable +data class SeriesDto( + val id: String, + val libraryId: String, + val name: String, + val created: String?, + val lastModified: String?, + val fileLastModified: String, + val booksCount: Int, + val booksReadCount: Int, + val booksUnreadCount: Int, + val booksInProgressCount: Int, + val metadata: SeriesMetadataDto, + val booksMetadata: BookMetadataAggregationDto +) + +@Serializable +data class SeriesMetadataDto( + val status: String, + val created: String?, + val lastModified: String?, + val title: String, + val titleSort: String, + val summary: String, + val summaryLock: Boolean, + val readingDirection: String, + val readingDirectionLock: Boolean, + val publisher: String, + val publisherLock: Boolean, + val ageRating: Int?, + val ageRatingLock: Boolean, + val language: String, + val languageLock: Boolean, + val genres: Set, + val genresLock: Boolean, + val tags: Set, + val tagsLock: Boolean +) + +@Serializable +data class BookMetadataAggregationDto( + val authors: List = emptyList(), + val releaseDate: String?, + val summary: String, + val summaryNumber: String, + + val created: String, + val lastModified: String +) + +@Serializable +data class AuthorDto( + val name: String, + val role: String +) + +@Serializable +data class ReadProgressUpdateDto( + val lastBookRead: Int, +) + +@Serializable +data class ReadListDto( + val id: String, + val name: String, + val bookIds: List, + val createdDate: String, + val lastModifiedDate: String, + val filtered: Boolean +) + +@Serializable +data class ReadProgressDto( + val booksCount: Int, + val booksReadCount: Int, + val booksUnreadCount: Int, + val booksInProgressCount: Int, + val lastReadContinuousIndex: Int, +) diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt index 35562b7311..1dafae78e0 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsController.kt @@ -1239,7 +1239,12 @@ class MangaDetailsController : updateHeader() showAddedSnack() }, - onMangaMoved = { updateHeader() }, + onMangaMoved = { + updateHeader() + if (presenter.preferences.autoAddTrack()) { + presenter.fetchChapters(andTracking = true) + } + }, onMangaDeleted = { presenter.confirmDeletion() } ) if (snack?.duration == Snackbar.LENGTH_INDEFINITE) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt index 3894db6a3b..7f2d7e79a6 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/MangaDetailsPresenter.kt @@ -30,6 +30,7 @@ import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.getOrDefault import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.UnattendedTrackService import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.SManga @@ -42,6 +43,7 @@ import eu.kanade.tachiyomi.ui.security.SecureActivityDelegate import eu.kanade.tachiyomi.util.chapter.ChapterFilter import eu.kanade.tachiyomi.util.chapter.ChapterUtil import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource +import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay import eu.kanade.tachiyomi.util.lang.trimOrNull import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.storage.DiskUtil @@ -799,6 +801,9 @@ class MangaDetailsPresenter( } if (trackItem != null) { db.insertTrack(trackItem).executeAsBlocking() + if (item.service is UnattendedTrackService) { + syncChaptersWithTrackServiceTwoWay(db, chapters, trackItem, item.service) + } trackItem } else item.track } @@ -847,7 +852,13 @@ class MangaDetailsPresenter( null } withContext(Dispatchers.IO) { - if (binding != null) db.insertTrack(binding).executeAsBlocking() + if (binding != null) { + db.insertTrack(binding).executeAsBlocking() + } + + if (service is UnattendedTrackService) { + syncChaptersWithTrackServiceTwoWay(db, chapters, item, service) + } } fetchTracks() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt index 04c60e00f5..f66900fcdb 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackHolder.kt @@ -68,8 +68,15 @@ class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) { val status = item.service.getStatus(track.status) if (status.isEmpty()) binding.trackStatus.setText(R.string.unknown_status) else binding.trackStatus.text = item.service.getStatus(track.status) - binding.trackScore.text = if (track.score == 0f) "-" else item.service.displayScore(track) - binding.trackScore.setCompoundDrawablesWithIntrinsicBounds(0, 0, starIcon(track), 0) + val supportsScoring = item.service.getScoreList().isNotEmpty() + if (supportsScoring) { + binding.trackScore.text = + if (track.score == 0f) "-" else item.service.displayScore(track) + binding.trackScore.setCompoundDrawablesWithIntrinsicBounds(0, 0, starIcon(track), 0) + } + binding.scoreContainer.isVisible = supportsScoring + binding.vertDivider2.isVisible = supportsScoring + binding.dateGroup.isVisible = item.service.supportsReadingDates if (item.service.supportsReadingDates) { binding.trackStartDate.text = diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackingBottomSheet.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackingBottomSheet.kt index 4d79a756cd..9605784173 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackingBottomSheet.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackingBottomSheet.kt @@ -11,16 +11,22 @@ import androidx.recyclerview.widget.RecyclerView import com.google.android.material.bottomsheet.BottomSheetBehavior import eu.kanade.tachiyomi.R import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.UnattendedTrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.databinding.TrackingBottomSheetBinding +import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.manga.MangaDetailsController import eu.kanade.tachiyomi.util.system.dpToPx +import eu.kanade.tachiyomi.util.system.launchIO import eu.kanade.tachiyomi.util.system.toast +import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener import eu.kanade.tachiyomi.util.view.checkHeightThen import eu.kanade.tachiyomi.util.view.updateLayoutParams import eu.kanade.tachiyomi.widget.E2EBottomSheetDialog import timber.log.Timber +import uy.kohesive.injekt.api.get +import uy.kohesive.injekt.injectLazy class TrackingBottomSheet(private val controller: MangaDetailsController) : E2EBottomSheetDialog(controller.activity!!), @@ -35,6 +41,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) : val presenter = controller.presenter + private val sourceManager: SourceManager by injectLazy() private var adapter: TrackAdapter? = null override fun createBinding(inflater: LayoutInflater) = @@ -140,10 +147,31 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) : return } - TrackSearchDialog(this, item.service, item.track != null).showDialog( - controller.router, - TAG_SEARCH_CONTROLLER - ) + if (item.service is UnattendedTrackService) { + if (item.track != null) { + controller.presenter.removeTracker(item, false) + return + } + + if (!item.service.accept(controller.presenter.source)) { + controller.view?.context?.toast(R.string.source_unsupported) + return + } + + launchIO { + try { + item.service.match(controller.presenter.manga)?.let { track -> + controller.presenter.registerTracking(track, item.service) + } + ?: withUIContext { controller.view?.context?.toast(R.string.no_match_found) } + } catch (e: Exception) { + withUIContext { controller.view?.context?.toast(R.string.no_match_found) } + } + } + } else { + TrackSearchDialog(this, item.service, item.track != null) + .showDialog(controller.router, TAG_SEARCH_CONTROLLER) + } } override fun onStatusClick(position: Int) { diff --git a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt index 4c55d167ce..5ce59cc282 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/setting/SettingsTrackingController.kt @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.setting import android.app.Activity import androidx.preference.PreferenceScreen import eu.kanade.tachiyomi.R +import eu.kanade.tachiyomi.data.track.NoLoginTrackService import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.anilist.AnilistApi @@ -31,6 +32,12 @@ class SettingsTrackingController : titleRes = R.string.sync_chapters_after_reading defaultValue = true } + switchPreference { + key = Keys.autoAddTrack + titleRes = R.string.track_when_adding_to_library + summaryRes = R.string.only_applies_silent_trackers + defaultValue = true + } preferenceCategory { titleRes = R.string.services @@ -51,6 +58,10 @@ class SettingsTrackingController : trackPreference(trackManager.bangumi) { activity?.openInBrowser(BangumiApi.authUrl(), trackManager.bangumi.getLogoColor()) } + trackPreference(trackManager.komga) { + trackManager.komga.loginNoop() + updatePreference(trackManager.komga.id) + } } } @@ -66,9 +77,14 @@ class SettingsTrackingController : { onClick { if (service.isLogged) { - val dialog = TrackLogoutDialog(service) - dialog.targetController = this@SettingsTrackingController - dialog.showDialog(router) + if (service is NoLoginTrackService) { + service.logout() + updatePreference(service.id) + } else { + val dialog = TrackLogoutDialog(service) + dialog.targetController = this@SettingsTrackingController + dialog.showDialog(router) + } } else { login() } diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt index 59ac1276cf..f7ceea3533 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/util/MangaExtensions.kt @@ -10,9 +10,19 @@ import eu.kanade.tachiyomi.data.database.models.Category import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.preference.PreferencesHelper +import eu.kanade.tachiyomi.data.track.TrackManager +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.data.track.UnattendedTrackService import eu.kanade.tachiyomi.source.LocalSource +import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.category.addtolibrary.SetCategoriesSheet +import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay +import eu.kanade.tachiyomi.util.system.launchIO +import eu.kanade.tachiyomi.util.system.withUIContext import eu.kanade.tachiyomi.util.view.snack +import timber.log.Timber +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get import java.util.Date fun Manga.isLocal() = source == LocalSource.ID @@ -95,6 +105,9 @@ fun Manga.addOrRemoveToFavorites( defaultCategory != null -> { favorite = true date_added = Date().time + if (preferences.autoAddTrack()) { + autoAddTrack(db, onMangaMoved) + } db.insertManga(this).executeAsBlocking() val mc = MangaCategory.create(this, defaultCategory) db.setMangaCategories(listOf(mc), listOf(this)) @@ -108,6 +121,9 @@ fun Manga.addOrRemoveToFavorites( defaultCategoryId == 0 || categories.isEmpty() -> { // 'Default' or no category favorite = true date_added = Date().time + if (preferences.autoAddTrack()) { + autoAddTrack(db, onMangaMoved) + } db.insertManga(this).executeAsBlocking() db.setMangaCategories(emptyList(), listOf(this)) onMangaMoved() @@ -133,6 +149,9 @@ fun Manga.addOrRemoveToFavorites( true ) { onMangaAdded() + if (preferences.autoAddTrack()) { + autoAddTrack(db, onMangaMoved) + } }.show() } } @@ -163,3 +182,29 @@ fun Manga.addOrRemoveToFavorites( } return null } + +fun Manga.autoAddTrack(db: DatabaseHelper, onMangaMoved: () -> Unit) { + val loggedServices = Injekt.get().services.filter { it.isLogged } + val source = Injekt.get().getOrStub(this.source) + loggedServices + .filterIsInstance() + .filter { it.accept(source) } + .forEach { service -> + launchIO { + try { + service.match(this@autoAddTrack)?.let { track -> + track.manga_id = this@autoAddTrack.id!! + (service as TrackService).bind(track) + db.insertTrack(track).executeAsBlocking() + + syncChaptersWithTrackServiceTwoWay(db, db.getChapters(this@autoAddTrack).executeAsBlocking(), track, service as TrackService) + withUIContext { + onMangaMoved() + } + } + } catch (e: Exception) { + Timber.w(e, "Could not match manga: ${this@autoAddTrack.title} with service $service") + } + } + } +} diff --git a/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterTrackSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterTrackSync.kt new file mode 100644 index 0000000000..35906a5ed6 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterTrackSync.kt @@ -0,0 +1,42 @@ +package eu.kanade.tachiyomi.util.chapter + +import eu.kanade.tachiyomi.data.database.DatabaseHelper +import eu.kanade.tachiyomi.data.database.models.Chapter +import eu.kanade.tachiyomi.data.database.models.Track +import eu.kanade.tachiyomi.data.track.TrackService +import eu.kanade.tachiyomi.util.system.launchIO +import timber.log.Timber + +/** + * Helper method for syncing a remote track with the local chapters, and back + * + * @param db the database. + * @param chapters a list of chapters from the source. + * @param remoteTrack the remote Track object. + * @param service the tracker service. + */ +fun syncChaptersWithTrackServiceTwoWay(db: DatabaseHelper, chapters: List, remoteTrack: Track, service: TrackService) { + val sortedChapters = chapters.sortedBy { it.chapter_number } + sortedChapters + .filterIndexed { index, chapter -> index < remoteTrack.last_chapter_read && !chapter.read } + .forEach { it.read = true } + db.updateChaptersProgress(sortedChapters).executeAsBlocking() + + val localLastRead = when { + sortedChapters.all { it.read } -> sortedChapters.size + sortedChapters.any { !it.read } -> sortedChapters.indexOfFirst { !it.read } + else -> 0 + } + + // update remote + remoteTrack.last_chapter_read = localLastRead + + launchIO { + try { + service.update(remoteTrack) + db.insertTrack(remoteTrack).executeAsBlocking() + } catch (e: Throwable) { + Timber.w(e) + } + } +} diff --git a/app/src/main/res/drawable/ic_tracker_komga.webp b/app/src/main/res/drawable/ic_tracker_komga.webp new file mode 100644 index 0000000000000000000000000000000000000000..ecfaa4ec43e69ad4797ccaab8b4f298d6f6fbe0d GIT binary patch literal 15834 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 literal 0 HcmV?d00001 diff --git a/app/src/main/res/layout/track_item.xml b/app/src/main/res/layout/track_item.xml index 725f2dc254..bb976c5794 100644 --- a/app/src/main/res/layout/track_item.xml +++ b/app/src/main/res/layout/track_item.xml @@ -180,6 +180,7 @@ app:layout_constraintTop_toTopOf="parent" /> + app:layout_constraintTop_toBottomOf="@+id/track_chapters" /> Not tracked Services Sync chapters after reading + Track when adding to library + Only applies to silent trackers, such as Komga Reading Currently Reading Dropped @@ -532,6 +534,8 @@ Manga URL not set, please click title and select manga again Refresh tracking Add tracking + Source is not supported + No match found Remove tracking from app Also remove from %1$s AniList @@ -539,6 +543,7 @@ Kitsu Bangumi Shikimori + Komga Started reading date Finished reading date Please login to MAL again