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 86a2b81a12..0da101b717 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 @@ -21,12 +21,14 @@ import eu.kanade.tachiyomi.data.library.LibraryUpdateService.Companion.start import eu.kanade.tachiyomi.data.notification.Notifications import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.util.chapter.NoChaptersException import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource +import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay import eu.kanade.tachiyomi.util.prepUpdateCover import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.storage.getUriCompat @@ -416,6 +418,10 @@ class LibraryUpdateService( try { val updatedTrack = service.refresh(track) db.insertTrack(updatedTrack).executeAsBlocking() + + if (service is UnattendedTrackService) { + syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service) + } } catch (e: Throwable) { // Ignore errors and continue 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 7b24759fde..0caa65365b 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 @@ -97,6 +97,8 @@ object PreferenceKeys { const val autoUpdateTrack = "pref_auto_update_manga_sync_key" + const val autoAddTrack = "pref_auto_add_track_key" + const val lastUsedSource = "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 cdfc3516db..f685fd6c7d 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 @@ -171,6 +171,8 @@ class PreferencesHelper(val context: Context) { fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true) + fun autoAddTrack() = prefs.getBoolean(Keys.autoAddTrack, true) + fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1) fun lastUsedCategory() = flowPrefs.getInt(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..2c76c69987 --- /dev/null +++ b/app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt @@ -0,0 +1,99 @@ +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.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.tracker_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 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 getCompletionStatus(): Int = COMPLETED + + override fun getScoreList(): List = emptyList() + + override fun displayScore(track: Track): String = "" + + override suspend fun add(track: Track): Track { + TODO("Not yet implemented: add") + } + + override suspend fun update(track: Track): Track { + 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) { + saveCredentials("user", "pass") + } + + // 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..c901580c3e --- /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.lang.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/browse/source/browse/BrowseSourcePresenter.kt b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt index 4ae85e1182..a4646b7eb1 100644 --- a/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt +++ b/app/src/main/java/eu/kanade/tachiyomi/ui/browse/source/browse/BrowseSourcePresenter.kt @@ -9,6 +9,9 @@ import eu.kanade.tachiyomi.data.database.models.Manga import eu.kanade.tachiyomi.data.database.models.MangaCategory import eu.kanade.tachiyomi.data.database.models.toMangaInfo 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.CatalogueSource import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.model.Filter @@ -30,6 +33,7 @@ import eu.kanade.tachiyomi.ui.browse.source.filter.TextSectionItem import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateItem import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper +import eu.kanade.tachiyomi.util.chapter.syncChaptersWithTrackServiceTwoWay import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.removeCovers @@ -102,6 +106,8 @@ open class BrowseSourcePresenter( */ private var pageSubscription: Subscription? = null + private val loggedServices by lazy { Injekt.get().services.filter { it.isLogged } } + init { query = searchQuery ?: "" } @@ -260,11 +266,36 @@ open class BrowseSourcePresenter( manga.removeCovers(coverCache) } else { ChapterSettingsHelper.applySettingDefaults(manga) + + if (prefs.autoAddTrack()) { + autoAddTrack(manga) + } } db.insertManga(manga).executeAsBlocking() } + private fun autoAddTrack(manga: Manga) { + loggedServices + .filterIsInstance() + .filter { it.accept(source) } + .forEach { service -> + launchIO { + try { + service.match(manga)?.let { track -> + track.manga_id = manga.id!! + (service as TrackService).bind(track) + db.insertTrack(track).executeAsBlocking() + + syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service as TrackService) + } + } catch (e: Exception) { + Timber.w(e, "Could not match manga: ${manga.title} with service $service") + } + } + } + } + /** * Set the filter states for the current source. * 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 8be864f0af..c0209f3528 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,8 @@ 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.TrackService +import eu.kanade.tachiyomi.data.track.UnattendedTrackService import eu.kanade.tachiyomi.data.track.model.TrackSearch import eu.kanade.tachiyomi.databinding.MangaControllerBinding import eu.kanade.tachiyomi.source.LocalSource @@ -72,6 +74,7 @@ import eu.kanade.tachiyomi.ui.recent.updates.UpdatesController import eu.kanade.tachiyomi.ui.webview.WebViewActivity import eu.kanade.tachiyomi.util.chapter.NoChaptersException import eu.kanade.tachiyomi.util.hasCustomCover +import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchUI import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.toast @@ -507,6 +510,24 @@ class MangaController : .showDialog(router) } } + + if (source != null && preferences.autoAddTrack()) { + presenter.trackList + .map { it.service } + .filterIsInstance() + .filter { it.accept(source!!) } + .forEach { service -> + launchIO { + try { + service.match(manga)?.let { track -> + presenter.registerTracking(track, service as TrackService) + } + } catch (e: Exception) { + Timber.w(e, "Could not match manga: ${manga.title} with service $service") + } + } + } + } } /** 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 8e1a6b8d30..87c6452d7d 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 @@ -17,6 +17,7 @@ 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.data.track.UnattendedTrackService import eu.kanade.tachiyomi.source.LocalSource import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.model.toSChapter @@ -26,6 +27,7 @@ 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.chapter.syncChaptersWithTrackServiceTwoWay import eu.kanade.tachiyomi.util.isLocal import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.withUIContext @@ -709,6 +711,10 @@ class MangaPresenter( async { val track = it.service.refresh(it.track!!) db.insertTrack(track).executeAsBlocking() + + if (it.service is UnattendedTrackService) { + syncChaptersWithTrackServiceTwoWay(db, chapters, track, it.service) + } } } .awaitAll() @@ -740,6 +746,10 @@ class MangaPresenter( try { service.bind(item) db.insertTrack(item).executeAsBlocking() + + if (service is UnattendedTrackService) { + syncChaptersWithTrackServiceTwoWay(db, chapters, item, service) + } } catch (e: Throwable) { withUIContext { view?.applicationContext?.toast(e.message) } } 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 012ab95e18..2afc1ba3f4 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 @@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.manga.track import android.annotation.SuppressLint import androidx.core.view.isVisible import androidx.recyclerview.widget.RecyclerView +import eu.kanade.tachiyomi.R.string import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.databinding.TrackItemBinding import uy.kohesive.injekt.injectLazy @@ -49,6 +50,12 @@ class TrackHolder(private val binding: TrackItemBinding, adapter: TrackAdapter) if (track.total_chapters > 0) track.total_chapters else "-" binding.trackStatus.text = item.service.getStatus(track.status) binding.trackScore.text = if (track.score == 0f) "-" else item.service.displayScore(track) + if (item.service.getScoreList().isEmpty()) { + with(binding.trackScore) { + text = context.getString(string.score_unsupported) + isEnabled = false + } + } if (item.service.supportsReadingDates) { binding.trackStartDate.text = 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 index 2099ca5726..5c2b117c8f 100644 --- 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 @@ -5,16 +5,25 @@ import android.view.LayoutInflater import android.view.View import androidx.recyclerview.widget.LinearLayoutManager import com.google.android.material.bottomsheet.BottomSheetBehavior +import eu.kanade.tachiyomi.R.string import eu.kanade.tachiyomi.data.database.models.Manga +import eu.kanade.tachiyomi.data.track.UnattendedTrackService import eu.kanade.tachiyomi.databinding.TrackControllerBinding +import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.ui.base.controller.openInBrowser import eu.kanade.tachiyomi.ui.manga.MangaController +import eu.kanade.tachiyomi.util.lang.launchIO +import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.system.copyToClipboard +import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog +import uy.kohesive.injekt.Injekt +import uy.kohesive.injekt.api.get class TrackSheet( val controller: MangaController, - val manga: Manga + val manga: Manga, + private val sourceManager: SourceManager = Injekt.get() ) : BaseBottomSheetDialog(controller.activity!!), TrackAdapter.OnClickListener, SetTrackStatusDialog.Listener, @@ -69,7 +78,31 @@ class TrackSheet( override fun onSetClick(position: Int) { val item = adapter.getItem(position) ?: return - TrackSearchDialog(controller, item.service).showDialog(controller.router, TAG_SEARCH_CONTROLLER) + + if (item.service is UnattendedTrackService) { + if (item.track != null) { + controller.presenter.unregisterTracking(item.service) + return + } + + if (!item.service.accept(sourceManager.getOrStub(manga.source))) { + controller.presenter.view?.applicationContext?.toast(string.source_unsupported) + return + } + + launchIO { + try { + item.service.match(manga)?.let { track -> + controller.presenter.registerTracking(track, item.service) + } + ?: withUIContext { controller.presenter.view?.applicationContext?.toast(string.error_no_match) } + } catch (e: Exception) { + withUIContext { controller.presenter.view?.applicationContext?.toast(string.error_no_match) } + } + } + } else { + TrackSearchDialog(controller, item.service).showDialog(controller.router, TAG_SEARCH_CONTROLLER) + } } override fun onTitleLongClick(position: Int) { @@ -94,7 +127,7 @@ class TrackSheet( override fun onScoreClick(position: Int) { val item = adapter.getItem(position) ?: return - if (item.track == null) return + if (item.track == null || item.service.getScoreList().isEmpty()) return SetTrackScoreDialog(controller, this, item).showDialog(controller.router) } 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 950d2dc601..20263fe712 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 @@ -38,6 +39,11 @@ class SettingsTrackingController : titleRes = R.string.pref_auto_update_manga_sync defaultValue = true } + switchPreference { + key = Keys.autoAddTrack + titleRes = R.string.pref_auto_add_track + defaultValue = true + } preferenceCategory { titleRes = R.string.services @@ -58,6 +64,10 @@ class SettingsTrackingController : trackPreference(trackManager.bangumi) { activity?.openInBrowser(BangumiApi.authUrl(), trackManager.bangumi.getLogoColor()) } + trackPreference(trackManager.komga) { + trackManager.komga.loginNoop() + updatePreference(trackManager.komga.id) + } } preferenceCategory { infoPreference(R.string.tracking_info) @@ -76,9 +86,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/chapter/ChapterTrackSync.kt b/app/src/main/java/eu/kanade/tachiyomi/util/chapter/ChapterTrackSync.kt new file mode 100644 index 0000000000..4cd66d2b58 --- /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.lang.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-xhdpi/ic_tracker_komga.webp b/app/src/main/res/drawable-xhdpi/ic_tracker_komga.webp new file mode 100644 index 0000000000..90c4ea1175 Binary files /dev/null and b/app/src/main/res/drawable-xhdpi/ic_tracker_komga.webp differ diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 85b808f09b..32ba77e700 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -376,6 +376,7 @@ Update chapter progress after reading + Track silently when adding manga to library Services One-way sync to update the chapter progress in tracking services. Set up tracking for individual manga entries from their tracking button. @@ -581,6 +582,7 @@ AniList MyAnimeList Kitsu + Komga Bangumi Shikimori Tracking @@ -589,6 +591,7 @@ %d trackers Add tracking + Unread Reading Currently reading Completed @@ -610,6 +613,9 @@ Invalid date supplied MAL login credentials not found Please login to MAL again + Not supported + Source is not supported + No match found A category with this name already exists!