Add continue watching, next-up card, API-key user selection, and scoped progress tracking#107
Conversation
Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/ed8720eb-9125-482b-a04e-3c681ed97b78 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/ed8720eb-9125-482b-a04e-3c681ed97b78 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/ed8720eb-9125-482b-a04e-3c681ed97b78 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/ed8720eb-9125-482b-a04e-3c681ed97b78 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/cde8024f-c93d-4829-88dc-bfb1f969e5bc Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/cde8024f-c93d-4829-88dc-bfb1f969e5bc Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
…h api key Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/cde8024f-c93d-4829-88dc-bfb1f969e5bc Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
…selection - Add `hasExplicitUserSelection` flag to SecurePreferences - Set flag false during API key auth (auto-pick), true during username/password auth - Set flag true when user explicitly selects a user in Settings - Gate continue-watching load in LibraryViewModel on this flag - Fix coil import (coil2 not coil3), add missing strings, fix indentation, fix line length - Also complete prior work: user selection w/ avatar in Settings, Next Up card in player Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/cde8024f-c93d-4829-88dc-bfb1f969e5bc Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
Reviewer's GuideImplements Jellyfin-style continue watching and next-up playback features with scoped server-side progress tracking, plus an API-key user selector that gates which user’s data is used and whether continue watching is visible. Sequence diagram for continue watching playback and server progress trackingsequenceDiagram
actor User
participant LibraryScreen
participant AppNavigation
participant PlayerScreen
participant PlayerViewModel
participant MediaRepository
participant SecurePreferences
participant JellyfinApiService
participant JellyfinServer
User->>LibraryScreen: Tap continue watching item
LibraryScreen->>AppNavigation: onContinueWatchingClick(itemId)
AppNavigation->>AppNavigation: Screen.Player.createRoute(itemId, trackProgress=true)
AppNavigation->>PlayerScreen: Navigate with itemId, trackProgressEnabled=true
PlayerScreen->>PlayerViewModel: loadMediaItem(itemId, trackProgressToServer=true)
PlayerViewModel->>PlayerViewModel: Reset lastProgressReportAtMs, hasMarkedPlayedForCurrentItem
PlayerViewModel->>SecurePreferences: getUserId()
SecurePreferences-->>PlayerViewModel: userId
PlayerViewModel->>MediaRepository: getItemResult(userId, itemId, fields with RunTimeTicks, UserData)
MediaRepository->>JellyfinApiService: getItem
JellyfinApiService->>JellyfinServer: GET /Users/{userId}/Items/{itemId}
JellyfinServer-->>JellyfinApiService: MediaItem JSON
JellyfinApiService-->>MediaRepository: MediaItem
MediaRepository-->>PlayerViewModel: Result(MediaItem)
PlayerViewModel->>PlayerViewModel: Compute duration, resumePositionMs from userData when trackProgressToServer=true
PlayerViewModel-->>PlayerScreen: uiState with trackProgressToServer=true, resumePositionMs>0
PlayerScreen->>PlayerScreen: LaunchedEffect resumes ExoPlayer
PlayerScreen->>PlayerScreen: player.seekTo(resumePositionMs)
loop Periodic playback updates
PlayerScreen->>PlayerViewModel: updatePlaybackState(isPlaying=true, currentPosition, bufferedPosition)
PlayerViewModel->>PlayerViewModel: maybeReportWatchProgress(isPlaying, positionMs)
alt trackProgressToServer && interval >= 15s
PlayerViewModel->>SecurePreferences: getUserId()
SecurePreferences-->>PlayerViewModel: userId
PlayerViewModel->>MediaRepository: updateUserItemData(itemId, userId, UpdateUserItemDataDto)
MediaRepository->>JellyfinApiService: updateUserItemData
JellyfinApiService->>JellyfinServer: POST /UserItems/{itemId}/UserData
JellyfinServer-->>JellyfinApiService: 204 No Content
else Throttled or tracking disabled
PlayerViewModel-->>PlayerViewModel: Skip report
end
end
User->>PlayerScreen: Back or playback reaches end
PlayerScreen->>PlayerViewModel: flushWatchProgress(positionMs or currentPosition)
PlayerViewModel->>PlayerViewModel: reportWatchProgress(force=true, markPlayedIfComplete=true)
PlayerViewModel->>MediaRepository: updateUserItemData(...)
alt playedPercentage >= 95 and not hasMarkedPlayedForCurrentItem
PlayerViewModel->>MediaRepository: markItemPlayed(itemId, userId)
MediaRepository->>JellyfinApiService: markItemPlayed
JellyfinApiService->>JellyfinServer: POST /UserPlayedItems/{itemId}
JellyfinServer-->>JellyfinApiService: 204 No Content
PlayerViewModel->>PlayerViewModel: hasMarkedPlayedForCurrentItem=true
end
rect rgb(235,235,255)
PlayerViewModel->>PlayerViewModel: handlePlaybackEnded()
PlayerViewModel->>SecurePreferences: getAutoPlayNextEpisode()
alt autoPlayNextEpisode && nextItemId != null
PlayerViewModel-->>PlayerScreen: PlayerEvent.NavigateToPlayer(nextItemId, trackProgressToServer=true)
PlayerScreen->>AppNavigation: Navigate to next Player route with trackProgress=true
end
end
Sequence diagram for Next-Up card trigger and navigationsequenceDiagram
participant PlayerScreen
participant PlayerViewModel
participant SecurePreferences
participant MediaRepository
participant JellyfinApiService
PlayerViewModel->>SecurePreferences: getUserId()
SecurePreferences-->>PlayerViewModel: userId
PlayerViewModel->>MediaRepository: getEpisodes(seriesId, userId, fields IndexNumber, ParentIndexNumber, ImageTags)
MediaRepository-->>PlayerViewModel: Response(List MediaItem)
PlayerViewModel->>PlayerViewModel: Sort episodes by season then episode
PlayerViewModel->>PlayerViewModel: Find nextEpisode after current
alt nextEpisode exists
PlayerViewModel->>JellyfinApiService: getPrimaryImageUrl(nextEpisode.id, imageTag, maxWidth=320)
JellyfinApiService-->>PlayerViewModel: imageUrl
PlayerViewModel->>PlayerViewModel: Update uiState.nextItemId, nextItemName, nextItemImageUrl
else no next episode
PlayerViewModel->>PlayerViewModel: Clear nextItemId, nextItemName, nextItemImageUrl
end
par Segments or next episode info change
PlayerViewModel->>PlayerViewModel: on segments loaded or next episode updated
PlayerViewModel->>PlayerViewModel: computeNextUpTriggerMs()
and Auto play check
PlayerViewModel->>SecurePreferences: getAutoPlayNextEpisode()
SecurePreferences-->>PlayerViewModel: autoPlayEnabled
end
PlayerViewModel->>PlayerViewModel: Determine nextUpShowAtMs based on
PlayerViewModel->>PlayerViewModel: Outro, Preview, or 10 seconds before end
PlayerViewModel-->>PlayerScreen: uiState.nextUpShowAtMs, showNextUpCard=false
loop Playback position updates
PlayerScreen->>PlayerViewModel: updatePlaybackState(isPlaying, currentPosition, bufferedPosition)
PlayerViewModel->>PlayerViewModel: if currentPosition >= nextUpShowAtMs then showNextUpCard=true
PlayerViewModel-->>PlayerScreen: uiState.showNextUpCard
PlayerScreen->>PlayerScreen: Render NextUpCard overlay when visible
end
alt User taps NextUpCard
PlayerScreen->>PlayerScreen: onPlayNextUp()
PlayerScreen->>PlayerViewModel: handlePlaybackEnded()
PlayerViewModel->>PlayerViewModel: reportWatchProgress(markPlayedIfComplete=true)
PlayerViewModel-->>PlayerScreen: PlayerEvent.NavigateToPlayer(nextItemId, trackProgressToServer)
else User dismisses card
PlayerScreen->>PlayerViewModel: dismissNextUpCard()
PlayerViewModel->>PlayerViewModel: showNextUpCard=false, nextUpShowAtMs=null
end
Updated class diagram for playback, settings, and library modelsclassDiagram
class PlayerViewModel {
-segmentRepository: SegmentRepository
-securePreferences: SecurePreferences
-httpClient: OkHttpClient
-jellyfinApiService: JellyfinApiService
-skipMeApiService: SkipMeApiService
-translationService: TranslationService
-lastProgressReportAtMs: Long
-hasMarkedPlayedForCurrentItem: Boolean
+loadMediaItem(itemId: String, trackProgressToServer: Boolean)
+updatePlaybackState(isPlaying: Boolean, currentPosition: Long, bufferedPosition: Long)
+handlePlaybackEnded()
+flushWatchProgress(positionMs: Long?)
-maybeReportWatchProgress(isPlaying: Boolean, positionMs: Long)
-reportWatchProgress(positionMs: Long, isPaused: Boolean, force: Boolean, markPlayedIfComplete: Boolean)
-findNextEpisode(mediaItem: MediaItem)
-computeNextUpTriggerMs()
+dismissNextUpCard()
+showNextUpCardNow()
}
class PlayerUiState {
+nextItemId: String?
+nextItemName: String?
+nextItemImageUrl: String?
+nextUpShowAtMs: Long?
+showNextUpCard: Boolean
+trackProgressToServer: Boolean
+resumePositionMs: Long
}
class PlayerEvent {
}
class NavigateToPlayer {
+itemId: String
+trackProgressToServer: Boolean
}
class LibraryViewModel {
-securePreferences: SecurePreferences
-mediaRepository: MediaRepository
+loadLibraries()
}
class LibraryUiState {
}
class Success {
+libraries: List~Library~
+continueWatching: List~ContinueWatchingItem~
+isSharingLibraryId: String?
+sharingProgress: Float?
}
class ContinueWatchingItem {
+id: String
+name: String
+type: String?
+seriesName: String?
+seasonNumber: Int?
+episodeNumber: Int?
+primaryImageTag: String?
+playbackPositionTicks: Long
+runTimeTicks: Long
+progress: Float
}
class SettingsViewModel {
-securePreferences: SecurePreferences
-jellyfinRepository: JellyfinRepository
-authRepository: AuthRepository
-apiService: JellyfinApiService
-translationService: TranslationService
+loadPreferences()
+loadAvailableUsers()
+selectUser(userId: String, username: String)
}
class SettingsUiState {
+isApiKeyLogin: Boolean
+availableUsers: List~UserInfo~
+isLoadingUsers: Boolean
+selectedUserId: String
+selectedUsername: String
}
class UserInfo {
+id: String
+name: String
+avatarUrl: String?
}
class JellyfinApiService {
+updateUserItemData(itemId: String, data: UpdateUserItemDataDto, userId: String?): Response~Unit~
+markItemPlayed(itemId: String, userId: String?): Response~Unit~
+getUserImageUrl(userId: String, imageTag: String?): String?
+getPrimaryImageUrl(itemId: String, imageTag: String, maxWidth: Int): String?
}
class MediaRepository {
-apiService: JellyfinApiService
+getContinueWatching(userId: String, limit: Int): Response~ItemsResponse~
+updateUserItemData(itemId: String, userId: String, data: UpdateUserItemDataDto): Response~Unit~
+markItemPlayed(itemId: String, userId: String): Response~Unit~
}
class SecurePreferences {
+saveIsApiKeyLogin(isApiKey: Boolean)
+getIsApiKeyLogin(): Boolean
+saveHasExplicitUserSelection(hasSelection: Boolean)
+getHasExplicitUserSelection(): Boolean
+saveUserId(userId: String)
+getUserId(): String?
+saveUsername(username: String)
+getUsername(): String?
+setAutoPlayNextEpisode(enabled: Boolean)
+getAutoPlayNextEpisode(): Boolean
}
class UpdateUserItemDataDto {
+playbackPositionTicks: Long?
+playedPercentage: Double?
+played: Boolean?
}
PlayerViewModel --> PlayerUiState
PlayerViewModel --> PlayerEvent
PlayerEvent <|-- NavigateToPlayer
LibraryViewModel --> LibraryUiState
LibraryUiState <|-- Success
Success --> ContinueWatchingItem
SettingsViewModel --> SettingsUiState
SettingsUiState --> UserInfo
SettingsViewModel --> JellyfinApiService
SettingsViewModel --> AuthRepository
SettingsViewModel --> SecurePreferences
LibraryViewModel --> SecurePreferences
LibraryViewModel --> MediaRepository
PlayerViewModel --> MediaRepository
PlayerViewModel --> SecurePreferences
MediaRepository --> JellyfinApiService
JellyfinApiService --> UpdateUserItemDataDto
File-Level Changes
Tips and commandsInteracting with Sourcery
Customizing Your ExperienceAccess your dashboard to:
Getting Help
|
| ) | ||
| ) { | ||
| // Pop current player from backstack to avoid loops | ||
| popUpTo("${Screen.Player.route}/{itemId}") { inclusive = true } |
There was a problem hiding this comment.
WARNING: popUpTo route pattern no longer matches the registered route
The composable route was changed to "${Screen.Player.route}/{itemId}?trackProgress={trackProgress}" in AppNavigation.kt, but popUpTo here still uses the old "${Screen.Player.route}/{itemId}". Compose Navigation matches routes exactly, so this popUpTo will silently fail to find any entry and player screens will stack up on each auto-play transition.
| popUpTo("${Screen.Player.route}/{itemId}") { inclusive = true } | |
| popUpTo("${Screen.Player.route}/{itemId}?trackProgress={trackProgress}") { inclusive = true } |
| viewModel: PlayerViewModel, | ||
| positionMs: Long? = null | ||
| ) { | ||
| viewModel.flushWatchProgress(positionMs) |
There was a problem hiding this comment.
WARNING: Progress flush is fire-and-forget — may not complete before navigation
flushWatchProgress launches a new coroutine inside viewModelScope. Navigation begins immediately afterward, and once the destination changes the ViewModel may be cleared before the HTTP request completes, silently dropping the final progress report.
Consider making flushWatchProgress a suspending function and awaiting it in a coroutine scope tied to the composable lifecycle (e.g. via rememberCoroutineScope) before calling navController.navigate/popBackStack, or at minimum use a NonCancellable context for the report.
Code Review SummaryStatus: 2 Issues Found | Recommendation: Address before merge Overview
Issue Details (click to expand)WARNING
Other Observations (not in diff)Issues found in unchanged code that cannot receive inline comments:
Incremental Update (latest commits)Changed files reviewed — no new issues found:
Files Reviewed (16 files)
Fix these issues in Kilo Cloud Reviewed by claude-4.6-sonnet-20260217 · 388,983 tokens |
…section Place the API-key user picker directly below the server info and above the Change Server button, matching the requested order: server info → user selection (API key only) → change server Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/64cadf43-1940-40b0-ac1b-2234185b352c Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/0c64161d-9178-4e72-a525-f215f951a272 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/0c64161d-9178-4e72-a525-f215f951a272 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/0c64161d-9178-4e72-a525-f215f951a272 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
…itching, user-aware continue watching refresh Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/43769a49-1d6f-4738-9e4e-3dd7c6389679 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
…eet dismiss Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/42569065-d718-48c8-8ef8-7d87894171f2 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
…layer Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/34a9126f-5cc4-4d45-9091-db9b2b97e918 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
|
Kilo Code Review could not run — your account is out of credits. Add credits or switch to a free model to enable reviews on this change. |
…ggle in player Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/34a9126f-5cc4-4d45-9091-db9b2b97e918 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
…back-navigation Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/554597d5-a182-4155-8575-bce5e910dcc6 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
…ving end-position Agent-Logs-Url: https://github.com/intro-skipper/segment-editor-mobile/sessions/a328bac4-e614-4ce9-adcf-16399506c979 Co-authored-by: AbandonedCart <1173913+AbandonedCart@users.noreply.github.com>
PlayerScreen.kt(navigateBack) — reset tracking: writefalsetoseriesEntry.savedStateHandle["trackProgress"]on pop path; droptrackProgressparam fromScreen.Series.createRouteon new-screen pathPlayerViewModel.kt(reportWatchProgress) — fix completion: whenmarkPlayedIfComplete && isComplete, callmarkItemPlayedonly (skipupdateUserItemDataso end-position is never saved); otherwise callupdateUserItemDataonly