Skip to content

[playlists] Shared playlist player + project metadata, misc fixes#1996

Merged
frankrousseau merged 22 commits intomainfrom
playlist-sharing
Apr 27, 2026
Merged

[playlists] Shared playlist player + project metadata, misc fixes#1996
frankrousseau merged 22 commits intomainfrom
playlist-sharing

Conversation

@frankrousseau
Copy link
Copy Markdown
Contributor

Problems

  • Studios share playlist links with clients but Kitsu had no
    guest-facing front-end to consume them: no playback, comments,
    annotations
  • The studio annotation mixin was tightly coupled to the Options-API
    player and not reusable for an unauthenticated guest view
  • Drawing on a guest video did not follow panzoom, flickered on
    resize, and shapes drawn by guests were invisible in the studio
    players
  • Mobile / landscape-phone made the player unusable: header
    overflowed, comments panel pushed video off-screen, footer cluttered
  • All-projects list could not surface the project metadata descriptors
    attached to each production
  • Custom actions page was Options-API only and unusable on mobile
  • Task type page lacked a retake-count filter

Solutions

  • New SharedPlaylist page (token-based, no JWT) +
    SharedPlaylistPlayer / SharedPlaylistHeader /
    SharedPlaylistButtonBar / SharedCommentsPanel /
    SharedAnnotationOverlay, themed dark, with active-entity
    auto-scroll and Crisp hidden
  • Comments panel: status combobox + post button, checklist editing
    for own comments, guest avatars, frame-click navigation, status
    changes repaint the playlist-progress bar live
  • Annotation overlay backed by fabric.Canvas + PSBrush from a
    new shared lib/annotation.js (extracted from the mixin —
    pencil widths, mouse pressure simulation, diff bookkeeping,
    read-only shape builder); pen, rectangle, circle, arrow tools
  • Arrow class recovered from Implement shape annotation drawing with rectangle and circle #1830 as
    src/lib/arrowshape.js; annotationMixin.addObjectToCanvas
    extended to render rect / circle / arrow so guest drawings show
    up in the classic player too
  • Fabric viewport transform synced to RawVideoPlayer panzoom events
    (zoom + pan + transform) so strokes follow zoom/pan and
    drawing while zoomed maps back to buffer coordinates
  • Mobile pass: collapsed header (logo + playlist name + logout),
    comments panel as full-width overlay hidden by default, tight 30px
    footer buttons, landscape-phones hide header + progress bars
  • Move Shared* network calls into Vuex actions, document the
    Vuex-first network policy in CLAUDE.md
  • Load project metadata descriptors with the all-projects list and
    render them as sortable / filterable columns in the productions
    table (+ store tests)
  • Migrate Custom actions page (CustomActions, CustomActionList,
    EditCustomActionModal) to Composition API with a mobile layout
  • Add a retake-count filter to the task type page

frankrousseau and others added 22 commits April 22, 2026 18:12
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.
- 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.
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.
@frankrousseau frankrousseau merged commit b9682f1 into main Apr 27, 2026
12 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant