mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-20 04:09:18 +01:00
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>
This commit is contained in:
parent
dfe125fa5f
commit
6717f7dd3b
@ -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)
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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)
|
||||
|
@ -0,0 +1,8 @@
|
||||
package eu.kanade.tachiyomi.data.track
|
||||
|
||||
/**
|
||||
* A TrackService that doesn't need explicit login.
|
||||
*/
|
||||
interface NoLoginTrackService {
|
||||
fun loginNoop()
|
||||
}
|
@ -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 }
|
||||
|
||||
|
@ -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?
|
||||
}
|
116
app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt
Normal file
116
app/src/main/java/eu/kanade/tachiyomi/data/track/komga/Komga.kt
Normal file
@ -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<String> = 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<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): 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
|
||||
}
|
||||
}
|
@ -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<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
|
||||
}
|
||||
}
|
@ -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,
|
||||
)
|
@ -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) {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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 =
|
||||
|
@ -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<TrackingBottomSheetBinding>(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) {
|
||||
|
@ -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()
|
||||
}
|
||||
|
@ -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<TrackManager>().services.filter { it.isLogged }
|
||||
val source = Injekt.get<SourceManager>().getOrStub(this.source)
|
||||
loggedServices
|
||||
.filterIsInstance<UnattendedTrackService>()
|
||||
.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")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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<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)
|
||||
}
|
||||
}
|
||||
}
|
BIN
app/src/main/res/drawable/ic_tracker_komga.webp
Normal file
BIN
app/src/main/res/drawable/ic_tracker_komga.webp
Normal file
Binary file not shown.
After Width: | Height: | Size: 16 KiB |
@ -180,6 +180,7 @@
|
||||
app:layout_constraintTop_toTopOf="parent" />
|
||||
|
||||
<View
|
||||
android:id="@+id/vert_divider_2"
|
||||
android:layout_width="1dp"
|
||||
android:layout_height="0dp"
|
||||
android:layout_marginTop="10dp"
|
||||
@ -199,7 +200,7 @@
|
||||
android:background="@color/strong_divider"
|
||||
app:layout_constraintStart_toStartOf="parent"
|
||||
app:layout_constraintEnd_toEndOf="parent"
|
||||
app:layout_constraintTop_toBottomOf="@+id/score_container" />
|
||||
app:layout_constraintTop_toBottomOf="@+id/track_chapters" />
|
||||
|
||||
<TextView
|
||||
android:id="@+id/track_start_date"
|
||||
|
@ -517,6 +517,8 @@
|
||||
<string name="not_tracked">Not tracked</string>
|
||||
<string name="services">Services</string>
|
||||
<string name="sync_chapters_after_reading">Sync chapters after reading</string>
|
||||
<string name="track_when_adding_to_library">Track when adding to library</string>
|
||||
<string name="only_applies_silent_trackers">Only applies to silent trackers, such as Komga</string>
|
||||
<string name="reading">Reading</string>
|
||||
<string name="currently_reading">Currently Reading</string>
|
||||
<string name="dropped">Dropped</string>
|
||||
@ -532,6 +534,8 @@
|
||||
<string name="url_not_set_click_again">Manga URL not set, please click title and select manga again</string>
|
||||
<string name="refresh_tracking">Refresh tracking</string>
|
||||
<string name="add_tracking">Add tracking</string>
|
||||
<string name="source_unsupported">Source is not supported</string>
|
||||
<string name="no_match_found">No match found</string>
|
||||
<string name="remove_tracking">Remove tracking from app</string>
|
||||
<string name="remove_tracking_from_">Also remove from %1$s</string>
|
||||
<string name="anilist" translatable="false">AniList</string>
|
||||
@ -539,6 +543,7 @@
|
||||
<string name="kitsu" translatable="false">Kitsu</string>
|
||||
<string name="bangumi" translatable="false">Bangumi</string>
|
||||
<string name="shikimori" translatable="false">Shikimori</string>
|
||||
<string name="komga" translatable="false">Komga</string>
|
||||
<string name="started_reading_date">Started reading date</string>
|
||||
<string name="finished_reading_date">Finished reading date</string>
|
||||
<string name="myanimelist_relogin">Please login to MAL again</string>
|
||||
|
Loading…
Reference in New Issue
Block a user