Support for private tracking with AniList and Bangumi (#1736)

Co-authored-by: MajorTanya <39014446+MajorTanya@users.noreply.github.com>
Co-authored-by: AntsyLich <59261191+AntsyLich@users.noreply.github.com>
This commit is contained in:
NarwhalHorns 2025-02-25 11:01:13 +00:00 committed by GitHub
parent badc229a23
commit 49b2b346b6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 214 additions and 25 deletions

View File

@ -16,6 +16,7 @@ The format is a modified version of [Keep a Changelog](https://keepachangelog.co
- Added option to enable incognito per extension ([@sdaqo](https://github.com/sdaqo), [@AntsyLich](https://github.com/AntsyLich)) ([#157](https://github.com/mihonapp/mihon/pull/157)) - Added option to enable incognito per extension ([@sdaqo](https://github.com/sdaqo), [@AntsyLich](https://github.com/AntsyLich)) ([#157](https://github.com/mihonapp/mihon/pull/157))
- Add button to favorite manga from history screen ([@Animeboynz](https://github.com/Animeboynz)) ([#1733](https://github.com/mihonapp/mihon/pull/1733)) - Add button to favorite manga from history screen ([@Animeboynz](https://github.com/Animeboynz)) ([#1733](https://github.com/mihonapp/mihon/pull/1733))
- Add Monochrome theme (made with e-ink displays in mind) ([@MajorTanya](https://github.com/MajorTanya)) ([#1752](https://github.com/mihonapp/mihon/pull/1752)) - Add Monochrome theme (made with e-ink displays in mind) ([@MajorTanya](https://github.com/MajorTanya)) ([#1752](https://github.com/mihonapp/mihon/pull/1752))
- Support for private tracking with AniList and Bangumi ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1736](https://github.com/mihonapp/mihon/pull/1736))
### Changed ### Changed
- Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603)) - Apply "Downloaded only" filter to all entries regardless of favourite status ([@NGB-Was-Taken](https://github.com/NGB-Was-Taken)) ([#1603](https://github.com/mihonapp/mihon/pull/1603))

View File

@ -10,6 +10,7 @@ fun Track.copyPersonalFrom(other: Track): Track {
status = other.status, status = other.status,
startDate = other.startDate, startDate = other.startDate,
finishDate = other.finishDate, finishDate = other.finishDate,
private = other.private,
) )
} }
@ -26,6 +27,7 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
it.tracking_url = remoteUrl it.tracking_url = remoteUrl
it.started_reading_date = startDate it.started_reading_date = startDate
it.finished_reading_date = finishDate it.finished_reading_date = finishDate
it.private = private
} }
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? { fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
@ -44,5 +46,6 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
remoteUrl = tracking_url, remoteUrl = tracking_url,
startDate = started_reading_date, startDate = started_reading_date,
finishDate = finished_reading_date, finishDate = finished_reading_date,
private = private,
) )
} }

View File

@ -10,10 +10,12 @@ import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.IntrinsicSize import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.layout.systemBars import androidx.compose.foundation.layout.systemBars
import androidx.compose.foundation.layout.windowInsetsPadding import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.foundation.layout.wrapContentSize
@ -22,6 +24,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.verticalScroll import androidx.compose.foundation.verticalScroll
import androidx.compose.material.icons.Icons import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.MoreVert import androidx.compose.material.icons.filled.MoreVert
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Badge
import androidx.compose.material3.BadgedBox
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
import androidx.compose.material3.HorizontalDivider import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -70,6 +75,7 @@ fun TrackInfoDialogHome(
onOpenInBrowser: (TrackItem) -> Unit, onOpenInBrowser: (TrackItem) -> Unit,
onRemoved: (TrackItem) -> Unit, onRemoved: (TrackItem) -> Unit,
onCopyLink: (TrackItem) -> Unit, onCopyLink: (TrackItem) -> Unit,
onTogglePrivate: (TrackItem) -> Unit,
) { ) {
Column( Column(
modifier = Modifier modifier = Modifier
@ -84,6 +90,7 @@ fun TrackInfoDialogHome(
if (item.track != null) { if (item.track != null) {
val supportsScoring = item.tracker.getScoreList().isNotEmpty() val supportsScoring = item.tracker.getScoreList().isNotEmpty()
val supportsReadingDates = item.tracker.supportsReadingDates val supportsReadingDates = item.tracker.supportsReadingDates
val supportsPrivate = item.tracker.supportsPrivateTracking
TrackInfoItem( TrackInfoItem(
title = item.track.title, title = item.track.title,
tracker = item.tracker, tracker = item.tracker,
@ -115,6 +122,9 @@ fun TrackInfoDialogHome(
onOpenInBrowser = { onOpenInBrowser(item) }, onOpenInBrowser = { onOpenInBrowser(item) },
onRemoved = { onRemoved(item) }, onRemoved = { onRemoved(item) },
onCopyLink = { onCopyLink(item) }, onCopyLink = { onCopyLink(item) },
private = item.track.private,
onTogglePrivate = { onTogglePrivate(item) }
.takeIf { supportsPrivate },
) )
} else { } else {
TrackInfoItemEmpty( TrackInfoItemEmpty(
@ -144,17 +154,37 @@ private fun TrackInfoItem(
onOpenInBrowser: () -> Unit, onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit, onRemoved: () -> Unit,
onCopyLink: () -> Unit, onCopyLink: () -> Unit,
private: Boolean,
onTogglePrivate: (() -> Unit)?,
) { ) {
val context = LocalContext.current val context = LocalContext.current
Column { Column {
Row( Row(
verticalAlignment = Alignment.CenterVertically, verticalAlignment = Alignment.CenterVertically,
) { ) {
TrackLogoIcon( BadgedBox(
tracker = tracker, badge = {
onClick = onOpenInBrowser, if (private) {
onLongClick = onCopyLink, Badge(
) containerColor = MaterialTheme.colorScheme.primary,
contentColor = MaterialTheme.colorScheme.onPrimary,
modifier = Modifier.absoluteOffset(x = (-5).dp),
) {
Icon(
imageVector = Icons.Filled.VisibilityOff,
contentDescription = stringResource(MR.strings.tracked_privately),
modifier = Modifier.size(14.dp),
)
}
}
},
) {
TrackLogoIcon(
tracker = tracker,
onClick = onOpenInBrowser,
onLongClick = onCopyLink,
)
}
Box( Box(
modifier = Modifier modifier = Modifier
.height(48.dp) .height(48.dp)
@ -181,6 +211,8 @@ private fun TrackInfoItem(
onOpenInBrowser = onOpenInBrowser, onOpenInBrowser = onOpenInBrowser,
onRemoved = onRemoved, onRemoved = onRemoved,
onCopyLink = onCopyLink, onCopyLink = onCopyLink,
private = private,
onTogglePrivate = onTogglePrivate,
) )
} }
@ -291,6 +323,8 @@ private fun TrackInfoItemMenu(
onOpenInBrowser: () -> Unit, onOpenInBrowser: () -> Unit,
onRemoved: () -> Unit, onRemoved: () -> Unit,
onCopyLink: () -> Unit, onCopyLink: () -> Unit,
private: Boolean,
onTogglePrivate: (() -> Unit)?,
) { ) {
var expanded by remember { mutableStateOf(false) } var expanded by remember { mutableStateOf(false) }
Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) { Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
@ -318,6 +352,25 @@ private fun TrackInfoItemMenu(
expanded = false expanded = false
}, },
) )
if (onTogglePrivate != null) {
DropdownMenuItem(
text = {
Text(
stringResource(
if (private) {
MR.strings.action_toggle_private_off
} else {
MR.strings.action_toggle_private_on
},
),
)
},
onClick = {
onTogglePrivate()
expanded = false
},
)
}
DropdownMenuItem( DropdownMenuItem(
text = { Text(stringResource(MR.strings.action_remove)) }, text = { Text(stringResource(MR.strings.action_remove)) },
onClick = { onClick = {

View File

@ -25,7 +25,9 @@ internal class TrackInfoDialogHomePreviewProvider :
remoteUrl = "https://example.com", remoteUrl = "https://example.com",
startDate = 0L, startDate = 0L,
finishDate = 0L, finishDate = 0L,
private = false,
) )
private val privateTrack = aTrack.copy(private = true)
private val trackItemWithoutTrack = TrackItem( private val trackItemWithoutTrack = TrackItem(
track = null, track = null,
tracker = DummyTracker( tracker = DummyTracker(
@ -40,6 +42,13 @@ internal class TrackInfoDialogHomePreviewProvider :
name = "Example Tracker 2", name = "Example Tracker 2",
), ),
) )
private val trackItemWithPrivateTrack = TrackItem(
track = privateTrack,
tracker = DummyTracker(
id = 2L,
name = "Example Tracker 2",
),
)
private val trackersWithAndWithoutTrack = @Composable { private val trackersWithAndWithoutTrack = @Composable {
TrackInfoDialogHome( TrackInfoDialogHome(
@ -57,6 +66,7 @@ internal class TrackInfoDialogHomePreviewProvider :
onOpenInBrowser = {}, onOpenInBrowser = {},
onRemoved = {}, onRemoved = {},
onCopyLink = {}, onCopyLink = {},
onTogglePrivate = {},
) )
} }
@ -73,6 +83,24 @@ internal class TrackInfoDialogHomePreviewProvider :
onOpenInBrowser = {}, onOpenInBrowser = {},
onRemoved = {}, onRemoved = {},
onCopyLink = {}, onCopyLink = {},
onTogglePrivate = {},
)
}
private val trackerWithPrivateTracking = @Composable {
TrackInfoDialogHome(
trackItems = listOf(trackItemWithPrivateTrack),
dateFormat = DateTimeFormatter.ofLocalizedDate(FormatStyle.MEDIUM),
onStatusClick = {},
onChapterClick = {},
onScoreClick = {},
onStartDateEdit = {},
onEndDateEdit = {},
onNewSearch = {},
onOpenInBrowser = {},
onRemoved = {},
onCopyLink = {},
onTogglePrivate = {},
) )
} }
@ -80,5 +108,6 @@ internal class TrackInfoDialogHomePreviewProvider :
get() = sequenceOf( get() = sequenceOf(
trackersWithAndWithoutTrack, trackersWithAndWithoutTrack,
noTrackers, noTrackers,
trackerWithPrivateTracking,
) )
} }

View File

@ -33,6 +33,7 @@ import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.automirrored.outlined.ArrowBack import androidx.compose.material.icons.automirrored.outlined.ArrowBack
import androidx.compose.material.icons.filled.CheckCircle import androidx.compose.material.icons.filled.CheckCircle
import androidx.compose.material.icons.filled.Close import androidx.compose.material.icons.filled.Close
import androidx.compose.material.icons.filled.VisibilityOff
import androidx.compose.material3.Button import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.DropdownMenuItem import androidx.compose.material3.DropdownMenuItem
@ -90,8 +91,9 @@ fun TrackerSearch(
queryResult: Result<List<TrackSearch>>?, queryResult: Result<List<TrackSearch>>?,
selected: TrackSearch?, selected: TrackSearch?,
onSelectedChange: (TrackSearch) -> Unit, onSelectedChange: (TrackSearch) -> Unit,
onConfirmSelection: () -> Unit, onConfirmSelection: (private: Boolean) -> Unit,
onDismissRequest: () -> Unit, onDismissRequest: () -> Unit,
supportsPrivateTracking: Boolean,
) { ) {
val focusManager = LocalFocusManager.current val focusManager = LocalFocusManager.current
val focusRequester = remember { FocusRequester() } val focusRequester = remember { FocusRequester() }
@ -164,15 +166,31 @@ fun TrackerSearch(
enter = fadeIn() + slideInVertically { it / 2 }, enter = fadeIn() + slideInVertically { it / 2 },
exit = slideOutVertically { it / 2 } + fadeOut(), exit = slideOutVertically { it / 2 } + fadeOut(),
) { ) {
Button( Row(
onClick = { onConfirmSelection() },
modifier = Modifier modifier = Modifier
.padding(12.dp) .padding(MaterialTheme.padding.small)
.windowInsetsPadding(WindowInsets.navigationBars) .windowInsetsPadding(WindowInsets.navigationBars)
.fillMaxWidth(), .fillMaxWidth(),
elevation = ButtonDefaults.elevatedButtonElevation(), horizontalArrangement = Arrangement.spacedBy(MaterialTheme.padding.small),
) { ) {
Text(text = stringResource(MR.strings.action_track)) Button(
onClick = { onConfirmSelection(false) },
modifier = Modifier.weight(1f),
elevation = ButtonDefaults.elevatedButtonElevation(),
) {
Text(text = stringResource(MR.strings.action_track))
}
if (supportsPrivateTracking) {
Button(
onClick = { onConfirmSelection(true) },
elevation = ButtonDefaults.elevatedButtonElevation(),
) {
Icon(
imageVector = Icons.Filled.VisibilityOff,
contentDescription = stringResource(MR.strings.action_toggle_private_on),
)
}
}
} }
} }
}, },

View File

@ -20,6 +20,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {}, onSelectedChange = {},
onConfirmSelection = {}, onConfirmSelection = {},
onDismissRequest = {}, onDismissRequest = {},
supportsPrivateTracking = false,
) )
} }
private val fullPageWithoutSelected = @Composable { private val fullPageWithoutSelected = @Composable {
@ -31,6 +32,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {}, onSelectedChange = {},
onConfirmSelection = {}, onConfirmSelection = {},
onDismissRequest = {}, onDismissRequest = {},
supportsPrivateTracking = false,
) )
} }
private val loading = @Composable { private val loading = @Composable {
@ -42,12 +44,27 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
onSelectedChange = {}, onSelectedChange = {},
onConfirmSelection = {}, onConfirmSelection = {},
onDismissRequest = {}, onDismissRequest = {},
supportsPrivateTracking = false,
)
}
private val fullPageWithPrivateTracking = @Composable {
val items = someTrackSearches().take(30).toList()
TrackerSearch(
state = TextFieldState(initialText = "search text"),
onDispatchQuery = {},
queryResult = Result.success(items),
selected = items[1],
onSelectedChange = {},
onConfirmSelection = {},
onDismissRequest = {},
supportsPrivateTracking = true,
) )
} }
override val values: Sequence<@Composable () -> Unit> = sequenceOf( override val values: Sequence<@Composable () -> Unit> = sequenceOf(
fullPageWithSecondSelected, fullPageWithSecondSelected,
fullPageWithoutSelected, fullPageWithoutSelected,
loading, loading,
fullPageWithPrivateTracking,
) )
private fun someTrackSearches(): Sequence<TrackSearch> = sequence { private fun someTrackSearches(): Sequence<TrackSearch> = sequence {

View File

@ -25,6 +25,7 @@ data class BackupTracking(
@ProtoNumber(10) var startedReadingDate: Long = 0, @ProtoNumber(10) var startedReadingDate: Long = 0,
// finishedReadingDate is called endReadTime in 1.x // finishedReadingDate is called endReadTime in 1.x
@ProtoNumber(11) var finishedReadingDate: Long = 0, @ProtoNumber(11) var finishedReadingDate: Long = 0,
@ProtoNumber(12) var private: Boolean = false,
@ProtoNumber(100) var mediaId: Long = 0, @ProtoNumber(100) var mediaId: Long = 0,
) { ) {
@ -48,6 +49,7 @@ data class BackupTracking(
startDate = this@BackupTracking.startedReadingDate, startDate = this@BackupTracking.startedReadingDate,
finishDate = this@BackupTracking.finishedReadingDate, finishDate = this@BackupTracking.finishedReadingDate,
remoteUrl = this@BackupTracking.trackingUrl, remoteUrl = this@BackupTracking.trackingUrl,
private = this@BackupTracking.private,
) )
} }
} }
@ -66,6 +68,7 @@ val backupTrackMapper = {
remoteUrl: String, remoteUrl: String,
startDate: Long, startDate: Long,
finishDate: Long, finishDate: Long,
private: Boolean,
-> ->
BackupTracking( BackupTracking(
syncId = syncId.toInt(), syncId = syncId.toInt(),
@ -80,5 +83,6 @@ val backupTrackMapper = {
startedReadingDate = startDate, startedReadingDate = startDate,
finishedReadingDate = finishDate, finishedReadingDate = finishDate,
trackingUrl = remoteUrl, trackingUrl = remoteUrl,
private = private,
) )
} }

View File

@ -404,6 +404,7 @@ class MangaRestorer(
track.remoteUrl, track.remoteUrl,
track.startDate, track.startDate,
track.finishDate, track.finishDate,
track.private,
track.id, track.id,
) )
} }

View File

@ -32,12 +32,15 @@ interface Track : Serializable {
var tracking_url: String var tracking_url: String
fun copyPersonalFrom(other: Track) { var private: Boolean
fun copyPersonalFrom(other: Track, copyRemotePrivate: Boolean = true) {
last_chapter_read = other.last_chapter_read last_chapter_read = other.last_chapter_read
score = other.score score = other.score
status = other.status status = other.status
started_reading_date = other.started_reading_date started_reading_date = other.started_reading_date
finished_reading_date = other.finished_reading_date finished_reading_date = other.finished_reading_date
if (copyRemotePrivate) private = other.private
} }
companion object { companion object {

View File

@ -29,4 +29,6 @@ class TrackImpl : Track {
override var finished_reading_date: Long = 0 override var finished_reading_date: Long = 0
override var tracking_url: String = "" override var tracking_url: String = ""
override var private: Boolean = false
} }

View File

@ -37,6 +37,8 @@ abstract class BaseTracker(
// Application and remote support for reading dates // Application and remote support for reading dates
override val supportsReadingDates: Boolean = false override val supportsReadingDates: Boolean = false
override val supportsPrivateTracking: Boolean = false
// TODO: Store all scores as 10 point in the future maybe? // TODO: Store all scores as 10 point in the future maybe?
override fun get10PointScore(track: DomainTrack): Double { override fun get10PointScore(track: DomainTrack): Double {
return track.score return track.score
@ -120,6 +122,11 @@ abstract class BaseTracker(
updateRemote(track) updateRemote(track)
} }
override suspend fun setRemotePrivate(track: Track, private: Boolean) {
track.private = private
updateRemote(track)
}
private suspend fun updateRemote(track: Track): Unit = withIOContext { private suspend fun updateRemote(track: Track): Unit = withIOContext {
try { try {
update(track) update(track)

View File

@ -22,6 +22,8 @@ interface Tracker {
// Application and remote support for reading dates // Application and remote support for reading dates
val supportsReadingDates: Boolean val supportsReadingDates: Boolean
val supportsPrivateTracking: Boolean
@ColorInt @ColorInt
fun getLogoColor(): Int fun getLogoColor(): Int
@ -82,4 +84,6 @@ interface Tracker {
suspend fun setRemoteStartDate(track: Track, epochMillis: Long) suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
suspend fun setRemoteFinishDate(track: Track, epochMillis: Long) suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
suspend fun setRemotePrivate(track: Track, private: Boolean)
} }

View File

@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableList
import kotlinx.collections.immutable.persistentListOf import kotlinx.collections.immutable.persistentListOf
import kotlinx.collections.immutable.toImmutableList import kotlinx.collections.immutable.toImmutableList
import kotlinx.serialization.encodeToString
import kotlinx.serialization.json.Json import kotlinx.serialization.json.Json
import tachiyomi.i18n.MR import tachiyomi.i18n.MR
import uy.kohesive.injekt.injectLazy import uy.kohesive.injekt.injectLazy
@ -43,6 +42,8 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
override val supportsReadingDates: Boolean = true override val supportsReadingDates: Boolean = true
override val supportsPrivateTracking: Boolean = true
private val scorePreference = trackPreferences.anilistScoreType() private val scorePreference = trackPreferences.anilistScoreType()
init { init {
@ -183,7 +184,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val remoteTrack = api.findLibManga(track, getUsername().toInt()) val remoteTrack = api.findLibManga(track, getUsername().toInt())
return if (remoteTrack != null) { return if (remoteTrack != null) {
track.copyPersonalFrom(remoteTrack) track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
track.library_id = remoteTrack.library_id track.library_id = remoteTrack.library_id
if (track.status != COMPLETED) { if (track.status != COMPLETED) {

View File

@ -42,8 +42,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
suspend fun addLibManga(track: Track): Track { suspend fun addLibManga(track: Track): Track {
return withIOContext { return withIOContext {
val query = """ val query = """
|mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) { |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) {
|SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) { |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
| id | id
| status | status
|} |}
@ -56,6 +56,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("mangaId", track.remote_id) put("mangaId", track.remote_id)
put("progress", track.last_chapter_read.toInt()) put("progress", track.last_chapter_read.toInt())
put("status", track.toApiStatus()) put("status", track.toApiStatus())
put("private", track.private)
} }
} }
with(json) { with(json) {
@ -79,11 +80,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
return withIOContext { return withIOContext {
val query = """ val query = """
|mutation UpdateManga( |mutation UpdateManga(
|${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, |${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
|${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput |${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
|) { |) {
|SaveMediaListEntry( |SaveMediaListEntry(
|id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, |id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
|scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt |scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
|) { |) {
|id |id
@ -102,6 +103,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
put("score", track.score.toInt()) put("score", track.score.toInt())
put("startedAt", createDate(track.started_reading_date)) put("startedAt", createDate(track.started_reading_date))
put("completedAt", createDate(track.finished_reading_date)) put("completedAt", createDate(track.finished_reading_date))
put("private", track.private)
} }
} }
authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime))) authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
@ -190,6 +192,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
|status |status
|scoreRaw: score(format: POINT_100) |scoreRaw: score(format: POINT_100)
|progress |progress
|private
|startedAt { |startedAt {
|year |year
|month |month

View File

@ -49,6 +49,7 @@ data class ALUserManga(
val startDateFuzzy: Long, val startDateFuzzy: Long,
val completedDateFuzzy: Long, val completedDateFuzzy: Long,
val manga: ALManga, val manga: ALManga,
val private: Boolean,
) { ) {
fun toTrack() = Track.create(TrackerManager.ANILIST).apply { fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
remote_id = manga.remoteId remote_id = manga.remoteId
@ -60,6 +61,7 @@ data class ALUserManga(
last_chapter_read = chaptersRead.toDouble() last_chapter_read = chaptersRead.toDouble()
library_id = libraryId library_id = libraryId
total_chapters = manga.totalChapters total_chapters = manga.totalChapters
private = this@ALUserManga.private
} }
private fun toTrackStatus() = when (listStatus) { private fun toTrackStatus() = when (listStatus) {

View File

@ -28,6 +28,7 @@ data class ALUserListItem(
val startedAt: ALFuzzyDate, val startedAt: ALFuzzyDate,
val completedAt: ALFuzzyDate, val completedAt: ALFuzzyDate,
val media: ALSearchItem, val media: ALSearchItem,
val private: Boolean,
) { ) {
fun toALUserManga(): ALUserManga { fun toALUserManga(): ALUserManga {
return ALUserManga( return ALUserManga(
@ -38,6 +39,7 @@ data class ALUserListItem(
startDateFuzzy = startedAt.toEpochMilli(), startDateFuzzy = startedAt.toEpochMilli(),
completedDateFuzzy = completedAt.toEpochMilli(), completedDateFuzzy = completedAt.toEpochMilli(),
manga = media.toALManga(), manga = media.toALManga(),
private = private,
) )
} }
} }

View File

@ -22,6 +22,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
private val api by lazy { BangumiApi(id, client, interceptor) } private val api by lazy { BangumiApi(id, client, interceptor) }
override val supportsPrivateTracking: Boolean = true
override fun getScoreList(): ImmutableList<String> = SCORE_LIST override fun getScoreList(): ImmutableList<String> = SCORE_LIST
override fun displayScore(track: DomainTrack): String { override fun displayScore(track: DomainTrack): String {
@ -49,7 +51,7 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
override suspend fun bind(track: Track, hasReadChapters: Boolean): Track { override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
val statusTrack = api.statusLibManga(track, getUsername()) val statusTrack = api.statusLibManga(track, getUsername())
return if (statusTrack != null) { return if (statusTrack != null) {
track.copyPersonalFrom(statusTrack) track.copyPersonalFrom(statusTrack, copyRemotePrivate = false)
track.library_id = statusTrack.library_id track.library_id = statusTrack.library_id
track.score = statusTrack.score track.score = statusTrack.score
track.last_chapter_read = statusTrack.last_chapter_read track.last_chapter_read = statusTrack.last_chapter_read

View File

@ -45,6 +45,7 @@ class BangumiApi(
put("type", track.toApiStatus()) put("type", track.toApiStatus())
put("rate", track.score.toInt().coerceIn(0, 10)) put("rate", track.score.toInt().coerceIn(0, 10))
put("ep_status", track.last_chapter_read.toInt()) put("ep_status", track.last_chapter_read.toInt())
put("private", track.private)
} }
.toString() .toString()
.toRequestBody() .toRequestBody()
@ -62,6 +63,7 @@ class BangumiApi(
put("type", track.toApiStatus()) put("type", track.toApiStatus())
put("rate", track.score.toInt().coerceIn(0, 10)) put("rate", track.score.toInt().coerceIn(0, 10))
put("ep_status", track.last_chapter_read.toInt()) put("ep_status", track.last_chapter_read.toInt())
put("private", track.private)
} }
.toString() .toString()
.toRequestBody() .toRequestBody()

View File

@ -30,6 +30,8 @@ class TrackSearch : Track {
override var finished_reading_date: Long = 0 override var finished_reading_date: Long = 0
override var private: Boolean = false
override lateinit var tracking_url: String override lateinit var tracking_url: String
var cover_url: String = "" var cover_url: String = ""

View File

@ -172,6 +172,7 @@ data class TrackInfoDialogHomeScreen(
) )
}, },
onCopyLink = { context.copyTrackerLink(it) }, onCopyLink = { context.copyTrackerLink(it) },
onTogglePrivate = screenModel::togglePrivate,
) )
} }
@ -247,6 +248,12 @@ data class TrackInfoDialogHomeScreen(
} }
} }
fun togglePrivate(item: TrackItem) {
screenModelScope.launchNonCancellable {
item.tracker.setRemotePrivate(item.track!!.toDbTrack(), !item.track.private)
}
}
private fun List<Track>.mapToTrackItem(): List<TrackItem> { private fun List<Track>.mapToTrackItem(): List<TrackItem> {
val loggedInTrackers = Injekt.get<TrackerManager>().loggedInTrackers() val loggedInTrackers = Injekt.get<TrackerManager>().loggedInTrackers()
val source = Injekt.get<SourceManager>().getOrStub(sourceId) val source = Injekt.get<SourceManager>().getOrStub(sourceId)
@ -673,11 +680,14 @@ data class TrackerSearchScreen(
queryResult = state.queryResult, queryResult = state.queryResult,
selected = state.selected, selected = state.selected,
onSelectedChange = screenModel::updateSelection, onSelectedChange = screenModel::updateSelection,
onConfirmSelection = { onConfirmSelection = f@{ private: Boolean ->
screenModel.registerTracking(state.selected!!) val selected = state.selected ?: return@f
selected.private = private
screenModel.registerTracking(selected)
navigator.pop() navigator.pop()
}, },
onDismissRequest = navigator::pop, onDismissRequest = navigator::pop,
supportsPrivateTracking = screenModel.supportsPrivateTracking,
) )
} }
@ -688,6 +698,8 @@ data class TrackerSearchScreen(
private val tracker: Tracker, private val tracker: Tracker,
) : StateScreenModel<Model.State>(State()) { ) : StateScreenModel<Model.State>(State()) {
val supportsPrivateTracking = tracker.supportsPrivateTracking
init { init {
// Run search on first launch // Run search on first launch
if (initialQuery.isNotBlank()) { if (initialQuery.isNotBlank()) {

View File

@ -17,6 +17,7 @@ data class DummyTracker(
override val id: Long, override val id: Long,
override val name: String, override val name: String,
override val supportsReadingDates: Boolean = false, override val supportsReadingDates: Boolean = false,
override val supportsPrivateTracking: Boolean = false,
override val isLoggedIn: Boolean = false, override val isLoggedIn: Boolean = false,
override val isLoggedInFlow: Flow<Boolean> = flowOf(false), override val isLoggedInFlow: Flow<Boolean> = flowOf(false),
val valLogoColor: Int = Color.rgb(18, 25, 35), val valLogoColor: Int = Color.rgb(18, 25, 35),
@ -119,4 +120,9 @@ data class DummyTracker(
track: eu.kanade.tachiyomi.data.database.models.Track, track: eu.kanade.tachiyomi.data.database.models.Track,
epochMillis: Long, epochMillis: Long,
) = Unit ) = Unit
override suspend fun setRemotePrivate(
track: eu.kanade.tachiyomi.data.database.models.Track,
private: Boolean,
) = Unit
} }

View File

@ -17,6 +17,7 @@ object TrackMapper {
remoteUrl: String, remoteUrl: String,
startDate: Long, startDate: Long,
finishDate: Long, finishDate: Long,
private: Boolean,
): Track = Track( ): Track = Track(
id = id, id = id,
mangaId = mangaId, mangaId = mangaId,
@ -31,5 +32,6 @@ object TrackMapper {
remoteUrl = remoteUrl, remoteUrl = remoteUrl,
startDate = startDate, startDate = startDate,
finishDate = finishDate, finishDate = finishDate,
private = private,
) )
} }

View File

@ -64,6 +64,7 @@ class TrackRepositoryImpl(
remoteUrl = mangaTrack.remoteUrl, remoteUrl = mangaTrack.remoteUrl,
startDate = mangaTrack.startDate, startDate = mangaTrack.startDate,
finishDate = mangaTrack.finishDate, finishDate = mangaTrack.finishDate,
private = mangaTrack.private,
) )
} }
} }

View File

@ -1,3 +1,5 @@
import kotlin.Boolean;
CREATE TABLE manga_sync( CREATE TABLE manga_sync(
_id INTEGER NOT NULL PRIMARY KEY, _id INTEGER NOT NULL PRIMARY KEY,
manga_id INTEGER NOT NULL, manga_id INTEGER NOT NULL,
@ -12,6 +14,7 @@ CREATE TABLE manga_sync(
remote_url TEXT NOT NULL, remote_url TEXT NOT NULL,
start_date INTEGER NOT NULL, start_date INTEGER NOT NULL,
finish_date INTEGER NOT NULL, finish_date INTEGER NOT NULL,
private INTEGER AS Boolean DEFAULT 0 NOT NULL,
UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE, UNIQUE (manga_id, sync_id) ON CONFLICT REPLACE,
FOREIGN KEY(manga_id) REFERENCES mangas (_id) FOREIGN KEY(manga_id) REFERENCES mangas (_id)
ON DELETE CASCADE ON DELETE CASCADE
@ -36,8 +39,8 @@ FROM manga_sync
WHERE manga_id = :mangaId; WHERE manga_id = :mangaId;
insert: insert:
INSERT INTO manga_sync(manga_id,sync_id,remote_id,library_id,title,last_chapter_read,total_chapters,status,score,remote_url,start_date,finish_date) INSERT INTO manga_sync(manga_id,sync_id,remote_id,library_id,title,last_chapter_read,total_chapters,status,score,remote_url,start_date,finish_date,private)
VALUES (:mangaId,:syncId,:remoteId,:libraryId,:title,:lastChapterRead,:totalChapters,:status,:score,:remoteUrl,:startDate,:finishDate); VALUES (:mangaId,:syncId,:remoteId,:libraryId,:title,:lastChapterRead,:totalChapters,:status,:score,:remoteUrl,:startDate,:finishDate,:private);
update: update:
UPDATE manga_sync UPDATE manga_sync
@ -53,5 +56,6 @@ SET
score = coalesce(:score, score), score = coalesce(:score, score),
remote_url = coalesce(:trackingUrl, remote_url), remote_url = coalesce(:trackingUrl, remote_url),
start_date = coalesce(:startDate, start_date), start_date = coalesce(:startDate, start_date),
finish_date = coalesce(:finishDate, finish_date) finish_date = coalesce(:finishDate, finish_date),
private = coalesce(:private, private)
WHERE _id = :id; WHERE _id = :id;

View File

@ -0,0 +1,4 @@
import kotlin.Boolean;
-- Add private field for tracking
ALTER TABLE manga_sync ADD COLUMN private INTEGER AS Boolean DEFAULT 0 NOT NULL;

View File

@ -16,4 +16,5 @@ data class Track(
val remoteUrl: String, val remoteUrl: String,
val startDate: Long, val startDate: Long,
val finishDate: Long, val finishDate: Long,
val private: Boolean,
) : Serializable ) : Serializable

View File

@ -521,7 +521,6 @@
<string name="enhanced_services">Enhanced trackers</string> <string name="enhanced_services">Enhanced trackers</string>
<string name="enhanced_services_not_installed">Available but source not installed: %s</string> <string name="enhanced_services_not_installed">Available but source not installed: %s</string>
<string name="enhanced_tracking_info">Provides enhanced features for specific sources. Entries are automatically tracked when added to your library.</string> <string name="enhanced_tracking_info">Provides enhanced features for specific sources. Entries are automatically tracked when added to your library.</string>
<string name="action_track">Track</string>
<string name="track_activity_name">Tracker login</string> <string name="track_activity_name">Tracker login</string>
<!-- Browse section --> <!-- Browse section -->
@ -752,6 +751,7 @@
<!-- Tracking Screen --> <!-- Tracking Screen -->
<string name="manga_tracking_tab">Tracking</string> <string name="manga_tracking_tab">Tracking</string>
<string name="add_tracking">Add tracking</string> <string name="add_tracking">Add tracking</string>
<string name="action_track">Track</string>
<string name="unread">Unread</string> <string name="unread">Unread</string>
<string name="reading">Reading</string> <string name="reading">Reading</string>
<string name="completed">Completed</string> <string name="completed">Completed</string>
@ -771,6 +771,9 @@
<string name="track_status">Status</string> <string name="track_status">Status</string>
<string name="track_started_reading_date">Start date</string> <string name="track_started_reading_date">Start date</string>
<string name="track_finished_reading_date">Finish date</string> <string name="track_finished_reading_date">Finish date</string>
<string name="tracked_privately">Tracked privately</string>
<string name="action_toggle_private_on">Track privately</string>
<string name="action_toggle_private_off">Track publicly</string>
<string name="track_type">Type</string> <string name="track_type">Type</string>
<string name="myanimelist_relogin">Please login to MAL again</string> <string name="myanimelist_relogin">Please login to MAL again</string>
<string name="source_unsupported">Source is not supported</string> <string name="source_unsupported">Source is not supported</string>