[playlists] Shared playlist player + project metadata, misc fixes#1996
Merged
frankrousseau merged 22 commits intomainfrom Apr 27, 2026
Merged
[playlists] Shared playlist player + project metadata, misc fixes#1996frankrousseau merged 22 commits intomainfrom
frankrousseau merged 22 commits intomainfrom
Conversation
Convert PreviewFileList to <script setup>, switch event and login helpers to arrow functions with reactive loading state, drop dead CSS rules targeting elements that live in subcomponents, and alphabetize selectors and properties across Logs, EventLogs, LoginLogs, PreviewFiles and PreviewFileList.
Made-with: Cursor
- Merge Project descriptors across productions; CRUD/reorder for all-projects - ProductionList and Productions page: columns, cells, column picker, resize - TableMetadataSelectorMenu: v-model:is-open, positioning - Shared metadata table UX: App/MetadataInput, resizable metadata columns on lists - Locales: productions metadata strings (en/fr) - Unit tests: productions store Made-with: Cursor
Convert CustomActions, CustomActionList and EditCustomActionModal to <script setup>. Drop the formatListMixin and modalMixin in favor of a local formatBoolean helper and the useModal composable. Replace the deprecated $tc with $t pipe-format pluralization. Add a card-based responsive layout below 768px to the list with a grid of name + url + entity type + is ajax, plus mobile labels. Drop the unused .modal-content .box p.text and .is-danger CSS rules in the edit modal.
…tion Made-with: Cursor
Add a Share button in the playlist player toolbar (manager only) that opens a modal to generate, copy and revoke share links with expiration and comment options. Active links are highlighted on the button. Create a public /playlists/shared/<token> page that validates the token, asks for a guest name when comments are enabled (stored in localStorage for subsequent visits), and shows an error card for invalid/expired links.
Wire the SharedPlaylistPlayer into the shared playlist page and trigger RawVideoPlayer.loadEntity(0) on mount (the entities prop is already populated when the player mounts after the guest identity form, so the existing watcher does not fire). Thread a shared-link URL prefix through RawVideoPlayer (movieUrlPrefix) and LightEntityThumbnail (urlPrefix) so both stream media through the token-scoped routes instead of the auth'd /api/pictures and /api/movies. Add a read-only mode to PlaylistedEntity that hides editor controls and displays the task type name and version below the entity name using the preview_file_task_type_name now returned by zou. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Fill in the shared playlist player on top of the bare preview viewer: top bar with project/playlist name + entity navigation, full playback controls (play/pause, timecode, loop, HD/LD toggle, volume, zoom, entity-list toggle, fullscreen), keyboard shortcuts (space, arrows, alt+j/k, home/end), VideoProgress with annotation marks, and PlaylistProgress with tile hover previews. Wire guards around the chromium offset so scrubbing/arrows/end key reach the last frame of each clip, floor max-duration to the frame grid, and re-layout the video when the entity list is toggled. Thread a shared-link URL prefix through PlaylistProgress so its tile sprite requests go through the token-scoped /movies/tiles route instead of the auth'd /api/movies/tiles. Guard the immediate previewId watcher with optional chaining and an img.onerror handler so the spinner doesn't get stuck when a tile isn't on disk. Switch PlaylistedEntity's read-only preview meta to the TaskTypeName widget (colored tag) now that zou serialises the full task type dict. Clean up RawVideoPlayer's playLoop on beforeUnmount. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Restyle SharedPlaylist, SharedPlaylistIdentityCard, SharedPlaylistPlayer and SharedCommentsPanel with glassmorphism, purple accent, and deep-surface tones. Add a dark video-progress timeslider PNG, override Comment and PlaylistedEntity widget styles scoped to the shared view, and ship black thumbnail backgrounds. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Rebuild the post area with a two-piece compound widget: a narrow color-only ComboboxStatus (left) glued to a button-simple post button (right), matching the AddComment pattern. Dropdown opens downward, constrained to client-allowed statuses only. - Unify comment tools (emoji, attach, frame, checklist) on lucide-based square ghost buttons; restyle the emoji picker and reuse the Checklist widget with dark-theme :deep overrides. - Wire frame-reference clicks from comments and checklists back to the player via a time-code-clicked event. - Autosize the comment textarea with a min-height floor. - Darken the playlist-progress bar background and force dark theme on shared playlist mount so task status styling picks up the dark scope. - Fix the emoji button submitting the form by setting type="button". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Fix the loading spinner sometimes spinning forever on the first video: hideLoading now cancels any pending showLoading timeout, and the threshold is loosened from readyState !== 4 to readyState < 3 so the player no longer flips back to loading once playback can start. - Track container size via ResizeObserver so the video re-fits when surrounding bars hide (landscape mobile) or when the entity strip toggles, not only on window resize. - Resolve comment authors from the enriched personMap (with a derived full_name fallback) so avatars render initials/colors and PeopleName has something to display. - Allow guests to edit checklist items on their own comments and PUT the updated checklist back through the shared comment endpoint with optimistic update + rollback on error. - Restyle CommentMenu and the EditCommentModal status combobox via deep overrides so they match the shared playlist dark surface palette. - Mobile responsive pass: comments panel becomes a full-width overlay over the player, hidden by default; header collapses to logo + playlist name + logout; footer compresses (smaller gaps/padding, consistent 36px play/pause width); timecode hidden, frame counter kept; logout button pulled tighter to the right edge. - Landscape phones (max-width: 900px + landscape) hide the header, video-progress and playlist-progress to maximise the video area. - Force 100dvh sizing with overflow: hidden on the outer wrappers and flex-shrink: 0 on every fixed-height row so the page no longer overflows on iOS Safari. - Hide the Crisp chat widget on the shared playlist (restored on unmount) since guests do not have access to studio support. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Add a SharedAnnotationOverlay component that draws the existing preview annotations on a fabric StaticCanvas overlay positioned exactly over the displayed video bounds (computed from intrinsic movie dimensions to handle letterbox/pillarbox). - Match the right shape per object type, including PSStroke (the pressure-sensitive brush from fabricjs-psbrush) which fabric.Path cannot deserialize. PSStroke prototype patches are duplicated here so the overlay works without the main PreviewPlayer being loaded. - Re-render asynchronously with a render token so concurrent frame changes do not stack stale shapes on the canvas. - Hide the overlay during playback and restore it when paused. - Resize the canvas via ResizeObserver so it tracks layout changes (orientation, panel toggle, entity strip toggle). - Block right-click on the video-container to prevent guests saving the video file via the browser context menu. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Extract pure annotation helpers (PSBrush setup, pencil width buckets, pressure simulation, additions diff bookkeeping, read-only shape builder) from the Options-API mixin into src/lib/annotation.js so the shared playlist can reuse the exact same brush configuration. - Add a useSharedAnnotationCanvas composable that owns the fabric.Canvas, tracks user-drawn strokes as additions, and exposes undo / clearLocal / getDiff / reset / setDrawingMode. Uses the same PSBrush pressure fallback (0.5) and the same speed-based pressure simulation that the studio mixin applies on mouse:move so guest strokes render at the same thickness. - Make SharedAnnotationOverlay fabric-interactive when isEditable is on: drawing mode auto-enables when the toolbar appears, the toolbar stays visible after save (so the user can keep drawing on the next frame), and a PUT to /api/shared/playlists/:token/annotations ships the diff. - Wire a pencil toggle in the SharedPlaylistPlayer footer (visible when the share link allows commenting and a guest is identified). The saved-annotations event updates the entity's preview_file_annotations in place so subsequent renders pick up the new strokes without a full refetch. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ader on mobile - VideoProgress accepts a backgroundUrl prop so consumers (the shared playlist player) can swap the timeline background image without forking the component, and falls back to the default texture when the prop is empty. - Drop the mobile @media rule that flipped Comment header rows to flex-direction: column. It was wrapping each item (validation tag, avatar, name, date, menu) onto its own line in the shared comments panel; the panel now keeps everything inline and lets the name truncate via the panel-side overrides. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Recover the Arrow class drafted in #1830 as src/lib/arrowshape.js: extends fabric.Line with a triangular head, two endpoint controls for reshape, toObject / fromObject for round-tripping, and SVG export. Cleaned up the dead code from the PR draft. - Add attachShapeDrawing to src/lib/annotation.js: a fabric mouse:down / move / up listener bundle that drag-creates a rectangle, circle or arrow, paints a grey semi-transparent preview while dragging and finalises with a transparent fill on release. Calls back into the composable so finalised shapes flow through the same setObjectData / pushAddition pipeline as PSBrush strokes. - Extend buildReadOnlyShape to deserialise arrows. - Track currentTool ('pen' | 'rectangle' | 'circle' | 'arrow') in useSharedAnnotationCanvas and switch the canvas between isDrawingMode (PSBrush) and the shape listeners automatically. Shapes use a fixed SHAPE_STROKE_WIDTH (4) instead of the pencil bucket so the constant-thickness lines do not look heavier than the pressure-modulated pen. - Toolbar in SharedAnnotationOverlay now shows the four tool buttons (pen / rect / circle / arrow) with active styling, separated from the undo / clear / save actions by a divider. - Teach annotationMixin.addObjectToCanvas about rect / circle / arrow so the shapes guests draw render in the studio playlist and preview players too. The arrow import side-effect registers fabric.Arrow globally for the existing deserialisation paths. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The annotate toggle pushed the footer over the edge on a 360px-wide phone. Drop the inter-button gap and the global flexrow-item margin, shrink playlist-button min-width to 30px, padding to 0.3em / 0.25em and the icon size to 14px so the full toolbar (play, frame counter, repeat, HD, sound, comments, annotate, zoom, entities, fullscreen) now fits on small phones without horizontal scrolling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Scroll the playlisted entities strip horizontally so the entity
currently playing stays centered. Watcher on playingEntityIndex ->
scrollPlayingEntityIntoView (smooth, only when the strip is visible).
Also re-runs when the strip is unhidden so the active card is in
view straight away.
- Wire a status-changed event from SharedCommentsPanel to
SharedPlaylistPlayer: when the guest posts or edits a comment with
a different task status, the panel emits {taskStatusId, color}
(taken from comment.task_status when the backend returns it, or
from the store's taskStatusMap as a fallback). The player updates
currentEntity.task_status_color in place so PlaylistProgress
repaints the matching segment without waiting for a refetch.
- Drop the 1rem right margin .flexrow-item leaks onto the
RawVideoPlayer root: the video now fills the player area edge to
edge instead of leaving a 16px gap on the right.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RawVideoPlayer already emits panzoom-changed with {x, y, scale}
whenever the user pans or zooms (or whenever panzoom internally
recomputes on resize). SharedPlaylistPlayer now stores the latest
transform in a ref and forwards it to SharedAnnotationOverlay, which
mirrors it onto the fabric canvas via setViewportTransform([scale, 0,
0, scale, x, y]). This:
- Keeps existing strokes glued to the video while the user pans or
zooms in to inspect a detail.
- Fixes the resize regression for free: panzoom recomputes its
transform on container resize, the event fires, fabric updates its
viewport, and annotations stay aligned without the JS measuring
loops we tried before.
- Lets the guest draw while zoomed in — fabric inverse-maps pointer
events through the viewport transform, so strokes are recorded in
the underlying buffer coordinates and render correctly at any
later zoom level.
The transform resets to identity when the loupe is toggled off (the
watcher that already calls resetPanZoom now also clears the local
ref).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Drop eslint-disable directives by renaming videoProgress and playlistProgress refs that shadowed the matching component imports in SharedPlaylistPlayer. - Replace deprecated event.keyCode with event.code in the keyboard handler. - Move guest/entity display computeds after their state dependencies. - Inline the videoProgressBackgroundUrl alias. - Collapse the five render-triggering watches in SharedAnnotationOverlay into a single grouped watch. - Parallelize the sequential attachment delete loop in SharedCommentsPanel with Promise.all. - Sort imports and CSS selectors alphabetically across the five files.
Drop the raw fetch() calls from SharedPlaylist.vue and SharedAnnotationOverlay.vue and route them through new actions on the playlists store, so auth/error handling, mutations and store updates stay centralised: - API: loadSharedPlaylist, loadSharedPlaylistContext and saveSharedPlaylistAnnotations. - Actions: loadSharedPlaylistContext commits the project, task types and task statuses; postSharedPlaylistGuest now preserves the caused error. In SharedPlaylist.vue, the stored-guest restore is factored into a restoreStoredGuest helper that reuses postSharedPlaylistGuest with a guest_id payload, and the identity error is surfaced through the new errorMessage prop on SharedPlaylistIdentityCard. Adds the share.guest_error locale and reorders the share block to keep the top-level keys alphabetised.
Add a 'No direct fetch from components' subsection to the Store guide: all network calls go through an api/<entity>.js method using client.* helpers and a matching Vuex action. Add an 'Import order' subsection to the component conventions: imports are split into third-party, libs/composables and components, sorted alphabetically by source path within each block.
Pull the SharedPlaylistPlayer header and footer out into their own components: - SharedPlaylistHeader takes the project/playlist names, entity nav and guest controls; emits previous-entity, next-entity and logout. - SharedPlaylistButtonBar takes the play/pause + tool toggles, with v-model bindings for isAnnotating, isCommentsHidden, isEntitiesHidden, isHd, isMuted, isRepeating, isZoomEnabled and volume; emits play, pause, toggle-full-screen and toggle-sound. This removes ~320 lines from SharedPlaylistPlayer (template + scoped styles for .shared-header, .playlist-footer, .playlist-button, .time-indicator and the related media queries). Add a download fallback in the preview area: when the current preview is neither movie nor picture nor sound, surface a centred download link (lucide DownloadIcon + extension chip) pointing at the shared preview-file URL with the actual extension. New locale key share.download_preview.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Problems
guest-facing front-end to consume them: no playback, comments,
annotations
player and not reusable for an unauthenticated guest view
resize, and shapes drawn by guests were invisible in the studio
players
overflowed, comments panel pushed video off-screen, footer cluttered
attached to each production
Solutions
SharedPlaylistpage (token-based, no JWT) +SharedPlaylistPlayer/SharedPlaylistHeader/SharedPlaylistButtonBar/SharedCommentsPanel/SharedAnnotationOverlay, themed dark, with active-entityauto-scroll and Crisp hidden
for own comments, guest avatars, frame-click navigation, status
changes repaint the playlist-progress bar live
fabric.Canvas+PSBrushfrom anew shared
lib/annotation.js(extracted from the mixin —pencil widths, mouse pressure simulation, diff bookkeeping,
read-only shape builder); pen, rectangle, circle, arrow tools
src/lib/arrowshape.js;annotationMixin.addObjectToCanvasextended to render rect / circle / arrow so guest drawings show
up in the classic player too
(
zoom+pan+transform) so strokes follow zoom/pan anddrawing while zoomed maps back to buffer coordinates
comments panel as full-width overlay hidden by default, tight 30px
footer buttons, landscape-phones hide header + progress bars
Vuex-first network policy in
CLAUDE.mdrender them as sortable / filterable columns in the productions
table (+ store tests)
CustomActions,CustomActionList,EditCustomActionModal) to Composition API with a mobile layout