feat: per-annotation scroll-to flag gating vertical scroll on jump
Add :meta {:scroll-to bool} to annotations, toggled by a "Scroll to clips on
jump" checkbox in the annotation form. goto! now separates follow? (horizontal
recenter) from scroll-to? (vertical scroll to the clip's track); the vertical
scroll only fires for annotations that opt in, resolved from the canonical
scene group at each jump site (jump button/popover, script highlight, link
chips). Persists for free via editable-group + the backend's opaque group
merge; round-trips through json keywordize, so no migration needed.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
0c305e42ea
commit
602c710377
2 changed files with 39 additions and 18 deletions
|
|
@ -239,6 +239,9 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px;
|
||||||
.pt-x:hover { text-decoration: underline; }
|
.pt-x:hover { text-decoration: underline; }
|
||||||
|
|
||||||
.form-hint { font-size: 11px; color: var(--mute); }
|
.form-hint { font-size: 11px; color: var(--mute); }
|
||||||
|
.form-check { display: flex; align-items: center; gap: 6px; font-family: var(--chicago);
|
||||||
|
font-size: 11px; color: var(--ink); cursor: pointer; user-select: none; }
|
||||||
|
.form-check input { cursor: pointer; }
|
||||||
.form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
|
.form-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 4px; }
|
||||||
.save, .cancel { padding: 6px 14px; font-size: 13px; }
|
.save, .cancel { padding: 6px 14px; font-size: 13px; }
|
||||||
/* default button: the heavy double Mac ring */
|
/* default button: the heavy double Mac ring */
|
||||||
|
|
|
||||||
|
|
@ -197,9 +197,12 @@
|
||||||
:behavior "smooth"}))))))
|
:behavior "smooth"}))))))
|
||||||
|
|
||||||
(defn- goto!
|
(defn- goto!
|
||||||
"Move the playhead to local frame `local` and seek the video to match."
|
"Move the playhead to local frame `local` and seek the video to match.
|
||||||
([local] (goto! local false))
|
`follow?` recentres horizontally; `scroll-to?` also scrolls the tracks
|
||||||
([local follow?]
|
vertically to the clip under the landing frame (opt-in per annotation)."
|
||||||
|
([local] (goto! local false false))
|
||||||
|
([local follow?] (goto! local follow? false))
|
||||||
|
([local follow? scroll-to?]
|
||||||
(let [ctx @(rf/subscribe [::subs/context])
|
(let [ctx @(rf/subscribe [::subs/context])
|
||||||
segs @(rf/subscribe [::subs/segments])
|
segs @(rf/subscribe [::subs/segments])
|
||||||
fps @(rf/subscribe [::subs/fps])
|
fps @(rf/subscribe [::subs/fps])
|
||||||
|
|
@ -209,7 +212,7 @@
|
||||||
(when @play (swap! play assoc :idx (seg-at segs local))) ; keep playback in sync
|
(when @play (swap! play assoc :idx (seg-at segs local))) ; keep playback in sync
|
||||||
(when follow?
|
(when follow?
|
||||||
(r/after-render #(do (follow! fps zoom local true) ; smooth on jump/link
|
(r/after-render #(do (follow! fps zoom local true) ; smooth on jump/link
|
||||||
(scroll-to-seg-track! segs local)))))))
|
(when scroll-to? (scroll-to-seg-track! segs local))))))))
|
||||||
|
|
||||||
(defn video-monitor []
|
(defn video-monitor []
|
||||||
[:video {:src @(rf/subscribe [::subs/clip-url]) :controls true :preload "auto"
|
[:video {:src @(rf/subscribe [::subs/clip-url]) :controls true :preload "auto"
|
||||||
|
|
@ -391,9 +394,16 @@
|
||||||
(scene/link-local @(rf/subscribe [::subs/scene]) @(rf/subscribe [::subs/context])
|
(scene/link-local @(rf/subscribe [::subs/scene]) @(rf/subscribe [::subs/context])
|
||||||
{:ref (keyword ref) :at (js/parseInt at 10)}))
|
{:ref (keyword ref) :at (js/parseInt at 10)}))
|
||||||
|
|
||||||
(defn- goto-link! [local]
|
(defn- ann-scroll-to?
|
||||||
(when local
|
"Does group `a` opt into vertical scroll-to-clip when navigated to?"
|
||||||
(goto! local true)))
|
[a] (boolean (get-in a [:meta :scroll-to])))
|
||||||
|
|
||||||
|
(defn- ref-scroll-to? [scene ref]
|
||||||
|
(ann-scroll-to? (get-in scene [:groups (keyword ref)])))
|
||||||
|
|
||||||
|
(defn- goto-link!
|
||||||
|
([local] (goto-link! local false))
|
||||||
|
([local scroll-to?] (when local (goto! local true scroll-to?))))
|
||||||
|
|
||||||
(defn content-display
|
(defn content-display
|
||||||
"Read-only render of annotation `content`: markdown blocks (headings, lists,
|
"Read-only render of annotation `content`: markdown blocks (headings, lists,
|
||||||
|
|
@ -410,7 +420,7 @@
|
||||||
(let [lf (scene/link-local scene ctx {:ref ref :at at})]
|
(let [lf (scene/link-local scene ctx {:ref ref :at at})]
|
||||||
[:span.link-chip {:class (when-not lf "broken")
|
[:span.link-chip {:class (when-not lf "broken")
|
||||||
:title (when-not lf "linked clip no longer in this timeline")
|
:title (when-not lf "linked clip no longer in this timeline")
|
||||||
:on-click #(goto-link! lf)}
|
:on-click #(goto-link! lf (ref-scroll-to? scene ref))}
|
||||||
(when-not lf "△ ") label
|
(when-not lf "△ ") label
|
||||||
(when lf [:span.link-f (str " " (js/Math.round lf) "f")])])))))
|
(when lf [:span.link-f (str " " (js/Math.round lf) "f")])])))))
|
||||||
|
|
||||||
|
|
@ -483,7 +493,8 @@
|
||||||
:on-input emit :on-key-up save! :on-mouse-up save! :on-blur save!
|
:on-input emit :on-key-up save! :on-mouse-up save! :on-blur save!
|
||||||
:on-click (fn [e] (when-let [chip (.closest (.-target e) ".link-chip")]
|
:on-click (fn [e] (when-let [chip (.closest (.-target e) ".link-chip")]
|
||||||
(when-let [lf (link-frame (.. chip -dataset -ref) (.. chip -dataset -at))]
|
(when-let [lf (link-frame (.. chip -dataset -ref) (.. chip -dataset -at))]
|
||||||
(goto-link! lf))))}]))
|
(goto-link! lf (ref-scroll-to? @(rf/subscribe [::subs/scene])
|
||||||
|
(.. chip -dataset -ref))))))}]))
|
||||||
|
|
||||||
(defn- point-candidates [scene ctx q timelines?]
|
(defn- point-candidates [scene ctx q timelines?]
|
||||||
(let [needle (str/lower-case q)
|
(let [needle (str/lower-case q)
|
||||||
|
|
@ -649,9 +660,10 @@
|
||||||
than one bar) — a button that opens a popover of one target per piece, each
|
than one bar) — a button that opens a popover of one target per piece, each
|
||||||
labelled with the clip + frame it lands on (as the annotation editor shows it)."
|
labelled with the clip + frame it lands on (as the annotation editor shows it)."
|
||||||
[open a scene segs]
|
[open a scene segs]
|
||||||
(let [bars (:bars a)]
|
(let [bars (:bars a)
|
||||||
|
scroll? (ref-scroll-to? scene (:id a))]
|
||||||
(if (< (count bars) 2)
|
(if (< (count bars) 2)
|
||||||
[:button.jump-btn {:on-click #(goto! (:start a) true)} "↪ jump"]
|
[:button.jump-btn {:on-click #(goto! (:start a) true scroll?)} "↪ jump"]
|
||||||
[:span {:style {:position "relative"}}
|
[:span {:style {:position "relative"}}
|
||||||
[:button.jump-btn {:on-click #(swap! open (fn [x] (when (not= x (:id a)) (:id a))))}
|
[:button.jump-btn {:on-click #(swap! open (fn [x] (when (not= x (:id a)) (:id a))))}
|
||||||
(str "↪ jump (" (count bars) ") ▾")]
|
(str "↪ jump (" (count bars) ") ▾")]
|
||||||
|
|
@ -659,7 +671,7 @@
|
||||||
[:div.jump-pop
|
[:div.jump-pop
|
||||||
(for [[i [lo _]] (map-indexed vector bars)]
|
(for [[i [lo _]] (map-indexed vector bars)]
|
||||||
^{:key i}
|
^{:key i}
|
||||||
[:button {:on-click #(do (goto! lo true) (reset! open nil))}
|
[:button {:on-click #(do (goto! lo true scroll?) (reset! open nil))}
|
||||||
(str "▸ " (clip-at scene segs lo))])])])))
|
(str "▸ " (clip-at scene segs lo))])])])))
|
||||||
|
|
||||||
(defn commentary []
|
(defn commentary []
|
||||||
|
|
@ -746,11 +758,16 @@
|
||||||
[:form.form {:on-submit (fn [e] (.preventDefault e) (save))}
|
[:form.form {:on-submit (fn [e] (.preventDefault e) (save))}
|
||||||
[:div.form-head (cond root? "Edit description" new? "New annotation" :else "Edit annotation")]
|
[:div.form-head (cond root? "Edit description" new? "New annotation" :else "Edit annotation")]
|
||||||
(when-not root?
|
(when-not root?
|
||||||
|
[:<>
|
||||||
[:div.form-row
|
[:div.form-row
|
||||||
[:input.form-name {:placeholder "Name" :value (:name d)
|
[:input.form-name {:placeholder "Name" :value (:name d)
|
||||||
:on-change #(put (assoc d :name (.. % -target -value)))}]
|
:on-change #(put (assoc d :name (.. % -target -value)))}]
|
||||||
[:input.form-color {:type "color" :value (:color d)
|
[:input.form-color {:type "color" :value (:color d)
|
||||||
:on-change #(put (assoc d :color (.. % -target -value)))}]])
|
:on-change #(put (assoc d :color (.. % -target -value)))}]]
|
||||||
|
[:label.form-check {:title "When you jump to this annotation, scroll the tracks to its clips"}
|
||||||
|
[:input {:type "checkbox" :checked (boolean (get-in d [:meta :scroll-to]))
|
||||||
|
:on-change #(put (assoc-in d [:meta :scroll-to] (.. % -target -checked)))}]
|
||||||
|
[:span "Scroll to clips on jump"]]])
|
||||||
[:div.form-marks-label "Content"]
|
[:div.form-marks-label "Content"]
|
||||||
^{:key gid} [content-editor gid (:content orig)]
|
^{:key gid} [content-editor gid (:content orig)]
|
||||||
(if (and linking (= gid (:gid linking)))
|
(if (and linking (= gid (:gid linking)))
|
||||||
|
|
@ -905,7 +922,8 @@
|
||||||
(let [[x0 y0 x1 y1] rect]
|
(let [[x0 y0 x1 y1] rect]
|
||||||
[:div.script-hl {:title (:name a) :data-ann (name (:id a))
|
[:div.script-hl {:title (:name a) :data-ann (name (:id a))
|
||||||
:on-mouse-down #(.stopPropagation %)
|
:on-mouse-down #(.stopPropagation %)
|
||||||
:on-click #(goto! (:start a) true) ; jump the timeline to this annotation
|
:on-click #(goto! (:start a) true ; jump the timeline to this annotation
|
||||||
|
(ref-scroll-to? @(rf/subscribe [::subs/scene]) (:id a)))
|
||||||
:style {:left (* x0 w) :top (* y0 h)
|
:style {:left (* x0 w) :top (* y0 h)
|
||||||
:width (* (- x1 x0) w) :height (* (- y1 y0) h)
|
:width (* (- x1 x0) w) :height (* (- y1 y0) h)
|
||||||
:background (str (:color a) "44") :border (str "1.5px solid " (:color a))}}]))
|
:background (str (:color a) "44") :border (str "1.5px solid " (:color a))}}]))
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue