feat: improve annotation mark editing
This commit is contained in:
parent
7ac8f27b57
commit
dfa92a478f
5 changed files with 382 additions and 180 deletions
|
|
@ -55,20 +55,20 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
|
||||||
press. The default action (.save) gets the heavy Mac "default button" ring. */
|
press. The default action (.save) gets the heavy Mac "default button" ring. */
|
||||||
.jump-btn, .add-btn, .edit-btn, .del-btn, .expand-btn, .back-btn, .crumb,
|
.jump-btn, .add-btn, .edit-btn, .del-btn, .expand-btn, .back-btn, .crumb,
|
||||||
.play-btn, .step-btn, .row-x, .add-mark, .save, .cancel, .hl-ok, .hl-cancel,
|
.play-btn, .step-btn, .row-x, .add-mark, .save, .cancel, .hl-ok, .hl-cancel,
|
||||||
.pane-tabs button, .track-toggle {
|
.pane-tabs button, .track-toggle, .mark-script, .mark-script-new {
|
||||||
font-family: var(--chicago); background: var(--paper); color: var(--ink);
|
font-family: var(--chicago); background: var(--paper); color: var(--ink);
|
||||||
border: 1px solid var(--ink); border-radius: 8px; cursor: pointer; line-height: 1.3;
|
border: 1px solid var(--ink); border-radius: 8px; cursor: pointer; line-height: 1.3;
|
||||||
}
|
}
|
||||||
.jump-btn:hover, .add-btn:hover, .edit-btn:hover, .del-btn:hover, .expand-btn:hover,
|
.jump-btn:hover, .add-btn:hover, .edit-btn:hover, .del-btn:hover, .expand-btn:hover,
|
||||||
.back-btn:hover, .crumb:hover, .play-btn:hover, .step-btn:hover, .row-x:hover,
|
.back-btn:hover, .crumb:hover, .play-btn:hover, .step-btn:hover, .row-x:hover,
|
||||||
.add-mark:hover, .save:hover, .cancel:hover, .hl-ok:hover, .hl-cancel:hover,
|
.add-mark:hover, .save:hover, .cancel:hover, .hl-ok:hover, .hl-cancel:hover,
|
||||||
.pane-tabs button:hover, .track-toggle:hover {
|
.pane-tabs button:hover, .track-toggle:hover, .mark-script:hover, .mark-script-new:hover {
|
||||||
background: var(--hover);
|
background: var(--hover);
|
||||||
}
|
}
|
||||||
.jump-btn:active, .add-btn:active, .edit-btn:active, .del-btn:active, .expand-btn:active,
|
.jump-btn:active, .add-btn:active, .edit-btn:active, .del-btn:active, .expand-btn:active,
|
||||||
.back-btn:active, .crumb:active, .play-btn:active, .step-btn:active, .row-x:active,
|
.back-btn:active, .crumb:active, .play-btn:active, .step-btn:active, .row-x:active,
|
||||||
.add-mark:active, .save:active, .cancel:active, .hl-ok:active, .hl-cancel:active,
|
.add-mark:active, .save:active, .cancel:active, .hl-ok:active, .hl-cancel:active,
|
||||||
.pane-tabs button:active, .track-toggle:active {
|
.pane-tabs button:active, .track-toggle:active, .mark-script:active, .mark-script-new:active {
|
||||||
background: var(--ink); color: var(--paper);
|
background: var(--ink); color: var(--paper);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -101,10 +101,13 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
|
||||||
background: none; cursor: pointer; }
|
background: none; cursor: pointer; }
|
||||||
.draw-tools .draw-wid { width: 56px; }
|
.draw-tools .draw-wid { width: 56px; }
|
||||||
.draw-done { font-weight: bold; }
|
.draw-done { font-weight: bold; }
|
||||||
.mark-draw { background: none; border: 1px solid transparent; border-radius: 0;
|
.mark-draw, .mark-script {
|
||||||
font-size: 12px; cursor: pointer; padding: 0 3px; line-height: 1; }
|
width: 28px; height: 26px; flex: none;
|
||||||
.mark-draw:hover { border-color: var(--ink); }
|
display: inline-flex; align-items: center; justify-content: center;
|
||||||
.mark-draw.has { border-color: var(--ink); background: var(--paper); }
|
border-radius: 0; font-size: 13px; cursor: pointer; padding: 0; line-height: 1;
|
||||||
|
}
|
||||||
|
.mark-draw { background: var(--paper); color: var(--ink); border: 1px solid var(--ink); }
|
||||||
|
.mark-draw.has, .mark-script.has { background: var(--desktop); background-size: 4px 4px; }
|
||||||
|
|
||||||
.frame-readout {
|
.frame-readout {
|
||||||
position: absolute; bottom: 6px; right: 8px; z-index: 4;
|
position: absolute; bottom: 6px; right: 8px; z-index: 4;
|
||||||
|
|
@ -209,10 +212,13 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
|
||||||
.form {
|
.form {
|
||||||
flex: 1; min-width: 0; overflow-y: auto;
|
flex: 1; min-width: 0; overflow-y: auto;
|
||||||
background: var(--paper); border: 1px solid var(--ink); box-sizing: border-box;
|
background: var(--paper); border: 1px solid var(--ink); box-sizing: border-box;
|
||||||
padding: 10px 12px;
|
padding: 12px;
|
||||||
display: flex; flex-direction: column; gap: 10px;
|
display: flex; flex-direction: column; gap: 12px;
|
||||||
|
}
|
||||||
|
.form-head {
|
||||||
|
font-family: var(--chicago); font-size: 14px; color: var(--ink);
|
||||||
|
padding-bottom: 6px; border-bottom: 2px solid var(--ink);
|
||||||
}
|
}
|
||||||
.form-head { font-family: var(--chicago); font-size: 14px; color: var(--ink); }
|
|
||||||
.form-row { display: flex; gap: 8px; align-items: center; }
|
.form-row { display: flex; gap: 8px; align-items: center; }
|
||||||
.form-name { flex: 1; }
|
.form-name { flex: 1; }
|
||||||
.form input[type=text], .form-name, .form-content {
|
.form input[type=text], .form-name, .form-content {
|
||||||
|
|
@ -224,7 +230,7 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
|
||||||
.form-content { width: 100%; min-height: 70px; resize: vertical; box-sizing: border-box;
|
.form-content { width: 100%; min-height: 70px; resize: vertical; box-sizing: border-box;
|
||||||
font-family: inherit; }
|
font-family: inherit; }
|
||||||
.form-marks-label { font-family: var(--chicago); font-size: 11px; letter-spacing: .5px;
|
.form-marks-label { font-family: var(--chicago); font-size: 11px; letter-spacing: .5px;
|
||||||
color: var(--ink); margin-top: 4px; }
|
color: var(--ink); margin-top: 2px; padding-top: 2px; }
|
||||||
|
|
||||||
/* contenteditable content surface + inline link chips */
|
/* contenteditable content surface + inline link chips */
|
||||||
.content-editor { white-space: pre-wrap; word-break: break-word; outline: none; cursor: text;
|
.content-editor { white-space: pre-wrap; word-break: break-word; outline: none; cursor: text;
|
||||||
|
|
@ -239,8 +245,11 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
|
||||||
.link-chip .link-f { font-size: 11px; opacity: .7; }
|
.link-chip .link-f { font-size: 11px; opacity: .7; }
|
||||||
.link-chip:hover .link-f { opacity: 1; }
|
.link-chip:hover .link-f { opacity: 1; }
|
||||||
|
|
||||||
.mark-row { display: flex; flex-wrap: nowrap; align-items: center; gap: 6px; min-width: 0; }
|
.mark-row { display: flex; flex-wrap: wrap; align-items: center; gap: 6px; min-width: 0; }
|
||||||
.mark-arrow { color: var(--ink); }
|
.mark-arrow {
|
||||||
|
color: var(--ink); flex: none; font-family: var(--chicago);
|
||||||
|
min-width: 16px; text-align: center;
|
||||||
|
}
|
||||||
.row-x, .add-mark { padding: 3px 8px; font-size: 12px; }
|
.row-x, .add-mark { padding: 3px 8px; font-size: 12px; }
|
||||||
.add-mark { align-self: flex-start; }
|
.add-mark { align-self: flex-start; }
|
||||||
|
|
||||||
|
|
@ -251,7 +260,7 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
|
||||||
|
|
||||||
/* point editor + autocomplete */
|
/* point editor + autocomplete */
|
||||||
.pt-input { position: relative; flex: 1 1 130px; display: flex; align-items: center; min-width: 0; }
|
.pt-input { position: relative; flex: 1 1 130px; display: flex; align-items: center; min-width: 0; }
|
||||||
.mark-row .pt-input { flex: 0 1 160px; max-width: 180px; }
|
.mark-row .pt-input { flex: 1 1 128px; max-width: 190px; }
|
||||||
.pt-text {
|
.pt-text {
|
||||||
flex: 1; min-width: 0; box-sizing: border-box;
|
flex: 1; min-width: 0; box-sizing: border-box;
|
||||||
background: var(--paper); color: var(--ink); border: 1px solid var(--ink);
|
background: var(--paper); color: var(--ink); border: 1px solid var(--ink);
|
||||||
|
|
@ -277,11 +286,18 @@ body { overflow: hidden; background: var(--desktop); background-size: 4px 4px;
|
||||||
|
|
||||||
.link-insert { display: flex; align-items: flex-start; gap: 6px; align-self: stretch; }
|
.link-insert { display: flex; align-items: flex-start; gap: 6px; align-self: stretch; }
|
||||||
.link-insert .pt-input { flex: 1 1 auto; max-width: none; }
|
.link-insert .pt-input { flex: 1 1 auto; max-width: none; }
|
||||||
|
.link-insert-block .pt-input { flex: 1 1 220px; max-width: none; }
|
||||||
.hl-ok, .hl-cancel { padding: 2px 9px; font-size: 12px; }
|
.hl-ok, .hl-cancel { padding: 2px 9px; font-size: 12px; }
|
||||||
|
|
||||||
.pt-chip { flex: 1 1 0; min-width: 0; display: flex; align-items: center; gap: 4px;
|
.pt-chip { flex: 1 1 128px; min-width: 0; display: flex; align-items: center; gap: 4px;
|
||||||
background: var(--paper); border: 1px solid var(--ink); border-radius: 0;
|
background: var(--paper); border: 1px solid var(--ink); border-radius: 0;
|
||||||
padding: 2px 4px; }
|
padding: 3px 5px; min-height: 20px; box-shadow: inset -1px -1px 0 var(--shade); }
|
||||||
|
.pt-chip.empty {
|
||||||
|
border-style: dashed; color: var(--mute); background: var(--shade);
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
.pt-chip.pending { background: var(--paper); }
|
||||||
|
.pt-chip.pending .pt-frame { color: var(--ink); opacity: 1; }
|
||||||
.pt-chip-name { font-size: 12px; color: var(--ink); white-space: nowrap;
|
.pt-chip-name { font-size: 12px; color: var(--ink); white-space: nowrap;
|
||||||
overflow: hidden; text-overflow: ellipsis; flex: 1; }
|
overflow: hidden; text-overflow: ellipsis; flex: 1; }
|
||||||
.pt-frame { width: 56px; background: var(--paper); color: var(--ink); border: 1px solid var(--ink);
|
.pt-frame { width: 56px; background: var(--paper); color: var(--ink); border: 1px solid var(--ink);
|
||||||
|
|
@ -658,6 +674,17 @@ html.dark .timeline-head {
|
||||||
.zoom-read { font-size: 11px; color: var(--ink); min-width: 42px; text-align: center; }
|
.zoom-read { font-size: 11px; color: var(--ink); min-width: 42px; text-align: center; }
|
||||||
.script-empty { color: var(--mute); font-size: 12px; padding: 20px; }
|
.script-empty { color: var(--mute); font-size: 12px; padding: 20px; }
|
||||||
.ann.selected { box-shadow: inset 3px 0 0 var(--ink); }
|
.ann.selected { box-shadow: inset 3px 0 0 var(--ink); }
|
||||||
|
.script-return {
|
||||||
|
display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||||
|
padding: 6px 10px; border-bottom: 2px solid var(--ink);
|
||||||
|
background: var(--desktop); background-size: 4px 4px;
|
||||||
|
font-family: var(--chicago); font-size: 11px;
|
||||||
|
}
|
||||||
|
.script-return .hl-cancel { border-radius: 0; padding: 2px 8px; }
|
||||||
|
.script-return-text {
|
||||||
|
display: inline-block; background: var(--paper); border: 1px solid var(--ink);
|
||||||
|
padding: 2px 6px; box-shadow: 1px 1px 0 var(--ink);
|
||||||
|
}
|
||||||
|
|
||||||
/* note rail (list of project script-notes) */
|
/* note rail (list of project script-notes) */
|
||||||
.note-rail { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
.note-rail { display: flex; align-items: center; gap: 8px; flex-wrap: wrap;
|
||||||
|
|
@ -695,23 +722,42 @@ html.dark .timeline-head {
|
||||||
font-size: 11px; border: 1px solid var(--mute); border-radius: 0; padding: 3px; background: var(--paper); color: var(--ink); }
|
font-size: 11px; border: 1px solid var(--mute); border-radius: 0; padding: 3px; background: var(--paper); color: var(--ink); }
|
||||||
|
|
||||||
/* binding notes to annotations/marks (annotation form + cards) */
|
/* binding notes to annotations/marks (annotation form + cards) */
|
||||||
.mark-block { margin-bottom: 4px; }
|
.mark-block {
|
||||||
|
margin-bottom: 6px; padding: 5px 6px;
|
||||||
|
border: 1px solid var(--ink); background: var(--paper);
|
||||||
|
box-shadow: 2px 2px 0 var(--ink);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.mark-block:hover { background: var(--shade); }
|
||||||
|
.mark-block.pending { cursor: default; }
|
||||||
|
.mark-block.pending:hover { background: var(--paper); }
|
||||||
/* the currently-selected/last-touched mark while authoring — so you know which
|
/* the currently-selected/last-touched mark while authoring — so you know which
|
||||||
one a drawing binds to, and which you're about to delete */
|
one a drawing binds to, and which you're about to delete */
|
||||||
.mark-block.active-mark { box-shadow: inset 3px 0 0 var(--ink); background: var(--shade, rgba(128,128,128,.14));
|
.mark-block.active-mark {
|
||||||
border-radius: 2px; padding: 2px 0 2px 3px; margin-left: -3px; }
|
background: var(--desktop); background-size: 4px 4px;
|
||||||
|
box-shadow: inset 4px 0 0 var(--ink), 2px 2px 0 var(--ink);
|
||||||
|
}
|
||||||
.mark-block.broken { opacity: 0.6; }
|
.mark-block.broken { opacity: 0.6; }
|
||||||
.mark-block.broken .pt-chip-name { text-decoration: line-through; }
|
.mark-block.broken .pt-chip-name { text-decoration: line-through; }
|
||||||
.note-drop { display: flex; align-items: center; flex-wrap: wrap; gap: 4px; min-height: 20px;
|
.mark-note-list {
|
||||||
margin: 2px 0 2px 14px; padding: 2px 4px; border: 1px dashed var(--mute); border-radius: 0; }
|
display: flex; flex-wrap: wrap; gap: 4px;
|
||||||
.note-drop-label { font-size: 10px; color: var(--mute); }
|
margin: 5px 0 0 24px;
|
||||||
.note-drop-hint { font-size: 10px; color: var(--mute); font-style: italic; }
|
}
|
||||||
.note-source { display: flex; flex-wrap: wrap; gap: 4px; margin: 4px 0; }
|
.mark-script-wrap { position: relative; display: inline-flex; flex: none; }
|
||||||
.note-src { display: inline-flex; align-items: center; gap: 4px; font-size: 11px; cursor: grab;
|
.mark-script-pop {
|
||||||
background: var(--paper); color: var(--ink); border: 1px solid var(--ink); border-radius: 0; padding: 1px 6px; }
|
position: absolute; right: 0; top: calc(100% + 4px); z-index: 21;
|
||||||
.note-src:active { cursor: grabbing; }
|
width: 210px; padding: 6px;
|
||||||
|
background: var(--paper); border: 1px solid var(--ink); box-shadow: 2px 2px 0 var(--ink);
|
||||||
|
}
|
||||||
|
.mark-script-pop .pt-input { display: block; max-width: none; }
|
||||||
|
.mark-script-pop .pt-dropdown { position: static; margin-top: 4px; box-shadow: none; }
|
||||||
|
.mark-script-new {
|
||||||
|
width: 100%; margin-top: 6px; padding: 5px 8px;
|
||||||
|
border-radius: 0; text-align: left; font-size: 11px;
|
||||||
|
}
|
||||||
.bound-note { display: inline-flex; align-items: center; gap: 3px; font-size: 11px;
|
.bound-note { display: inline-flex; align-items: center; gap: 3px; font-size: 11px;
|
||||||
background: var(--paper); color: var(--ink); border: 1px solid var(--ink); border-radius: 0; padding: 0 4px; }
|
background: var(--paper); color: var(--ink); border: 1px solid var(--ink); border-radius: 0; padding: 1px 4px;
|
||||||
|
cursor: pointer; }
|
||||||
.bound-note.live { background: var(--ink); color: var(--paper); }
|
.bound-note.live { background: var(--ink); color: var(--paper); }
|
||||||
.bound-note-name { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
.bound-note-name { max-width: 120px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||||
.note-live { font-weight: bold; }
|
.note-live { font-weight: bold; }
|
||||||
|
|
@ -783,6 +829,7 @@ html.dark .timeline-head {
|
||||||
padding: 0 2px; font-size: 13px; line-height: 1; flex: none; align-self: center;
|
padding: 0 2px; font-size: 13px; line-height: 1; flex: none; align-self: center;
|
||||||
}
|
}
|
||||||
.mark-grip:active { cursor: grabbing; }
|
.mark-grip:active { cursor: grabbing; }
|
||||||
|
.mark-grip.disabled { cursor: default; opacity: .35; }
|
||||||
.mark-block.dragging { opacity: 0.4; }
|
.mark-block.dragging { opacity: 0.4; }
|
||||||
.mark-block.drop-before { position: relative; }
|
.mark-block.drop-before { position: relative; }
|
||||||
.mark-block.drop-before::before {
|
.mark-block.drop-before::before {
|
||||||
|
|
|
||||||
|
|
@ -378,6 +378,9 @@
|
||||||
(merge {:id rid :kind :text :content ""} region))]
|
(merge {:id rid :kind :text :content ""} region))]
|
||||||
(merge {:db db} (persist-note-fx db gid)))))
|
(merge {:db db} (persist-note-fx db gid)))))
|
||||||
|
|
||||||
|
(defn- add-in [coll x] (vec (distinct (conj (vec coll) x))))
|
||||||
|
(defn- rm-in [coll x] (vec (remove #(= x %) coll)))
|
||||||
|
|
||||||
;; select-then-highlight with no active note: spin up a note (named after the
|
;; select-then-highlight with no active note: spin up a note (named after the
|
||||||
;; selected text) with the region already in it, and make it active.
|
;; selected text) with the region already in it, and make it active.
|
||||||
(rf/reg-event-fx ::highlight-into-new-note
|
(rf/reg-event-fx ::highlight-into-new-note
|
||||||
|
|
@ -421,9 +424,6 @@
|
||||||
;; The pointer lives on the referrer (annotation or mark), so a shared note stays
|
;; The pointer lives on the referrer (annotation or mark), so a shared note stays
|
||||||
;; pure and freely reusable. These mutate the draft in-place (db only) and ride
|
;; pure and freely reusable. These mutate the draft in-place (db only) and ride
|
||||||
;; the annotation's Save, exactly like ::set-content and the marks editor.
|
;; the annotation's Save, exactly like ::set-content and the marks editor.
|
||||||
(defn- add-in [coll x] (vec (distinct (conj (vec coll) x))))
|
|
||||||
(defn- rm-in [coll x] (vec (remove #(= x %) coll)))
|
|
||||||
|
|
||||||
(rf/reg-event-db ::bind-note-annotation
|
(rf/reg-event-db ::bind-note-annotation
|
||||||
(fn [db [_ gid note-gid]]
|
(fn [db [_ gid note-gid]]
|
||||||
(update-in db [:scene :groups gid :notes] #(add-in % note-gid))))
|
(update-in db [:scene :groups gid :notes] #(add-in % note-gid))))
|
||||||
|
|
@ -443,8 +443,66 @@
|
||||||
(update-mark db gid mark-id #(update % :notes rm-in note-gid))))
|
(update-mark db gid mark-id #(update % :notes rm-in note-gid))))
|
||||||
|
|
||||||
;; click a mark row to make it the active one (drawings/edits target it)
|
;; click a mark row to make it the active one (drawings/edits target it)
|
||||||
(rf/reg-event-db ::set-active-mark
|
(defn- drawing-state-for [db ann mark-id]
|
||||||
(fn [db [_ mark-id]] (assoc-in db [:view :active-mark] mark-id)))
|
(let [existing (first (some (fn [m] (when (= mark-id (:id m)) (:drawings m)))
|
||||||
|
(get-in db [:scene :groups ann :marks])))]
|
||||||
|
{:ann ann :mark-id mark-id
|
||||||
|
:gid (or existing (keyword (str "draw-" (random-uuid))))
|
||||||
|
:new? (nil? existing)}))
|
||||||
|
|
||||||
|
(defn- start-drawing-db [db ann mark-id]
|
||||||
|
(-> db
|
||||||
|
(assoc-in [:view :active-mark] mark-id)
|
||||||
|
(assoc-in [:view :draw] (drawing-state-for db ann mark-id))))
|
||||||
|
|
||||||
|
(defn- draft-in-ctx [db ctx]
|
||||||
|
(some (fn [[gid g]]
|
||||||
|
(when (and (:draft g) (= ctx (:parent g))) [gid g]))
|
||||||
|
(get-in db [:scene :groups])))
|
||||||
|
|
||||||
|
(defn- draft-mark-at [db ctx lf]
|
||||||
|
(when-let [[gid g] (draft-in-ctx db ctx)]
|
||||||
|
(let [scene (:scene db)
|
||||||
|
segs (scene/content-segments scene ctx)]
|
||||||
|
(some (fn [m]
|
||||||
|
(when (some (fn [[lo hi]] (and (<= lo lf) (< lf hi)))
|
||||||
|
(scene/mark-bars scene gid (:id m) segs))
|
||||||
|
{:ann gid :mark-id (:id m)}))
|
||||||
|
(:marks g)))))
|
||||||
|
|
||||||
|
(defn- sync-draft-mark-for-playhead [db ctx lf]
|
||||||
|
(if-let [{:keys [ann mark-id]} (draft-mark-at db ctx lf)]
|
||||||
|
(if (= mark-id (get-in db [:view :active-mark]))
|
||||||
|
db
|
||||||
|
(start-drawing-db db ann mark-id))
|
||||||
|
(if (draft-in-ctx db ctx)
|
||||||
|
(-> db
|
||||||
|
(assoc-in [:view :active-mark] nil)
|
||||||
|
(assoc-in [:view :draw] nil))
|
||||||
|
db)))
|
||||||
|
|
||||||
|
(defn- mark-start-local [db ann mark-id]
|
||||||
|
(let [scene (:scene db)
|
||||||
|
ctx (:parent (get-in scene [:groups ann]))
|
||||||
|
segs (scene/content-segments scene ctx)]
|
||||||
|
(ffirst (scene/mark-bars scene ann mark-id segs))))
|
||||||
|
|
||||||
|
;; click/select a mark row: make it the drawing target and move the playhead to it.
|
||||||
|
(rf/reg-event-fx ::set-active-mark
|
||||||
|
(fn [{:keys [db]} [_ mark-id]]
|
||||||
|
(let [[ann _] (or (draft-in-ctx db (peek (get-in db [:view :stack])))
|
||||||
|
(some (fn [[gid g]] (when (:draft g) [gid g]))
|
||||||
|
(get-in db [:scene :groups])))
|
||||||
|
ctx (:parent (get-in db [:scene :groups ann]))
|
||||||
|
local (when ann (mark-start-local db ann mark-id))
|
||||||
|
db (cond-> db
|
||||||
|
ann (start-drawing-db ann mark-id)
|
||||||
|
(and ctx local) (assoc-in [:view :playheads ctx] local))
|
||||||
|
sf (when (and ctx local)
|
||||||
|
(scene/local->source
|
||||||
|
(scene/content-segments (:scene db) ctx) local))]
|
||||||
|
(cond-> (sync-route {:db db} db)
|
||||||
|
sf (assoc :player/seek (/ sf (:fps db)))))))
|
||||||
|
|
||||||
;; "add a new script note for this mark": remember the target mark + hop to the
|
;; "add a new script note for this mark": remember the target mark + hop to the
|
||||||
;; Script pane. When a note is created there (highlight-into-new-note) it binds to
|
;; Script pane. When a note is created there (highlight-into-new-note) it binds to
|
||||||
|
|
@ -452,6 +510,7 @@
|
||||||
(rf/reg-event-db ::new-note-for-mark
|
(rf/reg-event-db ::new-note-for-mark
|
||||||
(fn [db [_ gid mark-id]]
|
(fn [db [_ gid mark-id]]
|
||||||
(-> db (assoc-in [:view :note-target] {:gid gid :mark-id mark-id})
|
(-> db (assoc-in [:view :note-target] {:gid gid :mark-id mark-id})
|
||||||
|
(assoc-in [:view :active-note] nil)
|
||||||
(assoc-in [:view :pane] :script))))
|
(assoc-in [:view :pane] :script))))
|
||||||
(rf/reg-event-db ::cancel-note-target
|
(rf/reg-event-db ::cancel-note-target
|
||||||
(fn [db _] (-> db (assoc-in [:view :note-target] nil)
|
(fn [db _] (-> db (assoc-in [:view :note-target] nil)
|
||||||
|
|
@ -468,14 +527,7 @@
|
||||||
;; gid. The entity isn't written until ::save-drawing, so cancel is a clean no-op.
|
;; gid. The entity isn't written until ::save-drawing, so cancel is a clean no-op.
|
||||||
(rf/reg-event-db ::start-drawing
|
(rf/reg-event-db ::start-drawing
|
||||||
(fn [db [_ ann mark-id]]
|
(fn [db [_ ann mark-id]]
|
||||||
(let [existing (first (some (fn [m] (when (= mark-id (:id m)) (:drawings m)))
|
(start-drawing-db db ann mark-id)))
|
||||||
(get-in db [:scene :groups ann :marks])))]
|
|
||||||
(-> db
|
|
||||||
(assoc-in [:view :active-mark] mark-id) ; drawing a mark makes it the active one
|
|
||||||
(assoc-in [:view :draw]
|
|
||||||
{:ann ann :mark-id mark-id
|
|
||||||
:gid (or existing (keyword (str "draw-" (random-uuid))))
|
|
||||||
:new? (nil? existing)})))))
|
|
||||||
|
|
||||||
;; leave draw mode. Strokes autocommit as you draw, so this is just "done" —
|
;; leave draw mode. Strokes autocommit as you draw, so this is just "done" —
|
||||||
;; nothing to save or discard here (the form's Save/Cancel is the rollback net).
|
;; nothing to save or discard here (the form's Save/Cancel is the rollback net).
|
||||||
|
|
@ -553,7 +605,9 @@
|
||||||
|
|
||||||
(rf/reg-event-fx ::set-playhead
|
(rf/reg-event-fx ::set-playhead
|
||||||
(fn [{:keys [db]} [_ ctx lf]]
|
(fn [{:keys [db]} [_ ctx lf]]
|
||||||
(let [next-db (assoc-in db [:view :playheads ctx] lf)]
|
(let [next-db (-> db
|
||||||
|
(assoc-in [:view :playheads ctx] lf)
|
||||||
|
(sync-draft-mark-for-playhead ctx lf))]
|
||||||
(sync-route {:db next-db} next-db))))
|
(sync-route {:db next-db} next-db))))
|
||||||
(rf/reg-event-db ::set-playing (fn [db [_ p]] (assoc-in db [:view :playing?] p)))
|
(rf/reg-event-db ::set-playing (fn [db [_ p]] (assoc-in db [:view :playing?] p)))
|
||||||
;; reveal an annotation's immediate children into the current timeline lane
|
;; reveal an annotation's immediate children into the current timeline lane
|
||||||
|
|
|
||||||
|
|
@ -18,20 +18,22 @@
|
||||||
;; Two link kinds share the [label](scheme…) form:
|
;; Two link kinds share the [label](scheme…) form:
|
||||||
;; frame [label](mark:ref@at) jump the playhead to a spot
|
;; frame [label](mark:ref@at) jump the playhead to a spot
|
||||||
;; timeline [label](timeline:gid) push that timeline onto the stack
|
;; timeline [label](timeline:gid) push that timeline onto the stack
|
||||||
|
;; note [label](note:gid) jump to a script note
|
||||||
(def ^:private link-re
|
(def ^:private link-re
|
||||||
"\\[([^\\]]*)\\]\\((?:mark:([^@)]+)@(-?\\d+)|timeline:([^)]+))\\)")
|
"\\[([^\\]]*)\\]\\((?:mark:([^@)]+)@(-?\\d+)|timeline:([^)]+)|note:([^)]+))\\)")
|
||||||
|
|
||||||
(defn link-token
|
(defn link-token
|
||||||
"The token for a link map: {:kind :frame :label :ref :at} (the default) or
|
"The token for a link map: {:kind :frame :label :ref :at} (the default) or
|
||||||
{:kind :timeline :label :ref}."
|
{:kind :timeline :label :ref} or {:kind :script-note :label :ref}."
|
||||||
[{:keys [kind label ref at]}]
|
[{:keys [kind label ref at]}]
|
||||||
(if (= kind :timeline)
|
(case kind
|
||||||
(str "[" label "](timeline:" (name ref) ")")
|
:timeline (str "[" label "](timeline:" (name ref) ")")
|
||||||
|
:script-note (str "[" label "](note:" (name ref) ")")
|
||||||
(str "[" label "](mark:" (name ref) "@" at ")")))
|
(str "[" label "](mark:" (name ref) "@" at ")")))
|
||||||
|
|
||||||
(defn parse-content
|
(defn parse-content
|
||||||
"Split content `s` into [:text str] / [:link {…}] segments. Each link carries
|
"Split content `s` into [:text str] / [:link {…}] segments. Each link carries
|
||||||
:kind — :frame (with :ref/:at) or :timeline (with :ref)."
|
:kind — :frame (with :ref/:at), :timeline (with :ref), or :script-note."
|
||||||
[s]
|
[s]
|
||||||
(if (empty? s)
|
(if (empty? s)
|
||||||
[]
|
[]
|
||||||
|
|
@ -39,10 +41,11 @@
|
||||||
(loop [out [] last 0]
|
(loop [out [] last 0]
|
||||||
(if-let [m (.exec re s)]
|
(if-let [m (.exec re s)]
|
||||||
(let [idx (.-index m) pre (subs s last idx)
|
(let [idx (.-index m) pre (subs s last idx)
|
||||||
link (if (aget m 4)
|
link (cond
|
||||||
{:kind :timeline :label (aget m 1) :ref (keyword (aget m 4))}
|
(aget m 4) {:kind :timeline :label (aget m 1) :ref (keyword (aget m 4))}
|
||||||
{:kind :frame :label (aget m 1) :ref (keyword (aget m 2))
|
(aget m 5) {:kind :script-note :label (aget m 1) :ref (keyword (aget m 5))}
|
||||||
:at (js/parseInt (aget m 3) 10)})]
|
:else {:kind :frame :label (aget m 1) :ref (keyword (aget m 2))
|
||||||
|
:at (js/parseInt (aget m 3) 10)})]
|
||||||
(recur (cond-> out
|
(recur (cond-> out
|
||||||
(seq pre) (conj [:text pre])
|
(seq pre) (conj [:text pre])
|
||||||
:always (conj [:link link]))
|
:always (conj [:link link]))
|
||||||
|
|
@ -57,8 +60,9 @@
|
||||||
(= 3 (.-nodeType n)) (.-textContent n)
|
(= 3 (.-nodeType n)) (.-textContent n)
|
||||||
(= "BR" (.-tagName n)) "\n"
|
(= "BR" (.-tagName n)) "\n"
|
||||||
(and (.-classList n) (.contains (.-classList n) "link-chip"))
|
(and (.-classList n) (.contains (.-classList n) "link-chip"))
|
||||||
(link-token (if (= "timeline" (.. n -dataset -kind))
|
(link-token (case (.. n -dataset -kind)
|
||||||
{:kind :timeline :label (.. n -dataset -label) :ref (keyword (.. n -dataset -ref))}
|
"timeline" {:kind :timeline :label (.. n -dataset -label) :ref (keyword (.. n -dataset -ref))}
|
||||||
|
"script-note" {:kind :script-note :label (.. n -dataset -label) :ref (keyword (.. n -dataset -ref))}
|
||||||
{:kind :frame :label (.. n -dataset -label) :ref (keyword (.. n -dataset -ref))
|
{:kind :frame :label (.. n -dataset -label) :ref (keyword (.. n -dataset -ref))
|
||||||
:at (js/parseInt (.. n -dataset -at) 10)}))
|
:at (js/parseInt (.. n -dataset -at) 10)}))
|
||||||
;; A wrapper element (e.g. the <div> a browser inserts on Enter): recurse so
|
;; A wrapper element (e.g. the <div> a browser inserts on Enter): recurse so
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,7 @@
|
||||||
;; :choosing (fresh draft — marks + title picker only) | :creating (full form).
|
;; :choosing (fresh draft — marks + title picker only) | :creating (full form).
|
||||||
(rf/reg-sub ::draft-stage (fn [db] (get-in db [:view :draft-stage])))
|
(rf/reg-sub ::draft-stage (fn [db] (get-in db [:view :draft-stage])))
|
||||||
(rf/reg-sub ::script-jump (fn [db] (get-in db [:view :script-jump])))
|
(rf/reg-sub ::script-jump (fn [db] (get-in db [:view :script-jump])))
|
||||||
|
(rf/reg-sub ::note-target (fn [db] (get-in db [:view :note-target])))
|
||||||
(rf/reg-sub ::hidden-notes (fn [db] (get-in db [:view :hidden-notes] #{})))
|
(rf/reg-sub ::hidden-notes (fn [db] (get-in db [:view :hidden-notes] #{})))
|
||||||
(rf/reg-sub ::hidden-tags (fn [db] (get-in db [:view :hidden-tags] #{})))
|
(rf/reg-sub ::hidden-tags (fn [db] (get-in db [:view :hidden-tags] #{})))
|
||||||
(rf/reg-sub ::region-focus (fn [db] (get-in db [:view :region-focus])))
|
(rf/reg-sub ::region-focus (fn [db] (get-in db [:view :region-focus])))
|
||||||
|
|
|
||||||
|
|
@ -790,10 +790,10 @@
|
||||||
:border "1px solid rgba(78,143,194,0.85)"
|
:border "1px solid rgba(78,143,194,0.85)"
|
||||||
:pointer-events "none" :z-index 5}}])]]]]))))
|
:pointer-events "none" :z-index 5}}])]]]]))))
|
||||||
|
|
||||||
;; --- links: inline markdown chips that seek the timeline ------------------
|
;; --- links: inline markdown chips -----------------------------------------
|
||||||
;; A link is a ref-point named in the content as `[label](mark:ref@at)` (see
|
;; Content links are named tokens like `[label](mark:ref@at)`,
|
||||||
;; tl.scene). The editor is a contenteditable surface where links live as atomic
|
;; `[label](timeline:gid)`, or `[label](note:gid)` (see tl.md). The editor is a
|
||||||
;; chips (delete with backspace, click to seek); everything else is plain text.
|
;; contenteditable surface where links live as atomic chips.
|
||||||
|
|
||||||
(defn- link-frame
|
(defn- link-frame
|
||||||
"ctx-local frame for a chip's {:ref :at}, resolved against the live scene."
|
"ctx-local frame for a chip's {:ref :at}, resolved against the live scene."
|
||||||
|
|
@ -804,18 +804,30 @@
|
||||||
(defn- goto-link! [local]
|
(defn- goto-link! [local]
|
||||||
(when local (goto! local true)))
|
(when local (goto! local true)))
|
||||||
|
|
||||||
|
(defn- note-link-ok? [scene ref]
|
||||||
|
(= :script-note (get-in scene [:groups ref :type])))
|
||||||
|
|
||||||
(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,
|
||||||
paragraphs) with inline formatting plus clickable link chips."
|
paragraphs) with inline formatting plus clickable link chips."
|
||||||
[scene ctx content]
|
[scene ctx content]
|
||||||
(md/render content
|
(md/render content
|
||||||
(fn [{:keys [kind label ref at]}]
|
(fn [{:keys [kind label ref at]}]
|
||||||
(if (= :timeline kind)
|
(case kind
|
||||||
|
:timeline
|
||||||
(let [path (scene/path-to scene ref)]
|
(let [path (scene/path-to scene ref)]
|
||||||
[:span.link-chip {:class (when-not path "broken")
|
[:span.link-chip {:class (when-not path "broken")
|
||||||
:title (when-not path "timeline no longer here")
|
:title (when-not path "timeline no longer here")
|
||||||
:on-click #(when path (rf/dispatch [::events/open-stack path]))}
|
:on-click #(when path (rf/dispatch [::events/open-stack path]))}
|
||||||
"⤢ " label])
|
"⤢ " label])
|
||||||
|
|
||||||
|
:script-note
|
||||||
|
(let [ok? (note-link-ok? scene ref)]
|
||||||
|
[:span.link-chip {:class (when-not ok? "broken")
|
||||||
|
:title (when-not ok? "script note no longer here")
|
||||||
|
:on-click #(when ok? (rf/dispatch [::events/jump-to-note ref]))}
|
||||||
|
(if ok? "📄 " "△ ") label])
|
||||||
|
|
||||||
(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")
|
||||||
|
|
@ -827,18 +839,29 @@
|
||||||
(let [span (js/document.createElement "span")
|
(let [span (js/document.createElement "span")
|
||||||
scene @(rf/subscribe [::subs/scene])
|
scene @(rf/subscribe [::subs/scene])
|
||||||
timeline? (= :timeline kind)
|
timeline? (= :timeline kind)
|
||||||
lf (when-not timeline?
|
note? (= :script-note kind)
|
||||||
|
lf (when-not (or timeline? note?)
|
||||||
(scene/link-local scene @(rf/subscribe [::subs/context]) {:ref ref :at at}))
|
(scene/link-local scene @(rf/subscribe [::subs/context]) {:ref ref :at at}))
|
||||||
ok (if timeline? (some? (scene/path-to scene ref)) lf)]
|
ok (cond
|
||||||
|
timeline? (some? (scene/path-to scene ref))
|
||||||
|
note? (note-link-ok? scene ref)
|
||||||
|
:else lf)]
|
||||||
(set! (.-className span) (if ok "link-chip" "link-chip broken"))
|
(set! (.-className span) (if ok "link-chip" "link-chip broken"))
|
||||||
(set! (.-contentEditable span) "false")
|
(set! (.-contentEditable span) "false")
|
||||||
(when-not ok (set! (.-title span) (if timeline? "timeline no longer here"
|
(when-not ok (set! (.-title span) (cond
|
||||||
"linked clip no longer in this timeline")))
|
timeline? "timeline no longer here"
|
||||||
(set! (.-textContent span) (str (cond timeline? "⤢ " (not ok) "△ ") label))
|
note? "script note no longer here"
|
||||||
|
:else "linked clip no longer in this timeline")))
|
||||||
|
(set! (.-textContent span) (str (cond
|
||||||
|
timeline? "⤢ "
|
||||||
|
note? "📄 "
|
||||||
|
(not ok) "△ "
|
||||||
|
:else "")
|
||||||
|
label))
|
||||||
(aset (.-dataset span) "label" label)
|
(aset (.-dataset span) "label" label)
|
||||||
(aset (.-dataset span) "ref" (name ref))
|
(aset (.-dataset span) "ref" (name ref))
|
||||||
(when kind (aset (.-dataset span) "kind" (name kind)))
|
(when kind (aset (.-dataset span) "kind" (name kind)))
|
||||||
(when-not timeline? (aset (.-dataset span) "at" (str at)))
|
(when-not (or timeline? note?) (aset (.-dataset span) "at" (str at)))
|
||||||
(when lf
|
(when lf
|
||||||
(let [f (js/document.createElement "span")]
|
(let [f (js/document.createElement "span")]
|
||||||
(set! (.-className f) "link-f")
|
(set! (.-className f) "link-f")
|
||||||
|
|
@ -891,10 +914,16 @@
|
||||||
(.removeAllRanges s) (.addRange s r))))
|
(.removeAllRanges s) (.addRange s r))))
|
||||||
: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))]
|
(case (.. chip -dataset -kind)
|
||||||
(goto-link! lf))))}]))
|
"timeline" (when-let [path (scene/path-to @(rf/subscribe [::subs/scene])
|
||||||
|
(keyword (.. chip -dataset -ref)))]
|
||||||
|
(rf/dispatch [::events/open-stack path]))
|
||||||
|
"script-note" (rf/dispatch [::events/jump-to-note
|
||||||
|
(keyword (.. chip -dataset -ref))])
|
||||||
|
(when-let [lf (link-frame (.. chip -dataset -ref) (.. chip -dataset -at))]
|
||||||
|
(goto-link! lf)))))}]))
|
||||||
|
|
||||||
(defn- point-candidates [scene ctx q timelines?]
|
(defn- point-candidates [scene ctx notes q timelines?]
|
||||||
(let [needle (str/lower-case q)
|
(let [needle (str/lower-case q)
|
||||||
n (when (re-matches #"-?\d+" q) (js/parseInt q 10))]
|
n (when (re-matches #"-?\d+" q) (js/parseInt q 10))]
|
||||||
(vec
|
(vec
|
||||||
|
|
@ -915,7 +944,13 @@
|
||||||
(keep (fn [{:keys [gid name in path]}]
|
(keep (fn [{:keys [gid name in path]}]
|
||||||
(when (str/includes? (str/lower-case (str "timeline " name " " in)) needle)
|
(when (str/includes? (str/lower-case (str "timeline " name " " in)) needle)
|
||||||
{:label name :kind :timeline :group (when in (str "in " in))
|
{:label name :kind :timeline :group (when in (str "in " in))
|
||||||
:point {:kind :timeline :ref gid} :path path})))))))))
|
:point {:kind :timeline :ref gid} :path path})))))
|
||||||
|
(when timelines?
|
||||||
|
(->> notes
|
||||||
|
(keep (fn [{:keys [id name]}]
|
||||||
|
(when (str/includes? (str/lower-case (str "note script " name)) needle)
|
||||||
|
{:label name :kind :script-note :group "script note"
|
||||||
|
:point {:kind :script-note :ref id}})))))))))
|
||||||
|
|
||||||
(defn- point-label [cand off local]
|
(defn- point-label [cand off local]
|
||||||
(str (:label cand) " @" (js/Math.round local) "f"
|
(str (:label cand) " @" (js/Math.round local) "f"
|
||||||
|
|
@ -923,7 +958,7 @@
|
||||||
(str " +" off))))
|
(str " +" off))))
|
||||||
|
|
||||||
(defn- pick-point [cand off]
|
(defn- pick-point [cand off]
|
||||||
(if (= :timeline (:kind cand))
|
(if (contains? #{:timeline :script-note} (:kind cand))
|
||||||
(assoc (:point cand) :label (:label cand) :local (:local cand) :candidate cand :offset 0)
|
(assoc (:point cand) :label (:label cand) :local (:local cand) :candidate cand :offset 0)
|
||||||
(let [o (if (:abs cand) 0 (min (max 0 off) (max 0 (dec (or (:len cand) 1)))))
|
(let [o (if (:abs cand) 0 (min (max 0 off) (max 0 (dec (or (:len cand) 1)))))
|
||||||
local (+ (:local cand) o)]
|
local (+ (:local cand) o)]
|
||||||
|
|
@ -945,7 +980,8 @@
|
||||||
;; start CLOSED; only opens on focus (an auto-focused picker opens
|
;; start CLOSED; only opens on focus (an auto-focused picker opens
|
||||||
;; itself via :on-focus). Was defaulting open even when unfocused.
|
;; itself via :on-focus). Was defaulting open even when unfocused.
|
||||||
open? (r/atom (boolean auto-focus?))]
|
open? (r/atom (boolean auto-focus?))]
|
||||||
(let [cands (point-candidates scene ctx @text timelines?)
|
(let [notes @(rf/subscribe [::subs/notes])
|
||||||
|
cands (point-candidates scene ctx notes @text timelines?)
|
||||||
i (min @idx (max 0 (dec (count cands))))
|
i (min @idx (max 0 (dec (count cands))))
|
||||||
cur (or (:candidate @picked) (get cands i))
|
cur (or (:candidate @picked) (get cands i))
|
||||||
maxo (max 0 (dec (or (:len cur) 1)))
|
maxo (max 0 (dec (or (:len cur) 1)))
|
||||||
|
|
@ -956,6 +992,7 @@
|
||||||
(cond
|
(cond
|
||||||
(not timelines?) (goto! (:local (pick-point cand o2)))
|
(not timelines?) (goto! (:local (pick-point cand o2)))
|
||||||
(= :timeline (:kind cand)) (rf/dispatch [::events/open-stack (:path cand)])
|
(= :timeline (:kind cand)) (rf/dispatch [::events/open-stack (:path cand)])
|
||||||
|
(= :script-note (:kind cand)) nil
|
||||||
:else (rf/dispatch [::events/preview-frame
|
:else (rf/dispatch [::events/preview-frame
|
||||||
(:local (pick-point cand o2))]))))
|
(:local (pick-point cand o2))]))))
|
||||||
emit! (fn [cand o2]
|
emit! (fn [cand o2]
|
||||||
|
|
@ -989,7 +1026,7 @@
|
||||||
(reset! idx 0)
|
(reset! idx 0)
|
||||||
(reset! off 0)
|
(reset! off 0)
|
||||||
(reset! open? true)
|
(reset! open? true)
|
||||||
(preview! (first (point-candidates scene ctx (.. % -target -value) timelines?)) 0))
|
(preview! (first (point-candidates scene ctx notes (.. % -target -value) timelines?)) 0))
|
||||||
:on-key-down
|
:on-key-down
|
||||||
(fn [e]
|
(fn [e]
|
||||||
(case (.-key e)
|
(case (.-key e)
|
||||||
|
|
@ -1022,10 +1059,13 @@
|
||||||
:on-mouse-down #(.preventDefault %)
|
:on-mouse-down #(.preventDefault %)
|
||||||
:on-mouse-enter #(go! j)
|
:on-mouse-enter #(go! j)
|
||||||
:on-click #(emit! c 0)}
|
:on-click #(emit! c 0)}
|
||||||
(when timelines? [:span.cand-kind (if (= :timeline (:kind c)) "⤢ " "↪ ")])
|
(when timelines? [:span.cand-kind (case (:kind c)
|
||||||
|
:timeline "⤢ "
|
||||||
|
:script-note "📄 "
|
||||||
|
"↪ ")])
|
||||||
[:span.cand-label (:label c)]
|
[:span.cand-label (:label c)]
|
||||||
(when (:group c) [:span.cand-group (str " " (:group c))])
|
(when (:group c) [:span.cand-group (str " " (:group c))])
|
||||||
(when-not (= :timeline (:kind c))
|
(when-not (contains? #{:timeline :script-note} (:kind c))
|
||||||
[:span.link-f (str " " (js/Math.round (:local c)) "f")])])))])))
|
[:span.link-f (str " " (js/Math.round (:local c)) "f")])])))])))
|
||||||
|
|
||||||
(defn- local->draft-point [segs local]
|
(defn- local->draft-point [segs local]
|
||||||
|
|
@ -1093,14 +1133,17 @@
|
||||||
|
|
||||||
(defn- link-picker [{:keys [scene ctx on-commit on-cancel]}]
|
(defn- link-picker [{:keys [scene ctx on-commit on-cancel]}]
|
||||||
(r/with-let [picked (r/atom nil)]
|
(r/with-let [picked (r/atom nil)]
|
||||||
[:div.link-insert
|
[:div.mark-block.pending.link-insert-block
|
||||||
[point-picker {:scene scene :ctx ctx :value @picked :auto-focus? true :timelines? true
|
[:div.mark-row
|
||||||
:on-pick #(reset! picked %)
|
[:span.mark-grip.disabled {:title "Insert link"} "↪"]
|
||||||
:on-cancel on-cancel}]
|
[point-picker {:scene scene :ctx ctx :value @picked :auto-focus? true :timelines? true
|
||||||
[:button.hl-ok {:type "button" :title "Insert link" :disabled (nil? @picked)
|
:placeholder "clip / annotation / timeline / note..."
|
||||||
:on-click #(when @picked (on-commit @picked))}
|
:on-pick #(reset! picked %)
|
||||||
"✓"]
|
:on-cancel on-cancel}]
|
||||||
[:button.hl-cancel {:type "button" :title "Cancel" :on-click on-cancel} "✕"]]))
|
[:button.hl-ok {:type "button" :title "Insert link" :disabled (nil? @picked)
|
||||||
|
:on-click #(when @picked (on-commit @picked))}
|
||||||
|
"✓"]
|
||||||
|
[:button.hl-cancel {:type "button" :title "Cancel" :on-click on-cancel} "✕"]]]))
|
||||||
|
|
||||||
(defn- commit-link!
|
(defn- commit-link!
|
||||||
"Insert a link chip for ref-point `pt` (label `label`) at the editor caret."
|
"Insert a link chip for ref-point `pt` (label `label`) at the editor caret."
|
||||||
|
|
@ -1284,7 +1327,9 @@
|
||||||
:on-change #(put (assoc-in d [:marks i k :at] (to-frame (.. % -target -value) len)))}]
|
:on-change #(put (assoc-in d [:marks i k :at] (to-frame (.. % -target -value) len)))}]
|
||||||
[:span.pt-dur (str "/" len)]
|
[:span.pt-dur (str "/" len)]
|
||||||
[:button.pt-chip-x {:type "button" :title "Re-pick this end"
|
[:button.pt-chip-x {:type "button" :title "Re-pick this end"
|
||||||
:on-click #(rf/dispatch [::events/unset-endpoint (:gid d) i k])} "✕"]]))
|
:on-click (fn [e]
|
||||||
|
(.stopPropagation e)
|
||||||
|
(rf/dispatch [::events/unset-endpoint (:gid d) i k]))} "✕"]]))
|
||||||
|
|
||||||
(defn- proxy-frame-chip
|
(defn- proxy-frame-chip
|
||||||
"Editable endpoint for a proxy (synthetic-clip) mark: clip name + frame input
|
"Editable endpoint for a proxy (synthetic-clip) mark: clip name + frame input
|
||||||
|
|
@ -1301,41 +1346,89 @@
|
||||||
(to-frame (.. % -target -value) len)])}]
|
(to-frame (.. % -target -value) len)])}]
|
||||||
[:span.pt-dur (str "/" len)]
|
[:span.pt-dur (str "/" len)]
|
||||||
[:button.pt-chip-x {:type "button" :title "Re-pick this end"
|
[:button.pt-chip-x {:type "button" :title "Re-pick this end"
|
||||||
:on-click #(rf/dispatch [::events/unset-proxy-endpoint gid mark-id pid which])} "✕"]]))
|
:on-click (fn [e]
|
||||||
|
(.stopPropagation e)
|
||||||
|
(rf/dispatch [::events/unset-proxy-endpoint gid mark-id pid which]))} "✕"]]))
|
||||||
|
|
||||||
|
(defn- pending-frame-chip [scene segs {:keys [seg f]}]
|
||||||
|
(let [len (scene/seg-length segs seg)]
|
||||||
|
[:div.pt-chip.pending
|
||||||
|
[:span.pt-chip-name (clip-label scene segs seg)]
|
||||||
|
[:input.pt-frame {:type "number" :value (js/Math.round f) :disabled true}]
|
||||||
|
[:span.pt-dur (str "/" len)]]))
|
||||||
|
|
||||||
|
(defn- empty-frame-chip [label]
|
||||||
|
[:div.pt-chip.empty
|
||||||
|
[:span.pt-chip-name label]
|
||||||
|
[:span.pt-dur "empty"]])
|
||||||
|
|
||||||
|
(defn- pending-mark-block [{:keys [scene ctx segs pt choosing?]}]
|
||||||
|
(let [picked? (map? pt)]
|
||||||
|
[:div.mark-block.pending
|
||||||
|
[:div.mark-row
|
||||||
|
[:span.mark-grip.disabled {:title "New mark"} "⠿"]
|
||||||
|
(if picked?
|
||||||
|
[pending-frame-chip scene segs pt]
|
||||||
|
[point-picker {:scene scene :ctx ctx :class "active" :auto-focus? true
|
||||||
|
:placeholder "click start clip..."
|
||||||
|
:on-pick #(when-let [p (local->draft-point segs (:local %))]
|
||||||
|
(rf/dispatch [::events/draft-click-seg (:seg p) (:f p)]))}])
|
||||||
|
[:span.mark-arrow "→"]
|
||||||
|
(if picked?
|
||||||
|
[point-picker {:scene scene :ctx ctx :class "active" :auto-focus? true
|
||||||
|
:placeholder "click end clip..."
|
||||||
|
:on-pick #(let [end-local (if (and (zero? (:offset %))
|
||||||
|
(pos? (or (get-in % [:candidate :len]) 0)))
|
||||||
|
(+ (:local %) (get-in % [:candidate :len]))
|
||||||
|
(:local %))]
|
||||||
|
(when-let [p (local->draft-end-point segs end-local)]
|
||||||
|
(rf/dispatch [::events/draft-click-seg (:seg p) (:f p)])))}]
|
||||||
|
[empty-frame-chip "End point"])
|
||||||
|
[:button.mark-draw {:type "button" :title "Finish the mark before drawing" :disabled true} "🖼+"]
|
||||||
|
(when-not choosing?
|
||||||
|
[:button.mark-script {:type "button" :title "Finish the mark before adding a script note" :disabled true} "📄+"])
|
||||||
|
[:button.row-x {:type "button" :title "Finish or cancel from the timeline" :disabled true} "✕"]]]))
|
||||||
|
|
||||||
;; --- script-note bindings (annotation form) ------------------------------
|
;; --- script-note bindings (annotation form) ------------------------------
|
||||||
;; A note is bound by dragging its chip from the source list onto a drop target
|
;; The pointer lives on the mark and rides the annotation's Save. Existing notes
|
||||||
;; (a mark row, or the annotation-level target). The pointer lives on the referrer
|
;; bind from a compact autocomplete; Add new jumps to the script pane and binds
|
||||||
;; and rides the annotation's Save.
|
;; the highlighted note when it is created.
|
||||||
|
|
||||||
(defn- note-src-chip
|
(defn- mark-note-chip [ng n live on-unbind]
|
||||||
"Draggable source chip for note `n`; click also toggles an annotation-level bind."
|
[:span.bound-note {:class (when (contains? live ng) "live")
|
||||||
[gid n]
|
:title "Jump to this passage in the script"
|
||||||
[:span.note-src {:draggable true
|
:on-click #(rf/dispatch [::events/jump-to-note ng])}
|
||||||
:on-drag-start (fn [e] (.. e -dataTransfer (setData "text/note" (name (:id n)))))
|
(when (contains? live ng) [:span.note-live "»»» "])
|
||||||
:on-click #(rf/dispatch [::events/bind-note-annotation gid (:id n)])
|
|
||||||
:title "Drag onto a mark, or click to bind to the whole annotation"}
|
|
||||||
[:span.ann-swatch {:style {:background (:color n)}}]
|
[:span.ann-swatch {:style {:background (:color n)}}]
|
||||||
(:name n)])
|
[:span.bound-note-name (:name n)]
|
||||||
|
[:button.row-x {:type "button" :title "Unbind"
|
||||||
|
:on-click #(on-unbind ng)} "✕"]])
|
||||||
|
|
||||||
(defn- note-drop
|
(defn- mark-script-picker [gid mark-id notes bound]
|
||||||
"Drop zone rendering `bound` note-gids as removable chips; a dropped note calls
|
(r/with-let [open? (r/atom false)]
|
||||||
(on-bind note-gid), a chip's ✕ calls (on-unbind note-gid)."
|
(let [bound? (set bound)
|
||||||
[label bound nmap live on-bind on-unbind]
|
choices (remove #(contains? bound? (:id %)) notes)
|
||||||
[:div.note-drop {:on-drag-over #(.preventDefault %)
|
by-name (into {} (map (juxt :name :id)) choices)]
|
||||||
:on-drop (fn [e] (.preventDefault e)
|
[:span.mark-script-wrap
|
||||||
(let [g (.. e -dataTransfer (getData "text/note"))]
|
[:button.mark-script {:type "button"
|
||||||
(when (seq g) (on-bind (keyword g)))))}
|
:class (when (seq bound) "has")
|
||||||
(when label [:span.note-drop-label label])
|
:title "Attach script note"
|
||||||
(if (seq bound)
|
:on-click #(swap! open? not)}
|
||||||
(for [ng bound :let [n (nmap ng)] :when n]
|
(if (seq bound) "📄" "📄+")]
|
||||||
^{:key (name ng)}
|
(when @open?
|
||||||
[:span.bound-note {:class (when (contains? live ng) "live")}
|
[:div.mark-script-pop {:on-click #(.stopPropagation %)}
|
||||||
(when (contains? live ng) [:span.note-live "»»» "])
|
[autocomplete {:items (mapv :name choices)
|
||||||
[:span.ann-swatch {:style {:background (:color n)}}]
|
:placeholder "script note…"
|
||||||
[:span.bound-note-name (:name n)]
|
:auto-focus? true
|
||||||
[:button.row-x {:type "button" :title "Unbind" :on-click #(on-unbind ng)} "✕"]])
|
:on-choose (fn [choice]
|
||||||
[:span.note-drop-hint "drop a note"])])
|
(when-let [note-gid (by-name choice)]
|
||||||
|
(rf/dispatch [::events/bind-note-mark gid mark-id note-gid])
|
||||||
|
(reset! open? false)))}]
|
||||||
|
[:button.mark-script-new {:type "button"
|
||||||
|
:on-click (fn []
|
||||||
|
(reset! open? false)
|
||||||
|
(rf/dispatch [::events/new-note-for-mark gid mark-id]))}
|
||||||
|
"+ Add new"]])])))
|
||||||
|
|
||||||
;; drag-to-reorder marks: one live drag at a time, so a single module atom holds
|
;; drag-to-reorder marks: one live drag at a time, so a single module atom holds
|
||||||
;; {:src i :over j}. Deref'd in the form so the drop line follows the cursor.
|
;; {:src i :over j}. Deref'd in the form so the drop line follows the cursor.
|
||||||
|
|
@ -1363,7 +1456,8 @@
|
||||||
:on-drag-end (fn [_] (reset! mark-drag nil))})
|
:on-drag-end (fn [_] (reset! mark-drag nil))})
|
||||||
|
|
||||||
(defn annotation-form []
|
(defn annotation-form []
|
||||||
(r/with-let [orig (dissoc @(rf/subscribe [::subs/draft-group]) :draft :gid)]
|
(r/with-let [orig (dissoc @(rf/subscribe [::subs/draft-group]) :draft :gid)
|
||||||
|
last-scrolled (atom nil)]
|
||||||
(let [d @(rf/subscribe [::subs/draft-group])
|
(let [d @(rf/subscribe [::subs/draft-group])
|
||||||
scene @(rf/subscribe [::subs/scene])
|
scene @(rf/subscribe [::subs/scene])
|
||||||
;; anchor the form to the draft's home context, not the live stack top:
|
;; anchor the form to the draft's home context, not the live stack top:
|
||||||
|
|
@ -1393,6 +1487,12 @@
|
||||||
(rf/dispatch [::events/save-group gid (dissoc d :draft :gid)
|
(rf/dispatch [::events/save-group gid (dissoc d :draft :gid)
|
||||||
(when-not new? orig)])
|
(when-not new? orig)])
|
||||||
(rf/dispatch [::events/finish-edit]))]
|
(rf/dispatch [::events/finish-edit]))]
|
||||||
|
(when (and active (not= active @last-scrolled))
|
||||||
|
(reset! last-scrolled active)
|
||||||
|
(r/after-render
|
||||||
|
#(when-let [node (js/document.querySelector
|
||||||
|
(str "[data-mark-row='" active "']"))]
|
||||||
|
(.scrollIntoView node #js {:block "nearest" :behavior "smooth"}))))
|
||||||
[: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 (and (not root?) (not choosing?))
|
(when (and (not root?) (not choosing?))
|
||||||
|
|
@ -1401,36 +1501,7 @@
|
||||||
[: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 "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"]]
|
|
||||||
[: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 "Tags"]
|
|
||||||
(let [tags (vec (get-in d [:meta :tags]))]
|
|
||||||
[:div.tag-editor
|
|
||||||
(into [:div.tag-list]
|
|
||||||
(for [t tags]
|
|
||||||
^{:key t} [tag-chip t #(put (assoc-in d [:meta :tags] (vec (remove #{%} tags))))]))
|
|
||||||
[autocomplete {:items (filterv (complement (set tags)) @(rf/subscribe [::subs/project-tags]))
|
|
||||||
:placeholder "add tag…" :allow-new? true
|
|
||||||
:on-choose #(put (assoc-in d [:meta :tags] (conj tags %)))}]])])
|
|
||||||
(when-not choosing?
|
|
||||||
[:<>
|
|
||||||
[:div.form-marks-label "Content"]
|
|
||||||
^{:key gid} [content-editor gid (:content orig)]
|
|
||||||
(if (and linking (= gid (:gid linking)))
|
|
||||||
[link-picker {:scene scene :ctx ctx
|
|
||||||
:on-commit #(commit-link! (select-keys % [:ref :at :kind]) (:label %))
|
|
||||||
:on-cancel #(rf/dispatch [::events/cancel-linking])}]
|
|
||||||
[:button.add-mark {:type "button" :on-click #(rf/dispatch [::events/start-linking gid])}
|
|
||||||
"Insert link"])
|
|
||||||
(when (and linking (= gid (:gid linking)))
|
|
||||||
[:div.form-hint "Click a clip or the frame-readout to link it, or pick above."])])
|
|
||||||
(when-not root?
|
(when-not root?
|
||||||
[:<>
|
[:<>
|
||||||
;; the title IS the create/associate control: type a new title to make a
|
;; the title IS the create/associate control: type a new title to make a
|
||||||
|
|
@ -1463,7 +1534,9 @@
|
||||||
(when (= mark-id active) "active-mark ")
|
(when (= mark-id active) "active-mark ")
|
||||||
(when (contains? broken mark-id) "broken ")
|
(when (contains? broken mark-id) "broken ")
|
||||||
(when (and (:src drag) (= i (:over drag))
|
(when (and (:src drag) (= i (:over drag))
|
||||||
(not= i (:src drag))) "drop-before"))})
|
(not= i (:src drag))) "drop-before"))
|
||||||
|
:data-mark-row mark-id
|
||||||
|
:on-click #(rf/dispatch [::events/set-active-mark mark-id])})
|
||||||
[:div.mark-row
|
[:div.mark-row
|
||||||
[:span.mark-grip {:draggable true :title "Drag to reorder"
|
[:span.mark-grip {:draggable true :title "Drag to reorder"
|
||||||
:on-drag-start (fn [e]
|
:on-drag-start (fn [e]
|
||||||
|
|
@ -1508,43 +1581,52 @@
|
||||||
(goto! s true))
|
(goto! s true))
|
||||||
(rf/dispatch [::events/start-drawing gid mark-id]))}
|
(rf/dispatch [::events/start-drawing gid mark-id]))}
|
||||||
(if (seq (:drawings mark)) "🖼" "🖼+")]
|
(if (seq (:drawings mark)) "🖼" "🖼+")]
|
||||||
|
(when-not choosing?
|
||||||
|
[mark-script-picker gid mark-id notes (:notes mark)])
|
||||||
[:button.row-x {:type "button" :title "Remove"
|
[:button.row-x {:type "button" :title "Remove"
|
||||||
:on-click #(rf/dispatch [::events/remove-mark gid i])} "✕"]]
|
:on-click #(rf/dispatch [::events/remove-mark gid i])} "✕"]]
|
||||||
(when-not choosing?
|
(when-not choosing?
|
||||||
[note-drop nil (:notes mark) nmap live
|
(when (seq (:notes mark))
|
||||||
#(rf/dispatch [::events/bind-note-mark gid mark-id %])
|
(into [:div.mark-note-list]
|
||||||
#(rf/dispatch [::events/unbind-note-mark gid mark-id %])])]))
|
(for [ng (:notes mark) :let [n (nmap ng)] :when n]
|
||||||
(when (and (map? pt) (not (:proxy pt)))
|
^{:key (name ng)}
|
||||||
[:div.mark-row
|
[mark-note-chip ng n live #(rf/dispatch [::events/unbind-note-mark gid mark-id %])]))))]))
|
||||||
[:div.pt-chip [:span.pt-chip-name (str (clip-label scene segs (:seg pt))
|
(when (or (= pt :new) (and (map? pt) (not (:proxy pt))))
|
||||||
" @" (js/Math.round (:f pt)) "f")]]
|
[pending-mark-block {:scene scene :ctx ctx :segs segs :pt pt :choosing? choosing?}])
|
||||||
[:span.mark-arrow "→"]
|
[:div.form-hint "Drag across the timeline to select a range (click to move the playhead)."]])
|
||||||
[point-picker {:scene scene :ctx ctx :class "active" :auto-focus? true
|
(when-not choosing?
|
||||||
:placeholder "click end clip..."
|
[:<>
|
||||||
:on-pick #(let [end-local (if (and (zero? (:offset %))
|
(when-not root?
|
||||||
(pos? (or (get-in % [:candidate :len]) 0)))
|
|
||||||
(+ (:local %) (get-in % [:candidate :len]))
|
|
||||||
(:local %))]
|
|
||||||
(when-let [p (local->draft-end-point segs end-local)]
|
|
||||||
(rf/dispatch [::events/draft-click-seg (:seg p) (:f p)])))}]])
|
|
||||||
(when (= pt :new)
|
|
||||||
[:div.mark-row
|
|
||||||
[point-picker {:scene scene :ctx ctx :class "active"
|
|
||||||
:placeholder "click a clip..."
|
|
||||||
:on-pick #(when-let [p (local->draft-point segs (:local %))]
|
|
||||||
(rf/dispatch [::events/draft-click-seg (:seg p) (:f p)]))}]])
|
|
||||||
[:div.form-hint "Drag across the timeline to select a range (click to move the playhead)."]
|
|
||||||
(when-not choosing?
|
|
||||||
[:<>
|
[:<>
|
||||||
[:div.form-marks-label "Script notes"]
|
[:div.form-marks-label "Tags"]
|
||||||
[note-drop "Whole annotation" (:notes d) nmap live
|
(let [tags (vec (get-in d [:meta :tags]))]
|
||||||
#(rf/dispatch [::events/bind-note-annotation gid %])
|
[:div.tag-editor
|
||||||
#(rf/dispatch [::events/unbind-note-annotation gid %])]
|
(into [:div.tag-list]
|
||||||
(if (seq notes)
|
(for [t tags]
|
||||||
[:div.note-source
|
^{:key t} [tag-chip t #(put (assoc-in d [:meta :tags] (vec (remove #{%} tags))))]))
|
||||||
(for [n notes] ^{:key (name (:id n))} [note-src-chip gid n])]
|
[autocomplete {:items (filterv (complement (set tags)) @(rf/subscribe [::subs/project-tags]))
|
||||||
[:div.form-hint "No script notes yet — create them in the Script tab."])
|
:placeholder "add tag…" :allow-new? true
|
||||||
[:div.form-hint "Drag a note onto a mark (or the whole-annotation target). It shows »»» while the playhead is in range."]])])
|
:on-choose #(put (assoc-in d [:meta :tags] (conj tags %)))}]])])
|
||||||
|
[:div.form-marks-label "Content"]
|
||||||
|
^{:key gid} [content-editor gid (:content orig)]
|
||||||
|
(if (and linking (= gid (:gid linking)))
|
||||||
|
[link-picker {:scene scene :ctx ctx
|
||||||
|
:on-commit #(commit-link! (select-keys % [:ref :at :kind]) (:label %))
|
||||||
|
:on-cancel #(rf/dispatch [::events/cancel-linking])}]
|
||||||
|
[:button.add-mark {:type "button" :on-click #(rf/dispatch [::events/start-linking gid])}
|
||||||
|
"Insert link"])
|
||||||
|
(when (and linking (= gid (:gid linking)))
|
||||||
|
[:div.form-hint "Click a clip or the frame-readout to link it, or pick above."])
|
||||||
|
(when-not root?
|
||||||
|
[:<>
|
||||||
|
[: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"]]
|
||||||
|
[: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-actions
|
[:div.form-actions
|
||||||
(when-not choosing?
|
(when-not choosing?
|
||||||
[:button.save {:type "submit" :disabled (not valid?)} "Save"])
|
[:button.save {:type "submit" :disabled (not valid?)} "Save"])
|
||||||
|
|
@ -1875,6 +1957,7 @@
|
||||||
live @(rf/subscribe [::subs/active-note-set])
|
live @(rf/subscribe [::subs/active-note-set])
|
||||||
hidden @(rf/subscribe [::subs/hidden-notes])
|
hidden @(rf/subscribe [::subs/hidden-notes])
|
||||||
authed? @(rf/subscribe [::subs/authed?])
|
authed? @(rf/subscribe [::subs/authed?])
|
||||||
|
note-target @(rf/subscribe [::subs/note-target])
|
||||||
script-err @(rf/subscribe [::subs/script-error])
|
script-err @(rf/subscribe [::subs/script-error])
|
||||||
scale (or @zoom 1)
|
scale (or @zoom 1)
|
||||||
;; notes whose highlights are drawn on the page (eye toggle off)
|
;; notes whose highlights are drawn on the page (eye toggle off)
|
||||||
|
|
@ -1916,6 +1999,13 @@
|
||||||
(reset! scrolled live-note)
|
(reset! scrolled live-note)
|
||||||
(scroll-to-region! rid))))))
|
(scroll-to-region! rid))))))
|
||||||
[:div.script-pane
|
[:div.script-pane
|
||||||
|
(when note-target
|
||||||
|
[:div.script-return
|
||||||
|
[:button.hl-cancel {:type "button"
|
||||||
|
:title "Back to annotation"
|
||||||
|
:on-click #(rf/dispatch [::events/cancel-note-target])}
|
||||||
|
"‹ Back"]
|
||||||
|
[:span.script-return-text "Highlight script text to make a note for this mark."]])
|
||||||
(if url
|
(if url
|
||||||
[note-rail notes active hidden authed?]
|
[note-rail notes active hidden authed?]
|
||||||
[:div.script-hint
|
[:div.script-hint
|
||||||
|
|
@ -1947,12 +2037,15 @@
|
||||||
[:button.sel-add {:style {:left (:bx @sel) :top (:by @sel)}
|
[:button.sel-add {:style {:left (:bx @sel) :top (:by @sel)}
|
||||||
:on-mouse-down #(.preventDefault %) ; keep the selection alive
|
:on-mouse-down #(.preventDefault %) ; keep the selection alive
|
||||||
:on-click (fn []
|
:on-click (fn []
|
||||||
(if active
|
(if (and active (not note-target))
|
||||||
(rf/dispatch [::events/add-region-text active region])
|
(rf/dispatch [::events/add-region-text active region])
|
||||||
(rf/dispatch [::events/highlight-into-new-note region]))
|
(rf/dispatch [::events/highlight-into-new-note region]))
|
||||||
(.removeAllRanges (.getSelection js/window))
|
(.removeAllRanges (.getSelection js/window))
|
||||||
(reset! sel nil))}
|
(reset! sel nil))}
|
||||||
(if active (str "+ Highlight → " (:name note)) "+ New note from selection")]))]
|
(cond
|
||||||
|
note-target "+ Create note for mark"
|
||||||
|
active (str "+ Highlight → " (:name note))
|
||||||
|
:else "+ New note from selection")]))]
|
||||||
(when url [:div.script-empty "Loading script…"]))]
|
(when url [:div.script-empty "Loading script…"]))]
|
||||||
(when (and url active note)
|
(when (and url active note)
|
||||||
[note-editor note active scroll-to-region!])]
|
[note-editor note active scroll-to-region!])]
|
||||||
|
|
@ -1967,12 +2060,15 @@
|
||||||
(let [pane @(rf/subscribe [::subs/pane])
|
(let [pane @(rf/subscribe [::subs/pane])
|
||||||
draft @(rf/subscribe [::subs/draft-group])
|
draft @(rf/subscribe [::subs/draft-group])
|
||||||
draft? (some? draft)
|
draft? (some? draft)
|
||||||
|
note-target @(rf/subscribe [::subs/note-target])
|
||||||
save-err @(rf/subscribe [::subs/save-error])]
|
save-err @(rf/subscribe [::subs/save-error])]
|
||||||
[:div.pane-wrap
|
[:div.pane-wrap
|
||||||
(when save-err [:div.err {:style {:padding "4px 8px"}} "△ " save-err])
|
(when save-err [:div.err {:style {:padding "4px 8px"}} "△ " save-err])
|
||||||
[:div.pane-tabs
|
[:div.pane-tabs
|
||||||
[:button {:class (when (= pane :annotations) "active")
|
[:button {:class (when (= pane :annotations) "active")
|
||||||
:on-click #(rf/dispatch [::events/set-pane :annotations])} "Annotations"]
|
:on-click #(rf/dispatch [(if note-target
|
||||||
|
::events/cancel-note-target
|
||||||
|
::events/set-pane) :annotations])} "Annotations"]
|
||||||
[:button {:class (when (= pane :script) "active")
|
[:button {:class (when (= pane :script) "active")
|
||||||
:on-click #(rf/dispatch [::events/set-pane :script])} "Script"]]
|
:on-click #(rf/dispatch [::events/set-pane :script])} "Script"]]
|
||||||
[:div.pane-body
|
[:div.pane-body
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue