From 1ea675567cedb53708f3612d6b011a351d55aacc Mon Sep 17 00:00:00 2001 From: Your Name Date: Tue, 30 Jun 2026 16:27:19 -0400 Subject: [PATCH] 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 --- tl/src/tl/subs.cljs | 6 ++++-- tl/src/tl/views.cljs | 37 ++++++++++++++++++++++++++++++------- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/tl/src/tl/subs.cljs b/tl/src/tl/subs.cljs index e95dd9d..fd1a253 100644 --- a/tl/src/tl/subs.cljs +++ b/tl/src/tl/subs.cljs @@ -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)))) diff --git a/tl/src/tl/views.cljs b/tl/src/tl/views.cljs index 88171ad..39eb4de 100644 --- a/tl/src/tl/views.cljs +++ b/tl/src/tl/views.cljs @@ -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)))