mirror of
https://github.com/tachiyomiorg/tachiyomi.git
synced 2024-11-20 05:29: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.PreferencesHelper
|
||||||
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
import eu.kanade.tachiyomi.data.preference.getOrDefault
|
||||||
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.source.online.HttpSource
|
import eu.kanade.tachiyomi.source.online.HttpSource
|
||||||
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.storage.getUriCompat
|
import eu.kanade.tachiyomi.util.storage.getUriCompat
|
||||||
import eu.kanade.tachiyomi.util.manga.MangaShortcutManager
|
import eu.kanade.tachiyomi.util.manga.MangaShortcutManager
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
@ -496,6 +498,10 @@ class LibraryUpdateService(
|
|||||||
try {
|
try {
|
||||||
val newTrack = service.refresh(track)
|
val newTrack = service.refresh(track)
|
||||||
db.insertTrack(newTrack).executeAsBlocking()
|
db.insertTrack(newTrack).executeAsBlocking()
|
||||||
|
|
||||||
|
if (service is UnattendedTrackService) {
|
||||||
|
syncChaptersWithTrackServiceTwoWay(db, db.getChapters(manga).executeAsBlocking(), track, service)
|
||||||
|
}
|
||||||
} catch (e: Exception) {
|
} catch (e: Exception) {
|
||||||
Timber.e(e)
|
Timber.e(e)
|
||||||
}
|
}
|
||||||
|
@ -88,6 +88,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 lastUsedCatalogueSource = "last_catalogue_source"
|
const val lastUsedCatalogueSource = "last_catalogue_source"
|
||||||
|
|
||||||
const val lastUsedCategory = "last_used_category"
|
const val lastUsedCategory = "last_used_category"
|
||||||
|
@ -192,6 +192,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 lastUsedCatalogueSource() = flowPrefs.getLong(Keys.lastUsedCatalogueSource, -1)
|
fun lastUsedCatalogueSource() = flowPrefs.getLong(Keys.lastUsedCatalogueSource, -1)
|
||||||
|
|
||||||
fun lastUsedCategory() = rxPrefs.getInteger(Keys.lastUsedCategory, 0)
|
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.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 }
|
||||||
|
|
||||||
|
@ -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()
|
updateHeader()
|
||||||
showAddedSnack()
|
showAddedSnack()
|
||||||
},
|
},
|
||||||
onMangaMoved = { updateHeader() },
|
onMangaMoved = {
|
||||||
|
updateHeader()
|
||||||
|
if (presenter.preferences.autoAddTrack()) {
|
||||||
|
presenter.fetchChapters(andTracking = true)
|
||||||
|
}
|
||||||
|
},
|
||||||
onMangaDeleted = { presenter.confirmDeletion() }
|
onMangaDeleted = { presenter.confirmDeletion() }
|
||||||
)
|
)
|
||||||
if (snack?.duration == Snackbar.LENGTH_INDEFINITE) {
|
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.preference.getOrDefault
|
||||||
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.SManga
|
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.ChapterFilter
|
||||||
import eu.kanade.tachiyomi.util.chapter.ChapterUtil
|
import eu.kanade.tachiyomi.util.chapter.ChapterUtil
|
||||||
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.lang.trimOrNull
|
import eu.kanade.tachiyomi.util.lang.trimOrNull
|
||||||
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
import eu.kanade.tachiyomi.util.shouldDownloadNewChapters
|
||||||
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
import eu.kanade.tachiyomi.util.storage.DiskUtil
|
||||||
@ -799,6 +801,9 @@ class MangaDetailsPresenter(
|
|||||||
}
|
}
|
||||||
if (trackItem != null) {
|
if (trackItem != null) {
|
||||||
db.insertTrack(trackItem).executeAsBlocking()
|
db.insertTrack(trackItem).executeAsBlocking()
|
||||||
|
if (item.service is UnattendedTrackService) {
|
||||||
|
syncChaptersWithTrackServiceTwoWay(db, chapters, trackItem, item.service)
|
||||||
|
}
|
||||||
trackItem
|
trackItem
|
||||||
} else item.track
|
} else item.track
|
||||||
}
|
}
|
||||||
@ -847,7 +852,13 @@ class MangaDetailsPresenter(
|
|||||||
null
|
null
|
||||||
}
|
}
|
||||||
withContext(Dispatchers.IO) {
|
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()
|
fetchTracks()
|
||||||
}
|
}
|
||||||
|
@ -68,8 +68,15 @@ class TrackHolder(view: View, adapter: TrackAdapter) : BaseViewHolder(view) {
|
|||||||
val status = item.service.getStatus(track.status)
|
val status = item.service.getStatus(track.status)
|
||||||
if (status.isEmpty()) binding.trackStatus.setText(R.string.unknown_status)
|
if (status.isEmpty()) binding.trackStatus.setText(R.string.unknown_status)
|
||||||
else binding.trackStatus.text = item.service.getStatus(track.status)
|
else binding.trackStatus.text = item.service.getStatus(track.status)
|
||||||
binding.trackScore.text = if (track.score == 0f) "-" else item.service.displayScore(track)
|
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.trackScore.setCompoundDrawablesWithIntrinsicBounds(0, 0, starIcon(track), 0)
|
||||||
|
}
|
||||||
|
binding.scoreContainer.isVisible = supportsScoring
|
||||||
|
binding.vertDivider2.isVisible = supportsScoring
|
||||||
|
|
||||||
binding.dateGroup.isVisible = item.service.supportsReadingDates
|
binding.dateGroup.isVisible = item.service.supportsReadingDates
|
||||||
if (item.service.supportsReadingDates) {
|
if (item.service.supportsReadingDates) {
|
||||||
binding.trackStartDate.text =
|
binding.trackStartDate.text =
|
||||||
|
@ -11,16 +11,22 @@ import androidx.recyclerview.widget.RecyclerView
|
|||||||
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
import com.google.android.material.bottomsheet.BottomSheetBehavior
|
||||||
import eu.kanade.tachiyomi.R
|
import eu.kanade.tachiyomi.R
|
||||||
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.data.track.model.TrackSearch
|
import eu.kanade.tachiyomi.data.track.model.TrackSearch
|
||||||
import eu.kanade.tachiyomi.databinding.TrackingBottomSheetBinding
|
import eu.kanade.tachiyomi.databinding.TrackingBottomSheetBinding
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
import eu.kanade.tachiyomi.ui.manga.MangaDetailsController
|
||||||
import eu.kanade.tachiyomi.util.system.dpToPx
|
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.toast
|
||||||
|
import eu.kanade.tachiyomi.util.system.withUIContext
|
||||||
import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener
|
import eu.kanade.tachiyomi.util.view.RecyclerWindowInsetsListener
|
||||||
import eu.kanade.tachiyomi.util.view.checkHeightThen
|
import eu.kanade.tachiyomi.util.view.checkHeightThen
|
||||||
import eu.kanade.tachiyomi.util.view.updateLayoutParams
|
import eu.kanade.tachiyomi.util.view.updateLayoutParams
|
||||||
import eu.kanade.tachiyomi.widget.E2EBottomSheetDialog
|
import eu.kanade.tachiyomi.widget.E2EBottomSheetDialog
|
||||||
import timber.log.Timber
|
import timber.log.Timber
|
||||||
|
import uy.kohesive.injekt.api.get
|
||||||
|
import uy.kohesive.injekt.injectLazy
|
||||||
|
|
||||||
class TrackingBottomSheet(private val controller: MangaDetailsController) :
|
class TrackingBottomSheet(private val controller: MangaDetailsController) :
|
||||||
E2EBottomSheetDialog<TrackingBottomSheetBinding>(controller.activity!!),
|
E2EBottomSheetDialog<TrackingBottomSheetBinding>(controller.activity!!),
|
||||||
@ -35,6 +41,7 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
|
|||||||
|
|
||||||
val presenter = controller.presenter
|
val presenter = controller.presenter
|
||||||
|
|
||||||
|
private val sourceManager: SourceManager by injectLazy()
|
||||||
private var adapter: TrackAdapter? = null
|
private var adapter: TrackAdapter? = null
|
||||||
|
|
||||||
override fun createBinding(inflater: LayoutInflater) =
|
override fun createBinding(inflater: LayoutInflater) =
|
||||||
@ -140,10 +147,31 @@ class TrackingBottomSheet(private val controller: MangaDetailsController) :
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
TrackSearchDialog(this, item.service, item.track != null).showDialog(
|
if (item.service is UnattendedTrackService) {
|
||||||
controller.router,
|
if (item.track != null) {
|
||||||
TAG_SEARCH_CONTROLLER
|
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) {
|
override fun onStatusClick(position: Int) {
|
||||||
|
@ -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
|
||||||
@ -31,6 +32,12 @@ class SettingsTrackingController :
|
|||||||
titleRes = R.string.sync_chapters_after_reading
|
titleRes = R.string.sync_chapters_after_reading
|
||||||
defaultValue = true
|
defaultValue = true
|
||||||
}
|
}
|
||||||
|
switchPreference {
|
||||||
|
key = Keys.autoAddTrack
|
||||||
|
titleRes = R.string.track_when_adding_to_library
|
||||||
|
summaryRes = R.string.only_applies_silent_trackers
|
||||||
|
defaultValue = true
|
||||||
|
}
|
||||||
preferenceCategory {
|
preferenceCategory {
|
||||||
titleRes = R.string.services
|
titleRes = R.string.services
|
||||||
|
|
||||||
@ -51,6 +58,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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -66,9 +77,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()
|
||||||
}
|
}
|
||||||
|
@ -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.Manga
|
||||||
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
import eu.kanade.tachiyomi.data.database.models.MangaCategory
|
||||||
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.LocalSource
|
import eu.kanade.tachiyomi.source.LocalSource
|
||||||
|
import eu.kanade.tachiyomi.source.SourceManager
|
||||||
import eu.kanade.tachiyomi.ui.category.addtolibrary.SetCategoriesSheet
|
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 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
|
import java.util.Date
|
||||||
|
|
||||||
fun Manga.isLocal() = source == LocalSource.ID
|
fun Manga.isLocal() = source == LocalSource.ID
|
||||||
@ -95,6 +105,9 @@ fun Manga.addOrRemoveToFavorites(
|
|||||||
defaultCategory != null -> {
|
defaultCategory != null -> {
|
||||||
favorite = true
|
favorite = true
|
||||||
date_added = Date().time
|
date_added = Date().time
|
||||||
|
if (preferences.autoAddTrack()) {
|
||||||
|
autoAddTrack(db, onMangaMoved)
|
||||||
|
}
|
||||||
db.insertManga(this).executeAsBlocking()
|
db.insertManga(this).executeAsBlocking()
|
||||||
val mc = MangaCategory.create(this, defaultCategory)
|
val mc = MangaCategory.create(this, defaultCategory)
|
||||||
db.setMangaCategories(listOf(mc), listOf(this))
|
db.setMangaCategories(listOf(mc), listOf(this))
|
||||||
@ -108,6 +121,9 @@ fun Manga.addOrRemoveToFavorites(
|
|||||||
defaultCategoryId == 0 || categories.isEmpty() -> { // 'Default' or no category
|
defaultCategoryId == 0 || categories.isEmpty() -> { // 'Default' or no category
|
||||||
favorite = true
|
favorite = true
|
||||||
date_added = Date().time
|
date_added = Date().time
|
||||||
|
if (preferences.autoAddTrack()) {
|
||||||
|
autoAddTrack(db, onMangaMoved)
|
||||||
|
}
|
||||||
db.insertManga(this).executeAsBlocking()
|
db.insertManga(this).executeAsBlocking()
|
||||||
db.setMangaCategories(emptyList(), listOf(this))
|
db.setMangaCategories(emptyList(), listOf(this))
|
||||||
onMangaMoved()
|
onMangaMoved()
|
||||||
@ -133,6 +149,9 @@ fun Manga.addOrRemoveToFavorites(
|
|||||||
true
|
true
|
||||||
) {
|
) {
|
||||||
onMangaAdded()
|
onMangaAdded()
|
||||||
|
if (preferences.autoAddTrack()) {
|
||||||
|
autoAddTrack(db, onMangaMoved)
|
||||||
|
}
|
||||||
}.show()
|
}.show()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -163,3 +182,29 @@ fun Manga.addOrRemoveToFavorites(
|
|||||||
}
|
}
|
||||||
return null
|
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" />
|
app:layout_constraintTop_toTopOf="parent" />
|
||||||
|
|
||||||
<View
|
<View
|
||||||
|
android:id="@+id/vert_divider_2"
|
||||||
android:layout_width="1dp"
|
android:layout_width="1dp"
|
||||||
android:layout_height="0dp"
|
android:layout_height="0dp"
|
||||||
android:layout_marginTop="10dp"
|
android:layout_marginTop="10dp"
|
||||||
@ -199,7 +200,7 @@
|
|||||||
android:background="@color/strong_divider"
|
android:background="@color/strong_divider"
|
||||||
app:layout_constraintStart_toStartOf="parent"
|
app:layout_constraintStart_toStartOf="parent"
|
||||||
app:layout_constraintEnd_toEndOf="parent"
|
app:layout_constraintEnd_toEndOf="parent"
|
||||||
app:layout_constraintTop_toBottomOf="@+id/score_container" />
|
app:layout_constraintTop_toBottomOf="@+id/track_chapters" />
|
||||||
|
|
||||||
<TextView
|
<TextView
|
||||||
android:id="@+id/track_start_date"
|
android:id="@+id/track_start_date"
|
||||||
|
@ -517,6 +517,8 @@
|
|||||||
<string name="not_tracked">Not tracked</string>
|
<string name="not_tracked">Not tracked</string>
|
||||||
<string name="services">Services</string>
|
<string name="services">Services</string>
|
||||||
<string name="sync_chapters_after_reading">Sync chapters after reading</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="reading">Reading</string>
|
||||||
<string name="currently_reading">Currently Reading</string>
|
<string name="currently_reading">Currently Reading</string>
|
||||||
<string name="dropped">Dropped</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="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="refresh_tracking">Refresh tracking</string>
|
||||||
<string name="add_tracking">Add 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">Remove tracking from app</string>
|
||||||
<string name="remove_tracking_from_">Also remove from %1$s</string>
|
<string name="remove_tracking_from_">Also remove from %1$s</string>
|
||||||
<string name="anilist" translatable="false">AniList</string>
|
<string name="anilist" translatable="false">AniList</string>
|
||||||
@ -539,6 +543,7 @@
|
|||||||
<string name="kitsu" translatable="false">Kitsu</string>
|
<string name="kitsu" translatable="false">Kitsu</string>
|
||||||
<string name="bangumi" translatable="false">Bangumi</string>
|
<string name="bangumi" translatable="false">Bangumi</string>
|
||||||
<string name="shikimori" translatable="false">Shikimori</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="started_reading_date">Started reading date</string>
|
||||||
<string name="finished_reading_date">Finished reading date</string>
|
<string name="finished_reading_date">Finished reading date</string>
|
||||||
<string name="myanimelist_relogin">Please login to MAL again</string>
|
<string name="myanimelist_relogin">Please login to MAL again</string>
|
||||||
|
Loading…
Reference in New Issue
Block a user