feat: add hidden annotation meta for decluttering timeline

Add 'hidden' meta flag to annotations to hide them from the timeline lane
while keeping them available for linking. Hidden annotations appear as
small 6px colored rectangles with faint color and dither pattern overlay.

Click on a hidden rectangle to unhide it. Hidden annotations' marks are
not resolved/used, and they don't trigger scroll-to-clip during playback.

Add 'Hide from timeline' checkbox in annotation create/edit form.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
Your Name 2026-06-30 16:27:19 -04:00
parent e964347e51
commit 1ea675567c
2 changed files with 34 additions and 9 deletions

View file

@ -83,16 +83,18 @@
(->> (:groups scene)
(keep (fn [[gid g]]
(when (and (= :annotation (:type g)) (= ctx (:parent g)))
(let [src-segs (scene/resolve scene gid)
(let [src-segs (when-not (get-in g [:meta :hidden]) (scene/resolve scene gid))
bars (scene/merge-bars
(mapcat (fn [{[a b] :src}] (scene/pieces segs a b)) src-segs))
reason (scene/broken-reason scene gid)]
reason (scene/broken-reason scene gid)
hidden (get-in g [:meta :hidden])]
{:id gid :name (:name g) :color (or (:color g) "#4e8fc2")
:content (:content g) :children (count (:marks g))
:nested (get nested gid 0)
:draft (boolean (:draft g))
:script (:script g) ; pdf highlights linked to this group
:broken (boolean reason) :reason reason
:hidden (boolean hidden)
:start (or (ffirst bars) 0) :bars bars}))))
(sort-by (juxt :broken :start)) ; broken annotations sink to the bottom
vec))))

View file

@ -377,11 +377,12 @@
(reset! scrolled-on-load? true)
(scroll-to-seg-track! segs playhead))
;; clip-following: only while the playhead is actually inside a bar of
;; an annotation that opts in (:meta :scroll-to) — scroll each of its
;; clips into view as the playhead crosses onto a new bar (discontinuity).
;; an annotation that opts in (:meta :scroll-to) and is not hidden —
;; scroll each of its clips into view as the playhead crosses onto a new bar (discontinuity).
;; Track [ann-id bar-idx] to avoid scrolling within the same bar.
(when-let [current-bar (some (fn [a]
(when (and (ann-scroll-to? (get-in scene [:groups (:id a)]))
(when (and (not (get-in (get-in scene [:groups (:id a)]) [:meta :hidden]))
(ann-scroll-to? (get-in scene [:groups (:id a)]))
(some (fn [[lo hi]] (<= lo playhead hi)) (:bars a)))
[(:id a) (first (keep-indexed (fn [j [lo hi]]
(when (<= lo playhead hi) j))
@ -414,7 +415,8 @@
[:div.ann-lanes {:style {:height lane-h :width width}
:on-mouse-down #(scrub! @content fps zoom %)}
;; annotation bars — solid for saved, dashed for the in-progress draft
(for [[i a] (map-indexed vector anns)
;; visible annotations: full bars with labels
(for [[i a] (map-indexed vector (filter (fn [a] (not (get-in (get-in scene [:groups (:id a)]) [:meta :hidden]))) anns))
[j [lo hi]] (map-indexed vector (:bars a))]
^{:key (str (:id a) "-" j)}
[:div.ann-bar {:title (:name a)
@ -430,10 +432,27 @@
:cursor (if (:draft a) "default" "pointer")
:border-radius 2
:border (str (if (:draft a) "1px dashed " "1px solid ") (:color a))}}])
(for [[i a] (map-indexed vector anns) :let [[lo _] (first (:bars a))] :when lo]
;; visible annotation labels
(for [[i a] (map-indexed vector (filter (fn [a] (not (get-in (get-in scene [:groups (:id a)]) [:meta :hidden]))) anns)) :let [[lo _] (first (:bars a))] :when lo]
^{:key (str "lbl-" (:id a))}
[:div.ann-bar-label {:style {:top (* i 18)
:left (+ 4 (px lo fps zoom))}} (:name a)])]]]
:left (+ 4 (px lo fps zoom))}} (:name a)])
;; hidden annotations: tiny colored dots with dither pattern — click to unhide
(for [[i a] (map-indexed vector (filter (fn [a] (get-in (get-in scene [:groups (:id a)]) [:meta :hidden])) anns)) :let [[lo _] (first (:bars a))] :when lo]
^{:key (str "hidden-" (:id a))}
[:div.ann-hidden {:title (str (:name a) " (click to unhide)")
:on-click #(do (.stopPropagation %)
(rf/dispatch [::events/put-group (:id a)
(update (get-in scene [:groups (:id a)])
:meta dissoc :hidden)]))
:style {:top (+ 6 (* i 18)) :height 6
:left (px lo fps zoom) :width 12
:background (str (:color a) "33")
:cursor "pointer"
:border-radius 1
:border (str "1px solid " (:color a) "88")
:background-image "url(\"data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='2' height='2'%3E%3Crect width='2' height='2' fill='%23ffffff'/%3E%3Crect width='1' height='1' fill='%23000000'/%3E%3Crect x='1' y='1' width='1' height='1' fill='%23000000'/%3E%3C/svg%3E\")"
:background-size "2px 2px"}}])]]]
[:div.timeline-body-scroll {:ref (fn [n] (reset! body-scroll-el n))
:style {:padding-top (+ lane-h 24)}}
[:div.gutter-tracks
@ -846,7 +865,11 @@
[:label.form-check {:title "During playback, scroll each of this annotation's clips into view as the playhead reaches it"}
[:input {:type "checkbox" :checked (boolean (get-in d [:meta :scroll-to]))
:on-change #(put (assoc-in d [:meta :scroll-to] (.. % -target -checked)))}]
[:span "Follow clips during playback"]]])
[:span "Follow clips during playback"]]
[:label.form-check {:title "Hide this annotation from the timeline lane (still usable in links)"}
[:input {:type "checkbox" :checked (boolean (get-in d [:meta :hidden]))
:on-change #(put (assoc-in d [:meta :hidden] (.. % -target -checked)))}]
[:span "Hide from timeline"]]])
[:div.form-marks-label "Content"]
^{:key gid} [content-editor gid (:content orig)]
(if (and linking (= gid (:gid linking)))