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:
Jays2Kings 2021-07-03 14:26:04 -04:00
parent dfe125fa5f
commit 6717f7dd3b
19 changed files with 499 additions and 13 deletions

View File

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

View File

@ -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"

View File

@ -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)

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.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 }

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,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
}
}

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.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
}
}

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

@ -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) {

View File

@ -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()
}

View File

@ -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)
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 =

View File

@ -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) {

View File

@ -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) {
if (service is NoLoginTrackService) {
service.logout()
updatePreference(service.id)
} else {
val dialog = TrackLogoutDialog(service)
dialog.targetController = this@SettingsTrackingController
dialog.showDialog(router)
}
} else {
login()
}

View File

@ -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")
}
}
}
}

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.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)
}
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

View File

@ -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"

View File

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