feat: production deploy (Fly + R2) + frame controls and picker glow
Deploy: - Multi-stage Dockerfile (shadow-cljs release → Python+ffmpeg runtime), fly.toml (one machine, scale-to-zero, SQLite on a volume), .dockerignore, DEPLOY.md - Same-origin release build: api-port defaults "9001" for dev, "" in release (no CORS in prod) - env-driven settings (SECRET_KEY/DEBUG/ALLOWED_HOSTS/DB_PATH/CSRF), WhiteNoise serving static + the SPA, R2 (S3-compatible) media storage when R2_* is set - thumbnails reworked storage-agnostic: download clip, ffmpeg select-pass, upload tiles via default_storage (R2 in prod, disk in dev) - pin channels/daphne; add whitenoise/django-storages/boto3 UI: - frame step buttons flanking play (disabled at timeline bounds) - Add-annotation button moved into the toolbar, made prominent - picker mode pulses a 1-bit glow on clips / frame readout / (while linking) annotation bars; linking an annotation bar inserts a timeline link Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
parent
e00a056209
commit
e6d150c375
12 changed files with 322 additions and 87 deletions
|
|
@ -278,7 +278,16 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px;
|
|||
.slider input { width: 150px; touch-action: none; accent-color: var(--ink); }
|
||||
.transport { display: flex; align-items: center; gap: 4px; }
|
||||
.play-btn { padding: 2px 12px; font-size: 14px; }
|
||||
.step-btn { padding: 2px 7px; font-size: 11px; letter-spacing: -1px; }
|
||||
.step-btn { padding: 2px 9px; font-size: 13px; line-height: 1; }
|
||||
.step-btn:disabled, button:disabled { opacity: .3; cursor: default; }
|
||||
|
||||
/* prominent primary action: a filled (inverted) Chicago button with a hard shadow */
|
||||
.add-annotation { font-family: var(--chicago); font-weight: bold; font-size: 13px;
|
||||
background: var(--ink); color: var(--paper);
|
||||
border: 1px solid var(--ink); border-radius: 8px; padding: 4px 14px;
|
||||
cursor: pointer; box-shadow: 2px 2px 0 var(--ink); white-space: nowrap; }
|
||||
.add-annotation:hover { background: var(--paper); color: var(--ink); }
|
||||
.add-annotation:active { box-shadow: 0 0 0 var(--ink); transform: translate(2px, 2px); }
|
||||
|
||||
.timeline-scroll { flex: 1; overflow-y: auto; overflow-x: hidden; min-height: 0; }
|
||||
.timeline { display: flex; align-items: flex-start; background: var(--paper); }
|
||||
|
|
@ -409,6 +418,26 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px;
|
|||
.toolbar .dark-toggle { margin-left: auto; }
|
||||
.home-head-right { display: flex; align-items: center; gap: 8px; }
|
||||
|
||||
/* picker mode: pulse a black ring around everything pickable so it's obvious
|
||||
what to click — a 1-bit "glow" (no blur, stays HyperCard). Clips + the frame
|
||||
readout pulse whenever a draft is open; annotations pulse while linking. */
|
||||
@keyframes hc-pulse {
|
||||
0%, 100% { box-shadow: 0 0 0 1px var(--ink); }
|
||||
50% { box-shadow: 0 0 0 4px var(--ink); }
|
||||
}
|
||||
.picking .clip.clickable,
|
||||
.picking .fr-clip.clickable,
|
||||
.linking .ann-bar {
|
||||
position: relative; z-index: 6;
|
||||
animation: hc-pulse 0.9s ease-in-out infinite;
|
||||
}
|
||||
.linking .ann-bar { cursor: pointer; }
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.picking .clip.clickable, .picking .fr-clip.clickable, .linking .ann-bar {
|
||||
animation: none; box-shadow: 0 0 0 3px var(--ink);
|
||||
}
|
||||
}
|
||||
|
||||
/* iOS Safari zooms the page when focusing a control with font-size < 16px.
|
||||
On touch devices bump every form control to 16px so tapping never zooms. */
|
||||
@media (pointer: coarse) {
|
||||
|
|
|
|||
|
|
@ -32,4 +32,8 @@
|
|||
:dev
|
||||
{:compiler-options
|
||||
{:closure-defines
|
||||
{ }}}}}}
|
||||
{ }}}
|
||||
:release
|
||||
{:compiler-options
|
||||
{:closure-defines
|
||||
{tl.api/api-port ""}}}}}} ; prod: same-origin (no :9001, no CORS)
|
||||
|
|
|
|||
|
|
@ -7,8 +7,17 @@
|
|||
(:require [ajax.core :as ajax]
|
||||
[re-frame.core :as rf]))
|
||||
|
||||
;; Dev serves the UI (shadow, :8280) and the API (Django, :9001) on different
|
||||
;; ports, so the default pins the API to :9001. The production *release* build
|
||||
;; overrides this to "" (see shadow-cljs.edn), so the app talks to its own origin
|
||||
;; — one domain, no CORS.
|
||||
(goog-define api-port "9001")
|
||||
|
||||
(def base
|
||||
(str (.. js/window -location -protocol) "//" (.. js/window -location -hostname) ":9001"))
|
||||
(let [l js/window.location]
|
||||
(if (= "" api-port)
|
||||
(.-origin l)
|
||||
(str (.-protocol l) "//" (.-hostname l) ":" api-port))))
|
||||
|
||||
(def ^:private json-resp (ajax/json-response-format {:keywords? true}))
|
||||
|
||||
|
|
@ -74,8 +83,10 @@
|
|||
[id]
|
||||
(when-let [s @socket] (.close s))
|
||||
(when id
|
||||
(let [proto (if (= "https:" (.. js/window -location -protocol)) "wss:" "ws:")
|
||||
url (str proto "//" (.. js/window -location -hostname) ":9001/ws/projects/" id "/")
|
||||
(let [l js/window.location
|
||||
proto (if (= "https:" (.-protocol l)) "wss:" "ws:")
|
||||
host (if (= "" api-port) (.-host l) (str (.-hostname l) ":" api-port))
|
||||
url (str proto "//" host "/ws/projects/" id "/")
|
||||
s (js/WebSocket. url)]
|
||||
(reset! socket s)
|
||||
(set! (.-onerror s) (fn [_] (js/console.warn "scene sync socket error; peer updates paused")))
|
||||
|
|
|
|||
|
|
@ -282,7 +282,10 @@
|
|||
[:div.ann-bar {:title (:name a)
|
||||
:on-mouse-down (when-not (:draft a)
|
||||
(fn [e] (.stopPropagation e)
|
||||
(rf/dispatch [::events/expand (:id a)])))
|
||||
(if linking
|
||||
(do (.preventDefault e) ; pick: link to this timeline
|
||||
(commit-link! {:kind :timeline :ref (:id a)} (:name a)))
|
||||
(rf/dispatch [::events/expand (:id a)]))))
|
||||
:style {:position "absolute" :top (+ 2 (* i 18)) :height 14
|
||||
:left (px lo fps zoom) :width (max 4 (px (- hi lo) fps zoom))
|
||||
:background (str (:color a) (if (:draft a) "44" "cc"))
|
||||
|
|
@ -611,10 +614,6 @@
|
|||
[:div.muted "No description yet."])
|
||||
(when authed?
|
||||
[:button.edit-btn {:on-click #(rf/dispatch [::events/edit-here ctx])} "✎ Edit"])])
|
||||
(when authed?
|
||||
[:div.commentary-head
|
||||
[:button.add-btn {:on-click #(rf/dispatch [::events/open-draft])}
|
||||
"+ Add annotation"]])
|
||||
(if (seq anns)
|
||||
(doall
|
||||
(for [a anns]
|
||||
|
|
@ -753,9 +752,22 @@
|
|||
|
||||
(defn toolbar []
|
||||
(let [zoom @(rf/subscribe [::subs/zoom]) row-h @(rf/subscribe [::subs/row-h])
|
||||
playing? @(rf/subscribe [::subs/playing?])]
|
||||
playing? @(rf/subscribe [::subs/playing?])
|
||||
ph (js/Math.round @(rf/subscribe [::subs/playhead]))
|
||||
len @(rf/subscribe [::subs/length])
|
||||
authed? @(rf/subscribe [::subs/authed?])
|
||||
authoring? (some? @(rf/subscribe [::subs/draft-group]))]
|
||||
[:div.toolbar
|
||||
[:button.play-btn {:on-click toggle-play! :title "Play / pause"} (if playing? "⏸" "▶")]
|
||||
[:div.transport
|
||||
[:button.step-btn {:title "Previous frame" :disabled (<= ph 0)
|
||||
:on-click #(goto! (max 0 (dec ph)))} "◁"]
|
||||
[:button.play-btn {:on-click toggle-play! :title "Play / pause"} (if playing? "⏸" "▶")]
|
||||
[:button.step-btn {:title "Next frame" :disabled (>= ph len)
|
||||
:on-click #(goto! (min len (inc ph)))} "▷"]]
|
||||
(when (and authed? (not authoring?))
|
||||
[:button.add-annotation {:title "Add annotation"
|
||||
:on-click #(rf/dispatch [::events/open-draft])}
|
||||
"+ Annotation"])
|
||||
[:label.slider "Zoom X"
|
||||
[:input {:type "range" :min 2 :max 400 :value zoom
|
||||
:on-change #(rf/dispatch [::events/set-zoom (js/parseFloat (.. % -target -value))])}]]
|
||||
|
|
@ -894,8 +906,12 @@
|
|||
proj @(rf/subscribe [::subs/project])
|
||||
authed? @(rf/subscribe [::subs/authed?])
|
||||
thumb-status @(rf/subscribe [::subs/thumbnail-status])
|
||||
load-err @(rf/subscribe [::subs/load-error])]
|
||||
[:div.app
|
||||
load-err @(rf/subscribe [::subs/load-error])
|
||||
;; "picker mode": a draft is open (pick clips/frame for marks); + linking
|
||||
;; (pick clips/frame/annotations for a link). Drives the pulsing glow.
|
||||
authoring? (some? @(rf/subscribe [::subs/draft-group]))
|
||||
linking? (some? @(rf/subscribe [::subs/linking]))]
|
||||
[:div.app {:class (str (when authoring? "picking ") (when linking? "linking"))}
|
||||
[:div.top
|
||||
[:div.video-pane [video-monitor] [frame-readout]]
|
||||
[:div.annot-pane [annot-pane]]]
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue