diff --git a/frontend/src/arthur/events/project.cljs b/frontend/src/arthur/events/project.cljs index bc9ce5c..f0c9283 100644 --- a/frontend/src/arthur/events/project.cljs +++ b/frontend/src/arthur/events/project.cljs @@ -42,6 +42,7 @@ [arthur.flow.take :as take] [arthur.fx.http :as http] [arthur.synth :as synth] + [clojure.string :as str] [re-frame.core :as rf])) (defn- analysis-payload [analysis] @@ -577,9 +578,16 @@ ::save! {:id (when-not (false? can-edit?) pid) :base (get-in db [:project :seq]) :cid cid - :label (or (:label clip) (name id)) + :label (or (not-empty (get-in db [:project :name])) + (:label clip) (name id)) :clip clip}})))) +(rf/reg-event-fx + ::rename + (fn [{:keys [db]} [_ value]] + {:db (assoc-in db [:project :name] (not-empty (str/trim value))) + :dispatch [::save {:auto? true :force? true}]})) + (rf/reg-event-fx ::project-setting (fn [{:keys [db]} [_ key value]] diff --git a/frontend/src/arthur/ui/topbar.cljs b/frontend/src/arthur/ui/topbar.cljs index 9411589..5435f3a 100644 --- a/frontend/src/arthur/ui/topbar.cljs +++ b/frontend/src/arthur/ui/topbar.cljs @@ -13,55 +13,68 @@ [arthur.ui.share :as share] [arthur.ui.snapshots :as snapshots] [arthur.ui.undo :as undo] - [re-frame.core :as rf])) + [re-frame.core :as rf] + [reagent.core :as r])) -(defn- exporter [] +(defn- exporter [open?] (let [{:keys [target zoom busy? done total]} @(rf/subscribe [::export/state]) targets @(rf/subscribe [::export/targets])] [:<> - ;; ONE SELECT over everything exportable: each symbol, and each instance - ;; in the open one. They are one list because they - ;; are one kind of request — render this, alone. - ;; - ;; The option VALUE goes through `export/target-value`, never `name`: a - ;; symbol id is `:sym/face-8625` and an instance is a uuid, and writing - ;; either through `name` loses what identifies it. - [:select {:value (export/target-value target) - :disabled busy? - :title "what to render" - :on-change #(rf/dispatch [::export/set-target - (export/target-id (.. % -target -value))])} - (doall - (for [{:keys [label] :as target} targets] - ^{:key (export/target-value target)} - [:option {:value (export/target-value target)} - (str (when (:isolate target) "· ") label)]))] - [:select {:value zoom :disabled busy? - :title "integer zoom" - :on-change #(rf/dispatch [::export/set-zoom - (js/parseInt (.. % -target -value) 10)])} - (doall (for [z export/zooms] ^{:key z} [:option {:value z} (str z "x")]))] - [:button {:disabled busy? :on-click #(rf/dispatch [::export/start]) - :title "a lossless PNG sequence and the mixed audio, in one zip"} - (if busy? (str "rendering " done "/" total) "export")]])) + [:button {:disabled busy? :on-click #(reset! open? true)} "export…"] + (when @open? + [:<> [:div.export-scrim {:on-click #(when-not busy? (reset! open? false))}] + [:section.export-dialog {:role "dialog" :aria-modal true :aria-label "Export"} + [:h2 "Export"] + [:label.export-field "what to render" + [:select {:value (export/target-value target) :disabled busy? + :on-change #(rf/dispatch [::export/set-target + (export/target-id (.. % -target -value))])} + (doall (for [{:keys [label] :as t} targets] + ^{:key (export/target-value t)} + [:option {:value (export/target-value t)} + (str (when (:isolate t) "· ") label)]))]] + [:label.export-field "scale" + [:select {:value zoom :disabled busy? + :on-change #(rf/dispatch [::export/set-zoom + (js/parseInt (.. % -target -value) 10)])} + (doall (for [z export/zooms] ^{:key z} [:option {:value z} (str z "×")]))]] + [:p.dim "PNG sequence and audio in a ZIP archive"] + [:div.row + [:button {:disabled busy? :on-click #(rf/dispatch [::export/start])} + (if busy? (str "rendering " done "/" total) "start export")] + [:button {:disabled busy? :on-click #(reset! open? false)} "close"]]]])])) (defn view [] - (let [{project-name :name :keys [busy? seq] project-status :status} - @(rf/subscribe [::playback/project]) - {footage-status :status} @(rf/subscribe [::playback/footage]) - {export-status :status} @(rf/subscribe [::export/state])] - [:header.top - [:a.brand {:href "/" :title "your projects" - :on-click (fn [e] (.preventDefault e) (collab/navigate! "/"))} "arthur"] - [:span.status - (str (or project-name "untitled") (when seq (str " r" seq)) - ;; One line, and the most recent thing to have happened wins it. A - ;; strip with three status slots is three empty boxes most of the time. - (when-let [said (or export-status project-status footage-status)] - (str " · " said)))] - [exporter] - [:button {:disabled busy? :on-click #(rf/dispatch [::collab/create])} "new"] - [openmenu/view] - [undo/view] - [snapshots/view] - [share/view]])) + (r/with-let [renaming? (r/atom false) + draft (r/atom "") + export-open? (r/atom false)] + (let [{project-name :name :keys [busy? seq status]} @(rf/subscribe [::playback/project]) + {footage-status :status} @(rf/subscribe [::playback/footage]) + {export-status :status} @(rf/subscribe [::export/state]) + title (or project-name "untitled") + commit! (fn [] + (rf/dispatch [::project/rename @draft]) + (reset! renaming? false))] + [:header.top + [:a.brand {:href "/" :title "your projects" + :on-click (fn [e] (.preventDefault e) (collab/navigate! "/"))} "arthur"] + [:button {:disabled busy? :on-click #(rf/dispatch [::collab/create])} "new"] + [openmenu/view] + [undo/view] + [snapshots/view] + (if @renaming? + [:input.project-name {:auto-focus true :value @draft + :on-change #(reset! draft (.. % -target -value)) + :on-blur (fn [_] (commit!)) + :on-key-down (fn [e] + (case (.-key e) + "Enter" (do (.preventDefault e) (commit!)) + "Escape" (reset! renaming? false) + nil))}] + [:button.project-title {:disabled busy? :title "click to rename project" + :on-click (fn [] (reset! draft title) + (reset! renaming? true))} + title (when seq (str " r" seq))]) + [:span.status (or export-status status footage-status)] + [exporter export-open?] + [share/view]]))) diff --git a/static/arthur/app.css b/static/arthur/app.css index a92c9f4..fb46057 100644 --- a/static/arthur/app.css +++ b/static/arthur/app.css @@ -194,6 +194,19 @@ input[type="range"] { width: 100%; accent-color: var(--sel); } color: var(--dim); } +.project-title { max-width: 22vw; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } +.project-name { width: 180px; } +.export-scrim { position: fixed; inset: 0; z-index: 50; background: rgba(0, 0, 0, .45); } +.export-dialog { + position: fixed; z-index: 51; top: 50%; left: 50%; transform: translate(-50%, -50%); + width: min(360px, calc(100vw - 32px)); padding: 14px; + background: var(--pane); border: 1px solid var(--line); border-radius: 3px; + box-shadow: 0 8px 30px rgba(0, 0, 0, .45); +} +.export-dialog h2 { margin: 0 0 12px; font: inherit; font-weight: 600; } +.export-field { display: grid; gap: 4px; margin: 8px 0; color: var(--dim); } +.export-field select { color: var(--fg); } + /* -------------------------------------------------------------------------- the open menu */ @@ -520,6 +533,10 @@ button.share-button:hover, button.share-button.on { filter: brightness(1.1); } letter-spacing: .07em; } +.inspector-form { display: grid; grid-template-columns: 1fr 1fr; gap: 5px; margin-top: 7px; } +.inspector-field { display: grid; gap: 2px; color: var(--dim); } +.inspector-field input { width: 100%; color: var(--fg); } + /* Label / value, once, for every read-only fact in the pane. */ .facts { display: grid; grid-template-columns: auto 1fr; gap: 2px 8px; margin: 0; } .facts dt { color: var(--dim); }