Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
16 commits
Select commit Hold shift + click to select a range
8bf7b4f
[players] Update attachment error-state tests for the unavailable chip
frankrousseau Jul 13, 2026
a73839d
[players] Add native controls for a compared video over an image main
frankrousseau Jul 13, 2026
a9040c7
[players] Keep comparison across sub-preview nav and follow the compa…
frankrousseau Jul 13, 2026
45c048f
[playlists] Play untrimmed video sub-previews and follow the compared…
frankrousseau Jul 13, 2026
2e1e91b
[tasks] Fix use current thumb checkbox position
frankrousseau Jul 13, 2026
ca2229d
[playlists] Keep the progress strip tiled without holes
frankrousseau Jul 13, 2026
aeb6e59
[playlists] Jump the playlist playhead when switching to a picture en…
frankrousseau Jul 13, 2026
8d4f464
[playlists] Stop applying entity trim handles to sub-previews
frankrousseau Jul 13, 2026
4a23c0d
[players] Show and enforce shot trim handles in PreviewPlayer
frankrousseau Jul 13, 2026
c5dd40d
[players] Keep the canvas up during raw seeks on a playing video
frankrousseau Jul 13, 2026
3aad28a
[store] Keep entity map caches' identity across reloads
frankrousseau Jul 13, 2026
58d9169
[shots] Recompute nb_frames from the edit modal frame range again
frankrousseau Jul 13, 2026
99473e5
[players] Reload the playlist sub-preview even while playing
frankrousseau Jul 14, 2026
663331b
[playlists] Keep playing across entities that have no preview
frankrousseau Jul 14, 2026
a068726
Merge remote-tracking branch 'origin/main'
frankrousseau Jul 14, 2026
9f96618
[annotations] Restore fully erased objects as additions on undo
frankrousseau Jul 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions src/components/pages/Task.vue
Original file line number Diff line number Diff line change
Expand Up @@ -142,17 +142,6 @@
:title="$t('tasks.hookup_playlist')"
/>
</button>
<label
class="flexrow-item pointer"
v-if="isMovie && isCurrentUserManager"
>
<input
class="mr02"
type="checkbox"
v-model="isUseCurrentFrame"
/>
{{ $t('tasks.use_current_frame') }}
</label>
<button
class="button flexrow-item mr0"
:class="{
Expand All @@ -166,6 +155,17 @@
{{ $t('tasks.set_preview') }}
</span>
</button>
<label
class="flexrow-item pointer"
v-if="isMovie && isCurrentUserManager"
>
<input
class="mr02"
type="checkbox"
v-model="isUseCurrentFrame"
/>
{{ $t('tasks.use_current_frame') }}
</label>
<span class="error flexrow-item" v-if="errors.setPreview">
{{ $t('tasks.set_preview_error') }}
</span>
Expand Down
90 changes: 80 additions & 10 deletions src/components/players/players/PlaylistPlayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -446,8 +446,16 @@
? currentEntity.preview_nb_frames
: Math.round(2 * fps)
"
:handle-in="playlist.for_entity === 'shot' ? handleIn : -1"
:handle-out="playlist.for_entity === 'shot' ? handleOut : -1"
:handle-in="
playlist.for_entity === 'shot' && currentPreviewIndex === 0
? handleIn
: -1
"
:handle-out="
playlist.for_entity === 'shot' && currentPreviewIndex === 0
? handleOut
: -1
"
:preview-id="currentPreview ? currentPreview.id : ''"
@start-scrub="onScrubStart"
@end-scrub="onScrubEnd"
Expand Down Expand Up @@ -2180,6 +2188,18 @@ const playEntity = (entityIndex, updateFullPlaylist = true, frame = -1) => {
nextTick(() => {
rawPlayer.value?.loadEntity(entityIndex, 0, true)
})
// Unlike movies, pictures have no time-update channel driving the
// playlist playhead: jump it to the entity's slot here, even while
// playing. Strip clicks/drags pass updateFullPlaylist=false and keep
// their exact clicked position.
if (updateFullPlaylist && entity) {
if (isFullMode.value && !isPlaying.value) {
fullPlaylistPlayer.value.currentTime = entity.start_duration
playlistProgress.value = entity.start_duration
} else if (!isFullMode.value) {
playlistProgress.value = entity.start_duration
}
}
const ann = getAnnotation(0)
if (!isPlaying.value) loadAnnotation(ann)
if (wasDrawing) {
Expand All @@ -2188,10 +2208,13 @@ const playEntity = (entityIndex, updateFullPlaylist = true, frame = -1) => {
setAnnotationDrawingMode(true)
}, 100)
}
// Entities without any preview borrow the picture timer so continuous
// playback holds their slot and then moves on instead of stalling.
if (
isPlaying.value &&
entity &&
isPicturePreview(entity.preview_file_extension)
(isPicturePreview(entity.preview_file_extension) ||
!entity.preview_file_id)
) {
playPicture()
}
Expand Down Expand Up @@ -2628,6 +2651,7 @@ const onFrameUpdate = frame => {
if (props.playlist && isPlaying.value) {
const hasHandles =
['shot', 'edit', 'episode'].includes(props.playlist.for_entity) &&
currentPreviewIndex.value === 0 &&
handleOut.value < nbFrames.value
const reachedEnd = hasHandles
? frameNumber.value >= handleOut.value
Expand Down Expand Up @@ -3429,17 +3453,32 @@ const continuePlayingPlaylist = (entityIndex, startMs) => {
return
}
const previews = currentEntity.value?.preview_file_previews
if (previews && previews.length === currentPreviewIndex.value) {
// No previews at all (entity without preview): advance to the next
// entity, otherwise the else branch increments currentPreviewIndex
// forever and playback stalls on the empty entity.
if (!previews || previews.length === currentPreviewIndex.value) {
nextTick(() => {
onPlayNextEntity(true)
framesSeenOfPicture.value = 1
})
} else {
currentPreviewIndex.value++
nextTick(() => {
playingPictureTimeout = setTimeout(() => {
continuePlayingPlaylist(playingEntityIndex.value, Date.now())
}, 100)
// A still-image sub-preview can be followed by a video sub-preview.
// Play the whole clip from its start instead of rescheduling the image
// loop (which would freeze it and then skip it). Sub-previews are not
// the entity's trimmed main preview, so ignore the entity handle-in/out.
if (isCurrentPreviewMovie.value) {
clearTimeout(playingPictureTimeout)
rawPlayer.value?.setCurrentFrame(0)
rawPlayer.value?.play()
if (isComparing.value) rawPlayerComparison.value?.play()
isPlaying.value = true
} else {
playingPictureTimeout = setTimeout(() => {
continuePlayingPlaylist(playingEntityIndex.value, Date.now())
}, 100)
}
})
}
}
Expand Down Expand Up @@ -3895,6 +3934,14 @@ const getEntityHandles = entity => {

const resetHandles = entity => {
if (!['shot', 'edit', 'episode'].includes(props.playlist?.for_entity)) return
// No entity argument = reset for what is displayed right now. A
// sub-preview is not the entity's trimmed main preview, so the entity
// trim handles don't apply to its timeline.
if (!entity && currentPreviewIndex.value > 0) {
handleIn.value = 0
handleOut.value = nbFrames.value
return
}
entity = entity || currentEntity.value
const { handleIn: entityHandleIn, handleOut: entityHandleOut } =
getEntityHandles(entity)
Expand All @@ -3906,13 +3953,26 @@ const resetPlaylistFrameData = () => {
let playlistDur = 0
let curFrame = 0
entityList.value.forEach((entity, index) => {
// An entity without preview still spans its edit length (like a slug
// in a conform): playback holds the slot, the strip shows it in grey.
const defaultNbFrames =
entity.preview_nb_frames || 2 * fps.value * frameDuration.value
framesPerImage.value[index] = defaultNbFrames
const n =
Math.round((entity.preview_file_duration || 0) * fps.value) ||
defaultNbFrames
// Duration only counts for movie mains: a picture revision holding a
// video sub-preview carries that video's duration, and using it here
// would stretch the entity's strip slot past the width PlaylistProgress
// computes from preview_nb_frames, leaving holes in the strip.
const n = isMoviePreview(entity.preview_file_extension)
? Math.round((entity.preview_file_duration || 0) * fps.value) ||
defaultNbFrames
: defaultNbFrames
entity.start_duration = (curFrame + 1) / fps.value
// Frozen frame accounting for PlaylistProgress: the strip renders from
// these two fields only, so it tiles by construction. Recomputing widths
// from live preview fields drifts from the positions frozen here (fps is
// the *current* entity's rate, durations refresh after this pass).
entity.playlist_start_frame = curFrame
entity.playlist_nb_frames = n
for (let i = 0; i < n; i++) {
playlistShotPosition.value[curFrame + i] = {
index,
Expand Down Expand Up @@ -4316,6 +4376,16 @@ watch(currentPreviewIndex, () => {
}
})

// Keep the compared sub-preview aligned with the main one; when the compared
// revision has fewer sub-previews, stay on its last one.
watch(currentPreviewIndex, () => {
if (!isComparing.value || currentComparisonPreviewLength.value <= 0) return
currentComparisonPreviewIndex.value = Math.min(
currentPreviewIndex.value,
currentComparisonPreviewLength.value - 1
)
})

watch(playingEntityIndex, () => {
endAnnotationSaving()
resetUndoStacks()
Expand Down
Loading