mirror of
				https://github.com/mihonapp/mihon.git
				synced 2025-11-04 08:08:55 +01:00 
			
		
		
		
	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:
		@@ -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))
 | 
			
		||||
- 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))
 | 
			
		||||
- Support for private tracking with AniList and Bangumi ([@NarwhalHorns](https://github.com/NarwhalHorns)) ([#1736](https://github.com/mihonapp/mihon/pull/1736))
 | 
			
		||||
 | 
			
		||||
### 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))
 | 
			
		||||
 
 | 
			
		||||
@@ -10,6 +10,7 @@ fun Track.copyPersonalFrom(other: Track): Track {
 | 
			
		||||
        status = other.status,
 | 
			
		||||
        startDate = other.startDate,
 | 
			
		||||
        finishDate = other.finishDate,
 | 
			
		||||
        private = other.private,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -26,6 +27,7 @@ fun Track.toDbTrack(): DbTrack = DbTrack.create(trackerId).also {
 | 
			
		||||
    it.tracking_url = remoteUrl
 | 
			
		||||
    it.started_reading_date = startDate
 | 
			
		||||
    it.finished_reading_date = finishDate
 | 
			
		||||
    it.private = private
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
 | 
			
		||||
@@ -44,5 +46,6 @@ fun DbTrack.toDomainTrack(idRequired: Boolean = true): Track? {
 | 
			
		||||
        remoteUrl = tracking_url,
 | 
			
		||||
        startDate = started_reading_date,
 | 
			
		||||
        finishDate = finished_reading_date,
 | 
			
		||||
        private = private,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -10,10 +10,12 @@ import androidx.compose.foundation.layout.Column
 | 
			
		||||
import androidx.compose.foundation.layout.IntrinsicSize
 | 
			
		||||
import androidx.compose.foundation.layout.Row
 | 
			
		||||
import androidx.compose.foundation.layout.WindowInsets
 | 
			
		||||
import androidx.compose.foundation.layout.absoluteOffset
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxHeight
 | 
			
		||||
import androidx.compose.foundation.layout.fillMaxWidth
 | 
			
		||||
import androidx.compose.foundation.layout.height
 | 
			
		||||
import androidx.compose.foundation.layout.padding
 | 
			
		||||
import androidx.compose.foundation.layout.size
 | 
			
		||||
import androidx.compose.foundation.layout.systemBars
 | 
			
		||||
import androidx.compose.foundation.layout.windowInsetsPadding
 | 
			
		||||
import androidx.compose.foundation.layout.wrapContentSize
 | 
			
		||||
@@ -22,6 +24,9 @@ import androidx.compose.foundation.shape.RoundedCornerShape
 | 
			
		||||
import androidx.compose.foundation.verticalScroll
 | 
			
		||||
import androidx.compose.material.icons.Icons
 | 
			
		||||
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.HorizontalDivider
 | 
			
		||||
import androidx.compose.material3.Icon
 | 
			
		||||
@@ -70,6 +75,7 @@ fun TrackInfoDialogHome(
 | 
			
		||||
    onOpenInBrowser: (TrackItem) -> Unit,
 | 
			
		||||
    onRemoved: (TrackItem) -> Unit,
 | 
			
		||||
    onCopyLink: (TrackItem) -> Unit,
 | 
			
		||||
    onTogglePrivate: (TrackItem) -> Unit,
 | 
			
		||||
) {
 | 
			
		||||
    Column(
 | 
			
		||||
        modifier = Modifier
 | 
			
		||||
@@ -84,6 +90,7 @@ fun TrackInfoDialogHome(
 | 
			
		||||
            if (item.track != null) {
 | 
			
		||||
                val supportsScoring = item.tracker.getScoreList().isNotEmpty()
 | 
			
		||||
                val supportsReadingDates = item.tracker.supportsReadingDates
 | 
			
		||||
                val supportsPrivate = item.tracker.supportsPrivateTracking
 | 
			
		||||
                TrackInfoItem(
 | 
			
		||||
                    title = item.track.title,
 | 
			
		||||
                    tracker = item.tracker,
 | 
			
		||||
@@ -115,6 +122,9 @@ fun TrackInfoDialogHome(
 | 
			
		||||
                    onOpenInBrowser = { onOpenInBrowser(item) },
 | 
			
		||||
                    onRemoved = { onRemoved(item) },
 | 
			
		||||
                    onCopyLink = { onCopyLink(item) },
 | 
			
		||||
                    private = item.track.private,
 | 
			
		||||
                    onTogglePrivate = { onTogglePrivate(item) }
 | 
			
		||||
                        .takeIf { supportsPrivate },
 | 
			
		||||
                )
 | 
			
		||||
            } else {
 | 
			
		||||
                TrackInfoItemEmpty(
 | 
			
		||||
@@ -144,17 +154,37 @@ private fun TrackInfoItem(
 | 
			
		||||
    onOpenInBrowser: () -> Unit,
 | 
			
		||||
    onRemoved: () -> Unit,
 | 
			
		||||
    onCopyLink: () -> Unit,
 | 
			
		||||
    private: Boolean,
 | 
			
		||||
    onTogglePrivate: (() -> Unit)?,
 | 
			
		||||
) {
 | 
			
		||||
    val context = LocalContext.current
 | 
			
		||||
    Column {
 | 
			
		||||
        Row(
 | 
			
		||||
            verticalAlignment = Alignment.CenterVertically,
 | 
			
		||||
        ) {
 | 
			
		||||
            TrackLogoIcon(
 | 
			
		||||
                tracker = tracker,
 | 
			
		||||
                onClick = onOpenInBrowser,
 | 
			
		||||
                onLongClick = onCopyLink,
 | 
			
		||||
            )
 | 
			
		||||
            BadgedBox(
 | 
			
		||||
                badge = {
 | 
			
		||||
                    if (private) {
 | 
			
		||||
                        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(
 | 
			
		||||
                modifier = Modifier
 | 
			
		||||
                    .height(48.dp)
 | 
			
		||||
@@ -181,6 +211,8 @@ private fun TrackInfoItem(
 | 
			
		||||
                onOpenInBrowser = onOpenInBrowser,
 | 
			
		||||
                onRemoved = onRemoved,
 | 
			
		||||
                onCopyLink = onCopyLink,
 | 
			
		||||
                private = private,
 | 
			
		||||
                onTogglePrivate = onTogglePrivate,
 | 
			
		||||
            )
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
@@ -291,6 +323,8 @@ private fun TrackInfoItemMenu(
 | 
			
		||||
    onOpenInBrowser: () -> Unit,
 | 
			
		||||
    onRemoved: () -> Unit,
 | 
			
		||||
    onCopyLink: () -> Unit,
 | 
			
		||||
    private: Boolean,
 | 
			
		||||
    onTogglePrivate: (() -> Unit)?,
 | 
			
		||||
) {
 | 
			
		||||
    var expanded by remember { mutableStateOf(false) }
 | 
			
		||||
    Box(modifier = Modifier.wrapContentSize(Alignment.TopStart)) {
 | 
			
		||||
@@ -318,6 +352,25 @@ private fun TrackInfoItemMenu(
 | 
			
		||||
                    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(
 | 
			
		||||
                text = { Text(stringResource(MR.strings.action_remove)) },
 | 
			
		||||
                onClick = {
 | 
			
		||||
 
 | 
			
		||||
@@ -25,7 +25,9 @@ internal class TrackInfoDialogHomePreviewProvider :
 | 
			
		||||
        remoteUrl = "https://example.com",
 | 
			
		||||
        startDate = 0L,
 | 
			
		||||
        finishDate = 0L,
 | 
			
		||||
        private = false,
 | 
			
		||||
    )
 | 
			
		||||
    private val privateTrack = aTrack.copy(private = true)
 | 
			
		||||
    private val trackItemWithoutTrack = TrackItem(
 | 
			
		||||
        track = null,
 | 
			
		||||
        tracker = DummyTracker(
 | 
			
		||||
@@ -40,6 +42,13 @@ internal class TrackInfoDialogHomePreviewProvider :
 | 
			
		||||
            name = "Example Tracker 2",
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
    private val trackItemWithPrivateTrack = TrackItem(
 | 
			
		||||
        track = privateTrack,
 | 
			
		||||
        tracker = DummyTracker(
 | 
			
		||||
            id = 2L,
 | 
			
		||||
            name = "Example Tracker 2",
 | 
			
		||||
        ),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    private val trackersWithAndWithoutTrack = @Composable {
 | 
			
		||||
        TrackInfoDialogHome(
 | 
			
		||||
@@ -57,6 +66,7 @@ internal class TrackInfoDialogHomePreviewProvider :
 | 
			
		||||
            onOpenInBrowser = {},
 | 
			
		||||
            onRemoved = {},
 | 
			
		||||
            onCopyLink = {},
 | 
			
		||||
            onTogglePrivate = {},
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -73,6 +83,24 @@ internal class TrackInfoDialogHomePreviewProvider :
 | 
			
		||||
            onOpenInBrowser = {},
 | 
			
		||||
            onRemoved = {},
 | 
			
		||||
            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(
 | 
			
		||||
            trackersWithAndWithoutTrack,
 | 
			
		||||
            noTrackers,
 | 
			
		||||
            trackerWithPrivateTracking,
 | 
			
		||||
        )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -33,6 +33,7 @@ import androidx.compose.material.icons.Icons
 | 
			
		||||
import androidx.compose.material.icons.automirrored.outlined.ArrowBack
 | 
			
		||||
import androidx.compose.material.icons.filled.CheckCircle
 | 
			
		||||
import androidx.compose.material.icons.filled.Close
 | 
			
		||||
import androidx.compose.material.icons.filled.VisibilityOff
 | 
			
		||||
import androidx.compose.material3.Button
 | 
			
		||||
import androidx.compose.material3.ButtonDefaults
 | 
			
		||||
import androidx.compose.material3.DropdownMenuItem
 | 
			
		||||
@@ -90,8 +91,9 @@ fun TrackerSearch(
 | 
			
		||||
    queryResult: Result<List<TrackSearch>>?,
 | 
			
		||||
    selected: TrackSearch?,
 | 
			
		||||
    onSelectedChange: (TrackSearch) -> Unit,
 | 
			
		||||
    onConfirmSelection: () -> Unit,
 | 
			
		||||
    onConfirmSelection: (private: Boolean) -> Unit,
 | 
			
		||||
    onDismissRequest: () -> Unit,
 | 
			
		||||
    supportsPrivateTracking: Boolean,
 | 
			
		||||
) {
 | 
			
		||||
    val focusManager = LocalFocusManager.current
 | 
			
		||||
    val focusRequester = remember { FocusRequester() }
 | 
			
		||||
@@ -164,15 +166,31 @@ fun TrackerSearch(
 | 
			
		||||
                enter = fadeIn() + slideInVertically { it / 2 },
 | 
			
		||||
                exit = slideOutVertically { it / 2 } + fadeOut(),
 | 
			
		||||
            ) {
 | 
			
		||||
                Button(
 | 
			
		||||
                    onClick = { onConfirmSelection() },
 | 
			
		||||
                Row(
 | 
			
		||||
                    modifier = Modifier
 | 
			
		||||
                        .padding(12.dp)
 | 
			
		||||
                        .padding(MaterialTheme.padding.small)
 | 
			
		||||
                        .windowInsetsPadding(WindowInsets.navigationBars)
 | 
			
		||||
                        .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),
 | 
			
		||||
                            )
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
 | 
			
		||||
            onSelectedChange = {},
 | 
			
		||||
            onConfirmSelection = {},
 | 
			
		||||
            onDismissRequest = {},
 | 
			
		||||
            supportsPrivateTracking = false,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    private val fullPageWithoutSelected = @Composable {
 | 
			
		||||
@@ -31,6 +32,7 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
 | 
			
		||||
            onSelectedChange = {},
 | 
			
		||||
            onConfirmSelection = {},
 | 
			
		||||
            onDismissRequest = {},
 | 
			
		||||
            supportsPrivateTracking = false,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
    private val loading = @Composable {
 | 
			
		||||
@@ -42,12 +44,27 @@ internal class TrackerSearchPreviewProvider : PreviewParameterProvider<@Composab
 | 
			
		||||
            onSelectedChange = {},
 | 
			
		||||
            onConfirmSelection = {},
 | 
			
		||||
            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(
 | 
			
		||||
        fullPageWithSecondSelected,
 | 
			
		||||
        fullPageWithoutSelected,
 | 
			
		||||
        loading,
 | 
			
		||||
        fullPageWithPrivateTracking,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    private fun someTrackSearches(): Sequence<TrackSearch> = sequence {
 | 
			
		||||
 
 | 
			
		||||
@@ -25,6 +25,7 @@ data class BackupTracking(
 | 
			
		||||
    @ProtoNumber(10) var startedReadingDate: Long = 0,
 | 
			
		||||
    // finishedReadingDate is called endReadTime in 1.x
 | 
			
		||||
    @ProtoNumber(11) var finishedReadingDate: Long = 0,
 | 
			
		||||
    @ProtoNumber(12) var private: Boolean = false,
 | 
			
		||||
    @ProtoNumber(100) var mediaId: Long = 0,
 | 
			
		||||
) {
 | 
			
		||||
 | 
			
		||||
@@ -48,6 +49,7 @@ data class BackupTracking(
 | 
			
		||||
            startDate = this@BackupTracking.startedReadingDate,
 | 
			
		||||
            finishDate = this@BackupTracking.finishedReadingDate,
 | 
			
		||||
            remoteUrl = this@BackupTracking.trackingUrl,
 | 
			
		||||
            private = this@BackupTracking.private,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
@@ -66,6 +68,7 @@ val backupTrackMapper = {
 | 
			
		||||
        remoteUrl: String,
 | 
			
		||||
        startDate: Long,
 | 
			
		||||
        finishDate: Long,
 | 
			
		||||
        private: Boolean,
 | 
			
		||||
    ->
 | 
			
		||||
    BackupTracking(
 | 
			
		||||
        syncId = syncId.toInt(),
 | 
			
		||||
@@ -80,5 +83,6 @@ val backupTrackMapper = {
 | 
			
		||||
        startedReadingDate = startDate,
 | 
			
		||||
        finishedReadingDate = finishDate,
 | 
			
		||||
        trackingUrl = remoteUrl,
 | 
			
		||||
        private = private,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -404,6 +404,7 @@ class MangaRestorer(
 | 
			
		||||
                        track.remoteUrl,
 | 
			
		||||
                        track.startDate,
 | 
			
		||||
                        track.finishDate,
 | 
			
		||||
                        track.private,
 | 
			
		||||
                        track.id,
 | 
			
		||||
                    )
 | 
			
		||||
                }
 | 
			
		||||
 
 | 
			
		||||
@@ -32,12 +32,15 @@ interface Track : Serializable {
 | 
			
		||||
 | 
			
		||||
    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
 | 
			
		||||
        score = other.score
 | 
			
		||||
        status = other.status
 | 
			
		||||
        started_reading_date = other.started_reading_date
 | 
			
		||||
        finished_reading_date = other.finished_reading_date
 | 
			
		||||
        if (copyRemotePrivate) private = other.private
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    companion object {
 | 
			
		||||
 
 | 
			
		||||
@@ -29,4 +29,6 @@ class TrackImpl : Track {
 | 
			
		||||
    override var finished_reading_date: Long = 0
 | 
			
		||||
 | 
			
		||||
    override var tracking_url: String = ""
 | 
			
		||||
 | 
			
		||||
    override var private: Boolean = false
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -37,6 +37,8 @@ abstract class BaseTracker(
 | 
			
		||||
    // Application and remote support for reading dates
 | 
			
		||||
    override val supportsReadingDates: Boolean = false
 | 
			
		||||
 | 
			
		||||
    override val supportsPrivateTracking: Boolean = false
 | 
			
		||||
 | 
			
		||||
    // TODO: Store all scores as 10 point in the future maybe?
 | 
			
		||||
    override fun get10PointScore(track: DomainTrack): Double {
 | 
			
		||||
        return track.score
 | 
			
		||||
@@ -120,6 +122,11 @@ abstract class BaseTracker(
 | 
			
		||||
        updateRemote(track)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    override suspend fun setRemotePrivate(track: Track, private: Boolean) {
 | 
			
		||||
        track.private = private
 | 
			
		||||
        updateRemote(track)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private suspend fun updateRemote(track: Track): Unit = withIOContext {
 | 
			
		||||
        try {
 | 
			
		||||
            update(track)
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,8 @@ interface Tracker {
 | 
			
		||||
    // Application and remote support for reading dates
 | 
			
		||||
    val supportsReadingDates: Boolean
 | 
			
		||||
 | 
			
		||||
    val supportsPrivateTracking: Boolean
 | 
			
		||||
 | 
			
		||||
    @ColorInt
 | 
			
		||||
    fun getLogoColor(): Int
 | 
			
		||||
 | 
			
		||||
@@ -82,4 +84,6 @@ interface Tracker {
 | 
			
		||||
    suspend fun setRemoteStartDate(track: Track, epochMillis: Long)
 | 
			
		||||
 | 
			
		||||
    suspend fun setRemoteFinishDate(track: Track, epochMillis: Long)
 | 
			
		||||
 | 
			
		||||
    suspend fun setRemotePrivate(track: Track, private: Boolean)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ import eu.kanade.tachiyomi.data.track.model.TrackSearch
 | 
			
		||||
import kotlinx.collections.immutable.ImmutableList
 | 
			
		||||
import kotlinx.collections.immutable.persistentListOf
 | 
			
		||||
import kotlinx.collections.immutable.toImmutableList
 | 
			
		||||
import kotlinx.serialization.encodeToString
 | 
			
		||||
import kotlinx.serialization.json.Json
 | 
			
		||||
import tachiyomi.i18n.MR
 | 
			
		||||
import uy.kohesive.injekt.injectLazy
 | 
			
		||||
@@ -43,6 +42,8 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
 | 
			
		||||
 | 
			
		||||
    override val supportsReadingDates: Boolean = true
 | 
			
		||||
 | 
			
		||||
    override val supportsPrivateTracking: Boolean = true
 | 
			
		||||
 | 
			
		||||
    private val scorePreference = trackPreferences.anilistScoreType()
 | 
			
		||||
 | 
			
		||||
    init {
 | 
			
		||||
@@ -183,7 +184,7 @@ class Anilist(id: Long) : BaseTracker(id, "AniList"), DeletableTracker {
 | 
			
		||||
    override suspend fun bind(track: Track, hasReadChapters: Boolean): Track {
 | 
			
		||||
        val remoteTrack = api.findLibManga(track, getUsername().toInt())
 | 
			
		||||
        return if (remoteTrack != null) {
 | 
			
		||||
            track.copyPersonalFrom(remoteTrack)
 | 
			
		||||
            track.copyPersonalFrom(remoteTrack, copyRemotePrivate = false)
 | 
			
		||||
            track.library_id = remoteTrack.library_id
 | 
			
		||||
 | 
			
		||||
            if (track.status != COMPLETED) {
 | 
			
		||||
 
 | 
			
		||||
@@ -42,8 +42,8 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
 | 
			
		||||
    suspend fun addLibManga(track: Track): Track {
 | 
			
		||||
        return withIOContext {
 | 
			
		||||
            val query = """
 | 
			
		||||
            |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus) {
 | 
			
		||||
                |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status) {
 | 
			
		||||
            |mutation AddManga(${'$'}mangaId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean) {
 | 
			
		||||
                |SaveMediaListEntry (mediaId: ${'$'}mangaId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private) {
 | 
			
		||||
                |   id
 | 
			
		||||
                |   status
 | 
			
		||||
                |}
 | 
			
		||||
@@ -56,6 +56,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
 | 
			
		||||
                    put("mangaId", track.remote_id)
 | 
			
		||||
                    put("progress", track.last_chapter_read.toInt())
 | 
			
		||||
                    put("status", track.toApiStatus())
 | 
			
		||||
                    put("private", track.private)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            with(json) {
 | 
			
		||||
@@ -79,11 +80,11 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
 | 
			
		||||
        return withIOContext {
 | 
			
		||||
            val query = """
 | 
			
		||||
            |mutation UpdateManga(
 | 
			
		||||
                |${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus,
 | 
			
		||||
                |${'$'}listId: Int, ${'$'}progress: Int, ${'$'}status: MediaListStatus, ${'$'}private: Boolean,
 | 
			
		||||
                |${'$'}score: Int, ${'$'}startedAt: FuzzyDateInput, ${'$'}completedAt: FuzzyDateInput
 | 
			
		||||
            |) {
 | 
			
		||||
                |SaveMediaListEntry(
 | 
			
		||||
                    |id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status,
 | 
			
		||||
                    |id: ${'$'}listId, progress: ${'$'}progress, status: ${'$'}status, private: ${'$'}private,
 | 
			
		||||
                    |scoreRaw: ${'$'}score, startedAt: ${'$'}startedAt, completedAt: ${'$'}completedAt
 | 
			
		||||
                |) {
 | 
			
		||||
                    |id
 | 
			
		||||
@@ -102,6 +103,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
 | 
			
		||||
                    put("score", track.score.toInt())
 | 
			
		||||
                    put("startedAt", createDate(track.started_reading_date))
 | 
			
		||||
                    put("completedAt", createDate(track.finished_reading_date))
 | 
			
		||||
                    put("private", track.private)
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            authClient.newCall(POST(API_URL, body = payload.toString().toRequestBody(jsonMime)))
 | 
			
		||||
@@ -190,6 +192,7 @@ class AnilistApi(val client: OkHttpClient, interceptor: AnilistInterceptor) {
 | 
			
		||||
                        |status
 | 
			
		||||
                        |scoreRaw: score(format: POINT_100)
 | 
			
		||||
                        |progress
 | 
			
		||||
                        |private
 | 
			
		||||
                        |startedAt {
 | 
			
		||||
                            |year
 | 
			
		||||
                            |month
 | 
			
		||||
 
 | 
			
		||||
@@ -49,6 +49,7 @@ data class ALUserManga(
 | 
			
		||||
    val startDateFuzzy: Long,
 | 
			
		||||
    val completedDateFuzzy: Long,
 | 
			
		||||
    val manga: ALManga,
 | 
			
		||||
    val private: Boolean,
 | 
			
		||||
) {
 | 
			
		||||
    fun toTrack() = Track.create(TrackerManager.ANILIST).apply {
 | 
			
		||||
        remote_id = manga.remoteId
 | 
			
		||||
@@ -60,6 +61,7 @@ data class ALUserManga(
 | 
			
		||||
        last_chapter_read = chaptersRead.toDouble()
 | 
			
		||||
        library_id = libraryId
 | 
			
		||||
        total_chapters = manga.totalChapters
 | 
			
		||||
        private = this@ALUserManga.private
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    private fun toTrackStatus() = when (listStatus) {
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ data class ALUserListItem(
 | 
			
		||||
    val startedAt: ALFuzzyDate,
 | 
			
		||||
    val completedAt: ALFuzzyDate,
 | 
			
		||||
    val media: ALSearchItem,
 | 
			
		||||
    val private: Boolean,
 | 
			
		||||
) {
 | 
			
		||||
    fun toALUserManga(): ALUserManga {
 | 
			
		||||
        return ALUserManga(
 | 
			
		||||
@@ -38,6 +39,7 @@ data class ALUserListItem(
 | 
			
		||||
            startDateFuzzy = startedAt.toEpochMilli(),
 | 
			
		||||
            completedDateFuzzy = completedAt.toEpochMilli(),
 | 
			
		||||
            manga = media.toALManga(),
 | 
			
		||||
            private = private,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,8 @@ class Bangumi(id: Long) : BaseTracker(id, "Bangumi") {
 | 
			
		||||
 | 
			
		||||
    private val api by lazy { BangumiApi(id, client, interceptor) }
 | 
			
		||||
 | 
			
		||||
    override val supportsPrivateTracking: Boolean = true
 | 
			
		||||
 | 
			
		||||
    override fun getScoreList(): ImmutableList<String> = SCORE_LIST
 | 
			
		||||
 | 
			
		||||
    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 {
 | 
			
		||||
        val statusTrack = api.statusLibManga(track, getUsername())
 | 
			
		||||
        return if (statusTrack != null) {
 | 
			
		||||
            track.copyPersonalFrom(statusTrack)
 | 
			
		||||
            track.copyPersonalFrom(statusTrack, copyRemotePrivate = false)
 | 
			
		||||
            track.library_id = statusTrack.library_id
 | 
			
		||||
            track.score = statusTrack.score
 | 
			
		||||
            track.last_chapter_read = statusTrack.last_chapter_read
 | 
			
		||||
 
 | 
			
		||||
@@ -45,6 +45,7 @@ class BangumiApi(
 | 
			
		||||
                put("type", track.toApiStatus())
 | 
			
		||||
                put("rate", track.score.toInt().coerceIn(0, 10))
 | 
			
		||||
                put("ep_status", track.last_chapter_read.toInt())
 | 
			
		||||
                put("private", track.private)
 | 
			
		||||
            }
 | 
			
		||||
                .toString()
 | 
			
		||||
                .toRequestBody()
 | 
			
		||||
@@ -62,6 +63,7 @@ class BangumiApi(
 | 
			
		||||
                put("type", track.toApiStatus())
 | 
			
		||||
                put("rate", track.score.toInt().coerceIn(0, 10))
 | 
			
		||||
                put("ep_status", track.last_chapter_read.toInt())
 | 
			
		||||
                put("private", track.private)
 | 
			
		||||
            }
 | 
			
		||||
                .toString()
 | 
			
		||||
                .toRequestBody()
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,8 @@ class TrackSearch : Track {
 | 
			
		||||
 | 
			
		||||
    override var finished_reading_date: Long = 0
 | 
			
		||||
 | 
			
		||||
    override var private: Boolean = false
 | 
			
		||||
 | 
			
		||||
    override lateinit var tracking_url: String
 | 
			
		||||
 | 
			
		||||
    var cover_url: String = ""
 | 
			
		||||
 
 | 
			
		||||
@@ -172,6 +172,7 @@ data class TrackInfoDialogHomeScreen(
 | 
			
		||||
                )
 | 
			
		||||
            },
 | 
			
		||||
            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> {
 | 
			
		||||
            val loggedInTrackers = Injekt.get<TrackerManager>().loggedInTrackers()
 | 
			
		||||
            val source = Injekt.get<SourceManager>().getOrStub(sourceId)
 | 
			
		||||
@@ -673,11 +680,14 @@ data class TrackerSearchScreen(
 | 
			
		||||
            queryResult = state.queryResult,
 | 
			
		||||
            selected = state.selected,
 | 
			
		||||
            onSelectedChange = screenModel::updateSelection,
 | 
			
		||||
            onConfirmSelection = {
 | 
			
		||||
                screenModel.registerTracking(state.selected!!)
 | 
			
		||||
            onConfirmSelection = f@{ private: Boolean ->
 | 
			
		||||
                val selected = state.selected ?: return@f
 | 
			
		||||
                selected.private = private
 | 
			
		||||
                screenModel.registerTracking(selected)
 | 
			
		||||
                navigator.pop()
 | 
			
		||||
            },
 | 
			
		||||
            onDismissRequest = navigator::pop,
 | 
			
		||||
            supportsPrivateTracking = screenModel.supportsPrivateTracking,
 | 
			
		||||
        )
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
@@ -688,6 +698,8 @@ data class TrackerSearchScreen(
 | 
			
		||||
        private val tracker: Tracker,
 | 
			
		||||
    ) : StateScreenModel<Model.State>(State()) {
 | 
			
		||||
 | 
			
		||||
        val supportsPrivateTracking = tracker.supportsPrivateTracking
 | 
			
		||||
 | 
			
		||||
        init {
 | 
			
		||||
            // Run search on first launch
 | 
			
		||||
            if (initialQuery.isNotBlank()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ data class DummyTracker(
 | 
			
		||||
    override val id: Long,
 | 
			
		||||
    override val name: String,
 | 
			
		||||
    override val supportsReadingDates: Boolean = false,
 | 
			
		||||
    override val supportsPrivateTracking: Boolean = false,
 | 
			
		||||
    override val isLoggedIn: Boolean = false,
 | 
			
		||||
    override val isLoggedInFlow: Flow<Boolean> = flowOf(false),
 | 
			
		||||
    val valLogoColor: Int = Color.rgb(18, 25, 35),
 | 
			
		||||
@@ -119,4 +120,9 @@ data class DummyTracker(
 | 
			
		||||
        track: eu.kanade.tachiyomi.data.database.models.Track,
 | 
			
		||||
        epochMillis: Long,
 | 
			
		||||
    ) = Unit
 | 
			
		||||
 | 
			
		||||
    override suspend fun setRemotePrivate(
 | 
			
		||||
        track: eu.kanade.tachiyomi.data.database.models.Track,
 | 
			
		||||
        private: Boolean,
 | 
			
		||||
    ) = Unit
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -17,6 +17,7 @@ object TrackMapper {
 | 
			
		||||
        remoteUrl: String,
 | 
			
		||||
        startDate: Long,
 | 
			
		||||
        finishDate: Long,
 | 
			
		||||
        private: Boolean,
 | 
			
		||||
    ): Track = Track(
 | 
			
		||||
        id = id,
 | 
			
		||||
        mangaId = mangaId,
 | 
			
		||||
@@ -31,5 +32,6 @@ object TrackMapper {
 | 
			
		||||
        remoteUrl = remoteUrl,
 | 
			
		||||
        startDate = startDate,
 | 
			
		||||
        finishDate = finishDate,
 | 
			
		||||
        private = private,
 | 
			
		||||
    )
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -64,6 +64,7 @@ class TrackRepositoryImpl(
 | 
			
		||||
                    remoteUrl = mangaTrack.remoteUrl,
 | 
			
		||||
                    startDate = mangaTrack.startDate,
 | 
			
		||||
                    finishDate = mangaTrack.finishDate,
 | 
			
		||||
                    private = mangaTrack.private,
 | 
			
		||||
                )
 | 
			
		||||
            }
 | 
			
		||||
        }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,5 @@
 | 
			
		||||
import kotlin.Boolean;
 | 
			
		||||
 | 
			
		||||
CREATE TABLE manga_sync(
 | 
			
		||||
    _id INTEGER NOT NULL PRIMARY KEY,
 | 
			
		||||
    manga_id INTEGER NOT NULL,
 | 
			
		||||
@@ -12,6 +14,7 @@ CREATE TABLE manga_sync(
 | 
			
		||||
    remote_url TEXT NOT NULL,
 | 
			
		||||
    start_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,
 | 
			
		||||
    FOREIGN KEY(manga_id) REFERENCES mangas (_id)
 | 
			
		||||
    ON DELETE CASCADE
 | 
			
		||||
@@ -36,8 +39,8 @@ FROM manga_sync
 | 
			
		||||
WHERE manga_id = :mangaId;
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
VALUES (:mangaId,:syncId,:remoteId,:libraryId,:title,:lastChapterRead,:totalChapters,:status,:score,:remoteUrl,:startDate,:finishDate);
 | 
			
		||||
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,:private);
 | 
			
		||||
 | 
			
		||||
update:
 | 
			
		||||
UPDATE manga_sync
 | 
			
		||||
@@ -53,5 +56,6 @@ SET
 | 
			
		||||
    score = coalesce(:score, score),
 | 
			
		||||
    remote_url = coalesce(:trackingUrl, remote_url),
 | 
			
		||||
    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;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								data/src/main/sqldelight/tachiyomi/migrations/4.sqm
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								data/src/main/sqldelight/tachiyomi/migrations/4.sqm
									
									
									
									
									
										Normal 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;
 | 
			
		||||
@@ -16,4 +16,5 @@ data class Track(
 | 
			
		||||
    val remoteUrl: String,
 | 
			
		||||
    val startDate: Long,
 | 
			
		||||
    val finishDate: Long,
 | 
			
		||||
    val private: Boolean,
 | 
			
		||||
) : Serializable
 | 
			
		||||
 
 | 
			
		||||
@@ -521,7 +521,6 @@
 | 
			
		||||
    <string name="enhanced_services">Enhanced trackers</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="action_track">Track</string>
 | 
			
		||||
    <string name="track_activity_name">Tracker login</string>
 | 
			
		||||
 | 
			
		||||
      <!-- Browse section -->
 | 
			
		||||
@@ -752,6 +751,7 @@
 | 
			
		||||
    <!-- Tracking Screen -->
 | 
			
		||||
    <string name="manga_tracking_tab">Tracking</string>
 | 
			
		||||
    <string name="add_tracking">Add tracking</string>
 | 
			
		||||
    <string name="action_track">Track</string>
 | 
			
		||||
    <string name="unread">Unread</string>
 | 
			
		||||
    <string name="reading">Reading</string>
 | 
			
		||||
    <string name="completed">Completed</string>
 | 
			
		||||
@@ -771,6 +771,9 @@
 | 
			
		||||
    <string name="track_status">Status</string>
 | 
			
		||||
    <string name="track_started_reading_date">Start 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="myanimelist_relogin">Please login to MAL again</string>
 | 
			
		||||
    <string name="source_unsupported">Source is not supported</string>
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user