Add Komga as an unattended track service (#5049)

* fix: prevent crash if TrackService.getScoreList() is empty

* disabled track score button if service doesn't support scoring

* first implementation of the Komga tracking
this doesn't work for read lists

* auto track when adding to library

* handle refresh

* 2-way sync of chapters for unattended tracking services

* Update app/src/main/java/eu/kanade/tachiyomi/ui/manga/track/TrackSheet.kt

Co-authored-by: Andreas <andreas.everos@gmail.com>

* group strings together

* support for read lists

* sync read chapters on bind

* only mark local chapters as read during 2-way sync (incoming)

* local progress from read chapters will be sent to remote tracker on bind/refresh
this enables syncing after reading offline

* remove unused variable

* refactor the 2-way sync in a util function

* handle auto add to track for unattended services from the browse source screen when long clicking
this will also sync chapters, as it is possible to have read or marked as read chapters from there

* 2-way sync when library update for TRACKING

* refactor

* better handling of what has been read server side

* refactor: extract function

* fix: localLastRead could be -1 when all chapters are read

* refactor to rethrow exception so it can be shown in toast

* extract strings

* replace komga logo

Co-authored-by: Andreas <andreas.everos@gmail.com>
This commit is contained in:
Gauthier 2021-05-23 00:07:58 +08:00 committed by GitHub
parent dbe8931cf0
commit d6b3b0baf7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 481 additions and 7 deletions

View file

@ -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.notification.Notifications
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager 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.SourceManager
import eu.kanade.tachiyomi.source.model.SManga import eu.kanade.tachiyomi.source.model.SManga
import eu.kanade.tachiyomi.source.model.toSChapter import eu.kanade.tachiyomi.source.model.toSChapter
import eu.kanade.tachiyomi.source.model.toSManga import eu.kanade.tachiyomi.source.model.toSManga
import eu.kanade.tachiyomi.util.chapter.NoChaptersException import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource 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.prepUpdateCover
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
import eu.kanade.tachiyomi.util.storage.getUriCompat import eu.kanade.tachiyomi.util.storage.getUriCompat
@ -416,6 +418,10 @@ class LibraryUpdateService(
try { try {
val updatedTrack = service.refresh(track) val updatedTrack = service.refresh(track)
db.insertTrack(updatedTrack).executeAsBlocking() db.insertTrack(updatedTrack).executeAsBlocking()
if (service is UnattendedTrackService) {
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
}
} catch (e: Throwable) { } catch (e: Throwable) {
// Ignore errors and continue // Ignore errors and continue
Timber.e(e) Timber.e(e)

View file

@ -97,6 +97,8 @@ object PreferenceKeys {
const val autoUpdateTrack = "pref_auto_update_manga_sync_key" 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 lastUsedSource = "last_catalogue_source"
const val lastUsedCategory = "last_used_category" const val lastUsedCategory = "last_used_category"

View file

@ -171,6 +171,8 @@ class PreferencesHelper(val context: Context) {
fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true) fun autoUpdateTrack() = prefs.getBoolean(Keys.autoUpdateTrack, true)
fun autoAddTrack() = prefs.getBoolean(Keys.autoAddTrack, true)
fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1) fun lastUsedSource() = flowPrefs.getLong(Keys.lastUsedSource, -1)
fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0) fun lastUsedCategory() = flowPrefs.getInt(Keys.lastUsedCategory, 0)

View file

@ -0,0 +1,8 @@
package eu.kanade.tachiyomi.data.track
/**
* A TrackService that doesn't need explicit login.
*/
interface NoLoginTrackService {
fun loginNoop()
}

View file

@ -4,6 +4,7 @@ import android.content.Context
import eu.kanade.tachiyomi.data.track.anilist.Anilist import eu.kanade.tachiyomi.data.track.anilist.Anilist
import eu.kanade.tachiyomi.data.track.bangumi.Bangumi import eu.kanade.tachiyomi.data.track.bangumi.Bangumi
import eu.kanade.tachiyomi.data.track.kitsu.Kitsu 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.myanimelist.MyAnimeList
import eu.kanade.tachiyomi.data.track.shikimori.Shikimori import eu.kanade.tachiyomi.data.track.shikimori.Shikimori
@ -15,6 +16,7 @@ class TrackManager(context: Context) {
const val KITSU = 3 const val KITSU = 3
const val SHIKIMORI = 4 const val SHIKIMORI = 4
const val BANGUMI = 5 const val BANGUMI = 5
const val KOMGA = 6
} }
val myAnimeList = MyAnimeList(context, MYANIMELIST) val myAnimeList = MyAnimeList(context, MYANIMELIST)
@ -27,7 +29,9 @@ class TrackManager(context: Context) {
val bangumi = Bangumi(context, BANGUMI) 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 } fun getService(id: Int) = services.find { it.id == id }

View file

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

View file

@ -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<String> = 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<TrackSearch> {
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
}
}

View file

@ -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<ReadListDto>()
.toTrack()
} else {
client.newCall(GET(url))
.await()
.parseAs<SeriesDto>()
.toTrack()
}
val progress = client
.newCall(GET("$url/read-progress/tachiyomi"))
.await()
.parseAs<ReadProgressDto>()
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
}
}

View file

@ -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<String>,
val genresLock: Boolean,
val tags: Set<String>,
val tagsLock: Boolean
)
@Serializable
data class BookMetadataAggregationDto(
val authors: List<AuthorDto> = 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<String>,
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,
)

View file

@ -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.MangaCategory
import eu.kanade.tachiyomi.data.database.models.toMangaInfo import eu.kanade.tachiyomi.data.database.models.toMangaInfo
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.CatalogueSource
import eu.kanade.tachiyomi.source.SourceManager import eu.kanade.tachiyomi.source.SourceManager
import eu.kanade.tachiyomi.source.model.Filter 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.TriStateItem
import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem import eu.kanade.tachiyomi.ui.browse.source.filter.TriStateSectionItem
import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper 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.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.lang.withUIContext
import eu.kanade.tachiyomi.util.removeCovers import eu.kanade.tachiyomi.util.removeCovers
@ -102,6 +106,8 @@ open class BrowseSourcePresenter(
*/ */
private var pageSubscription: Subscription? = null private var pageSubscription: Subscription? = null
private val loggedServices by lazy { Injekt.get<TrackManager>().services.filter { it.isLogged } }
init { init {
query = searchQuery ?: "" query = searchQuery ?: ""
} }
@ -260,11 +266,36 @@ open class BrowseSourcePresenter(
manga.removeCovers(coverCache) manga.removeCovers(coverCache)
} else { } else {
ChapterSettingsHelper.applySettingDefaults(manga) ChapterSettingsHelper.applySettingDefaults(manga)
if (prefs.autoAddTrack()) {
autoAddTrack(manga)
}
} }
db.insertManga(manga).executeAsBlocking() db.insertManga(manga).executeAsBlocking()
} }
private fun autoAddTrack(manga: Manga) {
loggedServices
.filterIsInstance<UnattendedTrackService>()
.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. * Set the filter states for the current source.
* *

View file

@ -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.DownloadService
import eu.kanade.tachiyomi.data.download.model.Download import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.preference.PreferencesHelper 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.data.track.model.TrackSearch
import eu.kanade.tachiyomi.databinding.MangaControllerBinding import eu.kanade.tachiyomi.databinding.MangaControllerBinding
import eu.kanade.tachiyomi.source.LocalSource 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.ui.webview.WebViewActivity
import eu.kanade.tachiyomi.util.chapter.NoChaptersException import eu.kanade.tachiyomi.util.chapter.NoChaptersException
import eu.kanade.tachiyomi.util.hasCustomCover 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.lang.launchUI
import eu.kanade.tachiyomi.util.system.getResourceColor import eu.kanade.tachiyomi.util.system.getResourceColor
import eu.kanade.tachiyomi.util.system.toast import eu.kanade.tachiyomi.util.system.toast
@ -507,6 +510,24 @@ class MangaController :
.showDialog(router) .showDialog(router)
} }
} }
if (source != null && preferences.autoAddTrack()) {
presenter.trackList
.map { it.service }
.filterIsInstance<UnattendedTrackService>()
.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")
}
}
}
}
} }
/** /**

View file

@ -17,6 +17,7 @@ import eu.kanade.tachiyomi.data.download.model.Download
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.data.track.TrackManager import eu.kanade.tachiyomi.data.track.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService 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.LocalSource
import eu.kanade.tachiyomi.source.Source import eu.kanade.tachiyomi.source.Source
import eu.kanade.tachiyomi.source.model.toSChapter 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.ui.manga.track.TrackItem
import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper import eu.kanade.tachiyomi.util.chapter.ChapterSettingsHelper
import eu.kanade.tachiyomi.util.chapter.syncChaptersWithSource 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.isLocal
import eu.kanade.tachiyomi.util.lang.launchIO import eu.kanade.tachiyomi.util.lang.launchIO
import eu.kanade.tachiyomi.util.lang.withUIContext import eu.kanade.tachiyomi.util.lang.withUIContext
@ -709,6 +711,10 @@ class MangaPresenter(
async { async {
val track = it.service.refresh(it.track!!) val track = it.service.refresh(it.track!!)
db.insertTrack(track).executeAsBlocking() db.insertTrack(track).executeAsBlocking()
if (it.service is UnattendedTrackService) {
syncChaptersWithTrackServiceTwoWay(db, chapters, track, it.service)
}
} }
} }
.awaitAll() .awaitAll()
@ -740,6 +746,10 @@ class MangaPresenter(
try { try {
service.bind(item) service.bind(item)
db.insertTrack(item).executeAsBlocking() db.insertTrack(item).executeAsBlocking()
if (service is UnattendedTrackService) {
syncChaptersWithTrackServiceTwoWay(db, chapters, item, service)
}
} catch (e: Throwable) { } catch (e: Throwable) {
withUIContext { view?.applicationContext?.toast(e.message) } withUIContext { view?.applicationContext?.toast(e.message) }
} }

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.manga.track
import android.annotation.SuppressLint import android.annotation.SuppressLint
import androidx.core.view.isVisible import androidx.core.view.isVisible
import androidx.recyclerview.widget.RecyclerView import androidx.recyclerview.widget.RecyclerView
import eu.kanade.tachiyomi.R.string
import eu.kanade.tachiyomi.data.preference.PreferencesHelper import eu.kanade.tachiyomi.data.preference.PreferencesHelper
import eu.kanade.tachiyomi.databinding.TrackItemBinding import eu.kanade.tachiyomi.databinding.TrackItemBinding
import uy.kohesive.injekt.injectLazy 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 "-" if (track.total_chapters > 0) track.total_chapters else "-"
binding.trackStatus.text = item.service.getStatus(track.status) binding.trackStatus.text = item.service.getStatus(track.status)
binding.trackScore.text = if (track.score == 0f) "-" else item.service.displayScore(track) 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) { if (item.service.supportsReadingDates) {
binding.trackStartDate.text = binding.trackStartDate.text =

View file

@ -5,16 +5,25 @@ import android.view.LayoutInflater
import android.view.View import android.view.View
import androidx.recyclerview.widget.LinearLayoutManager import androidx.recyclerview.widget.LinearLayoutManager
import com.google.android.material.bottomsheet.BottomSheetBehavior 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.database.models.Manga
import eu.kanade.tachiyomi.data.track.UnattendedTrackService
import eu.kanade.tachiyomi.databinding.TrackControllerBinding 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.base.controller.openInBrowser
import eu.kanade.tachiyomi.ui.manga.MangaController 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.copyToClipboard
import eu.kanade.tachiyomi.util.system.toast
import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog import eu.kanade.tachiyomi.widget.sheet.BaseBottomSheetDialog
import uy.kohesive.injekt.Injekt
import uy.kohesive.injekt.api.get
class TrackSheet( class TrackSheet(
val controller: MangaController, val controller: MangaController,
val manga: Manga val manga: Manga,
private val sourceManager: SourceManager = Injekt.get()
) : BaseBottomSheetDialog(controller.activity!!), ) : BaseBottomSheetDialog(controller.activity!!),
TrackAdapter.OnClickListener, TrackAdapter.OnClickListener,
SetTrackStatusDialog.Listener, SetTrackStatusDialog.Listener,
@ -69,8 +78,32 @@ class TrackSheet(
override fun onSetClick(position: Int) { override fun onSetClick(position: Int) {
val item = adapter.getItem(position) ?: return val item = adapter.getItem(position) ?: return
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) TrackSearchDialog(controller, item.service).showDialog(controller.router, TAG_SEARCH_CONTROLLER)
} }
}
override fun onTitleLongClick(position: Int) { override fun onTitleLongClick(position: Int) {
adapter.getItem(position)?.track?.title?.let { adapter.getItem(position)?.track?.title?.let {
@ -94,7 +127,7 @@ class TrackSheet(
override fun onScoreClick(position: Int) { override fun onScoreClick(position: Int) {
val item = adapter.getItem(position) ?: return 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) SetTrackScoreDialog(controller, this, item).showDialog(controller.router)
} }

View file

@ -3,6 +3,7 @@ package eu.kanade.tachiyomi.ui.setting
import android.app.Activity import android.app.Activity
import androidx.preference.PreferenceScreen import androidx.preference.PreferenceScreen
import eu.kanade.tachiyomi.R 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.TrackManager
import eu.kanade.tachiyomi.data.track.TrackService import eu.kanade.tachiyomi.data.track.TrackService
import eu.kanade.tachiyomi.data.track.anilist.AnilistApi import eu.kanade.tachiyomi.data.track.anilist.AnilistApi
@ -38,6 +39,11 @@ class SettingsTrackingController :
titleRes = R.string.pref_auto_update_manga_sync titleRes = R.string.pref_auto_update_manga_sync
defaultValue = true defaultValue = true
} }
switchPreference {
key = Keys.autoAddTrack
titleRes = R.string.pref_auto_add_track
defaultValue = true
}
preferenceCategory { preferenceCategory {
titleRes = R.string.services titleRes = R.string.services
@ -58,6 +64,10 @@ class SettingsTrackingController :
trackPreference(trackManager.bangumi) { trackPreference(trackManager.bangumi) {
activity?.openInBrowser(BangumiApi.authUrl(), trackManager.bangumi.getLogoColor()) activity?.openInBrowser(BangumiApi.authUrl(), trackManager.bangumi.getLogoColor())
} }
trackPreference(trackManager.komga) {
trackManager.komga.loginNoop()
updatePreference(trackManager.komga.id)
}
} }
preferenceCategory { preferenceCategory {
infoPreference(R.string.tracking_info) infoPreference(R.string.tracking_info)
@ -76,9 +86,14 @@ class SettingsTrackingController :
{ {
onClick { onClick {
if (service.isLogged) { if (service.isLogged) {
if (service is NoLoginTrackService) {
service.logout()
updatePreference(service.id)
} else {
val dialog = TrackLogoutDialog(service) val dialog = TrackLogoutDialog(service)
dialog.targetController = this@SettingsTrackingController dialog.targetController = this@SettingsTrackingController
dialog.showDialog(router) dialog.showDialog(router)
}
} else { } else {
login() login()
} }

View file

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 76 KiB

View file

@ -376,6 +376,7 @@
<!-- Tracking section --> <!-- Tracking section -->
<string name="pref_auto_update_manga_sync">Update chapter progress after reading</string> <string name="pref_auto_update_manga_sync">Update chapter progress after reading</string>
<string name="pref_auto_add_track">Track silently when adding manga to library</string>
<string name="services">Services</string> <string name="services">Services</string>
<string name="tracking_info">One-way sync to update the chapter progress in tracking services. Set up tracking for individual manga entries from their tracking button.</string> <string name="tracking_info">One-way sync to update the chapter progress in tracking services. Set up tracking for individual manga entries from their tracking button.</string>
@ -581,6 +582,7 @@
<string name="tracker_anilist" translatable="false">AniList</string> <string name="tracker_anilist" translatable="false">AniList</string>
<string name="tracker_myanimelist" translatable="false">MyAnimeList</string> <string name="tracker_myanimelist" translatable="false">MyAnimeList</string>
<string name="tracker_kitsu" translatable="false">Kitsu</string> <string name="tracker_kitsu" translatable="false">Kitsu</string>
<string name="tracker_komga" translatable="false">Komga</string>
<string name="tracker_bangumi" translatable="false">Bangumi</string> <string name="tracker_bangumi" translatable="false">Bangumi</string>
<string name="tracker_shikimori" translatable="false">Shikimori</string> <string name="tracker_shikimori" translatable="false">Shikimori</string>
<string name="manga_tracking_tab">Tracking</string> <string name="manga_tracking_tab">Tracking</string>
@ -589,6 +591,7 @@
<item quantity="other">%d trackers</item> <item quantity="other">%d trackers</item>
</plurals> </plurals>
<string name="add_tracking">Add tracking</string> <string name="add_tracking">Add tracking</string>
<string name="unread">Unread</string>
<string name="reading">Reading</string> <string name="reading">Reading</string>
<string name="currently_reading">Currently reading</string> <string name="currently_reading">Currently reading</string>
<string name="completed">Completed</string> <string name="completed">Completed</string>
@ -610,6 +613,9 @@
<string name="error_invalid_date_supplied">Invalid date supplied</string> <string name="error_invalid_date_supplied">Invalid date supplied</string>
<string name="myanimelist_creds_missing">MAL login credentials not found</string> <string name="myanimelist_creds_missing">MAL login credentials not found</string>
<string name="myanimelist_relogin">Please login to MAL again</string> <string name="myanimelist_relogin">Please login to MAL again</string>
<string name="score_unsupported">Not supported</string>
<string name="source_unsupported">Source is not supported</string>
<string name="error_no_match">No match found</string>
<!-- Category activity --> <!-- Category activity -->
<string name="error_category_exists">A category with this name already exists!</string> <string name="error_category_exists">A category with this name already exists!</string>