diff --git a/tl/resources/public/css/app.css b/tl/resources/public/css/app.css index 4658ac5..6d53811 100644 --- a/tl/resources/public/css/app.css +++ b/tl/resources/public/css/app.css @@ -376,6 +376,12 @@ body { overflow: hidden; background: var(--desktop); background-size: 2px 2px; .err { color: var(--ink); font-weight: bold; font-size: 12px; margin: 8px 0; border: 1px solid var(--ink); padding: 4px 8px; background: var(--shade); } +/* 1-bit upload progress: black fill in a bordered white track */ +.upload-progress { display: flex; align-items: center; gap: 8px; margin: 10px 0; } +.upload-track { flex: 1; height: 14px; border: 1px solid var(--ink); background: var(--paper); } +.upload-fill { height: 100%; background: var(--ink); transition: width .15s linear; } +.upload-pct { font-family: var(--mono); font-size: 11px; min-width: 36px; text-align: right; } + /* editor: project bar above the timeline — a Mac window title bar (selected) */ .project-bar { display: flex; gap: 10px; align-items: center; padding: 4px 8px; background: var(--ink); color: var(--paper); font-family: var(--chicago); diff --git a/tl/src/tl/db.cljs b/tl/src/tl/db.cljs index 4c98f4f..fbc9391 100644 --- a/tl/src/tl/db.cljs +++ b/tl/src/tl/db.cljs @@ -7,6 +7,7 @@ :projects nil ; project list (when signed in) :project nil ; current project detail {:id :name :clip :otio} :create-error nil ; new-project upload failure + :create-progress nil ; new-project upload progress (0..1) while uploading :login-error nil ; sign-in failure :net-error nil ; backend unreachable (fetch-me / logout) :projects-error nil ; project-list fetch failure diff --git a/tl/src/tl/events.cljs b/tl/src/tl/events.cljs index 87f1e1d..a89b177 100644 --- a/tl/src/tl/events.cljs +++ b/tl/src/tl/events.cljs @@ -215,18 +215,39 @@ ;; --- create project ------------------------------------------------------- +;; Raw XHR (not :http-xhrio) so we can report upload progress for the big clip. +(rf/reg-fx :upload-project + (fn [{:keys [name otio clip]}] + (let [fd (doto (js/FormData.) + (.append "name" name) (.append "otio" otio) (.append "clip" clip)) + xhr (js/XMLHttpRequest.)] + (.open xhr "POST" (str api/base "/api/projects/")) + (set! (.-withCredentials xhr) true) + (set! (.. xhr -upload -onprogress) + (fn [e] (when (.-lengthComputable e) + (rf/dispatch [::create-progress (/ (.-loaded e) (.-total e))])))) + (set! (.-onload xhr) + (fn [_] + (let [body (try (js->clj (js/JSON.parse (.-responseText xhr)) :keywordize-keys true) + (catch :default _ {}))] + (if (<= 200 (.-status xhr) 299) + (rf/dispatch [::project-created body]) + (rf/dispatch [::project-create-error {:status (.-status xhr) :response body}]))))) + (set! (.-onerror xhr) (fn [_] (rf/dispatch [::project-create-error {:status 0}]))) + (.send xhr fd)))) + (rf/reg-event-fx ::create-project (fn [{:keys [db]} [_ name otio clip]] - (let [fd (doto (js/FormData.) - (.append "name" name) (.append "otio" otio) (.append "clip" clip))] - {:db (assoc db :create-error nil) - :http-xhrio (api/upload "/api/projects/" fd - {:on-success [::project-created] - :on-failure [::project-create-error]})}))) -(rf/reg-event-fx ::project-created (fn [_ [_ p]] {:route [:project/show {:id (:id p)}]})) + {:db (assoc db :create-error nil :create-progress 0) + :upload-project {:name name :otio otio :clip clip}})) +(rf/reg-event-db ::create-progress (fn [db [_ p]] (assoc db :create-progress p))) +(rf/reg-event-fx ::project-created + (fn [{:keys [db]} [_ p]] + {:db (assoc db :create-progress nil) :route [:project/show {:id (:id p)}]})) (rf/reg-event-db ::project-create-error (fn [db [_ failure]] - (assoc db :create-error (api/error-message failure "Upload failed.")))) + (assoc db :create-error (api/error-message failure "Upload failed.") + :create-progress nil))) ;; --- view ----------------------------------------------------------------- diff --git a/tl/src/tl/subs.cljs b/tl/src/tl/subs.cljs index 5e17035..ba481e2 100644 --- a/tl/src/tl/subs.cljs +++ b/tl/src/tl/subs.cljs @@ -18,6 +18,7 @@ (rf/reg-sub ::thumbnails (fn [db] (get-in db [:project :thumbnails]))) (rf/reg-sub ::thumbnail-status (fn [db] (get-in db [:project :thumbnail_status]))) (rf/reg-sub ::create-error (fn [db] (:create-error db))) +(rf/reg-sub ::create-progress (fn [db] (:create-progress db))) (rf/reg-sub ::login-error (fn [db] (:login-error db))) (rf/reg-sub ::net-error (fn [db] (:net-error db))) (rf/reg-sub ::projects-error (fn [db] (:projects-error db))) diff --git a/tl/src/tl/views.cljs b/tl/src/tl/views.cljs index e1770e8..94b8af5 100644 --- a/tl/src/tl/views.cljs +++ b/tl/src/tl/views.cljs @@ -986,7 +986,9 @@ (defn create-page [] (let [pname (r/atom "") otio (atom nil) clip (atom nil)] (fn [] - (let [err @(rf/subscribe [::subs/create-error])] + (let [err @(rf/subscribe [::subs/create-error]) + prog @(rf/subscribe [::subs/create-progress]) + uploading? (some? prog)] [:div.home [:div.home-head [:h1 "New project"] [:a {:href (routes/href :projects)} "← back"]] [:div.form @@ -999,9 +1001,14 @@ [:input {:type "file" :accept "video/*" :on-change #(reset! clip (-> % .-target .-files (aget 0)))}]] (when err [:div.err err]) + (when uploading? + [:div.upload-progress + [:div.upload-track [:div.upload-fill {:style {:width (str (* 100 prog) "%")}}]] + [:span.upload-pct (str (js/Math.round (* 100 prog)) "%")]]) [:div.form-actions - [:button.save {:disabled (or (str/blank? @pname) (not @otio) (not @clip)) - :on-click #(rf/dispatch [::events/create-project @pname @otio @clip])} "Create"] + [:button.save {:disabled (or uploading? (str/blank? @pname) (not @otio) (not @clip)) + :on-click #(rf/dispatch [::events/create-project @pname @otio @clip])} + (if uploading? "Uploading…" "Create")] [:a.cancel {:href (routes/href :projects)} "Cancel"]]]])))) (defn main-panel []