mirror of
https://github.com/penpot/penpot.git
synced 2026-08-26 22:58:43 +00:00
524 lines
19 KiB
Clojure
524 lines
19 KiB
Clojure
;; This Source Code Form is subject to the terms of the Mozilla Public
|
|
;; License, v. 2.0. If a copy of the MPL was not distributed with this
|
|
;; file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
|
;;
|
|
;; Copyright (c) KALEIDOS INC Sucursal en España SL
|
|
|
|
(ns app.main.ui.dashboard.fonts
|
|
(:require-macros [app.main.style :as stl])
|
|
(:require
|
|
[app.common.data :as d]
|
|
[app.common.data.macros :as dm]
|
|
[app.common.media :as cm]
|
|
[app.common.schema :as sm]
|
|
[app.common.types.font :as ctf]
|
|
[app.common.uuid :as uuid]
|
|
[app.config :as cf]
|
|
[app.main.data.fonts :as df]
|
|
[app.main.data.modal :as modal]
|
|
[app.main.data.notifications :as ntf]
|
|
[app.main.repo :as rp]
|
|
[app.main.store :as st]
|
|
[app.main.ui.components.context-menu-a11y :refer [context-menu*]]
|
|
[app.main.ui.components.file-uploader :refer [file-uploader]]
|
|
[app.main.ui.ds.product.empty-placeholder :refer [empty-placeholder*]]
|
|
[app.main.ui.icons :as deprecated-icon]
|
|
[app.main.ui.notifications.context-notification :refer [context-notification]]
|
|
[app.util.dom :as dom]
|
|
[app.util.http :as http]
|
|
[app.util.i18n :as i18n :refer [tr]]
|
|
[app.util.keyboard :as kbd]
|
|
[beicon.v2.core :as rx]
|
|
[cuerdas.core :as str]
|
|
[okulary.core :as l]
|
|
[rumext.v2 :as mf]))
|
|
|
|
(def ^:private accept-font-types
|
|
(str (str/join "," cm/font-types)
|
|
;; A workaround to solve a problem with chrome input selector
|
|
",.ttf,application/font-woff,.woff,.woff2,.otf"))
|
|
|
|
(defn- use-page-title
|
|
[team section]
|
|
(mf/with-effect [team]
|
|
(when team
|
|
(let [tname (if (:is-default team)
|
|
(tr "dashboard.your-penpot")
|
|
(:name team))]
|
|
(case section
|
|
:fonts (dom/set-html-title (tr "title.dashboard.fonts" tname))
|
|
:providers (dom/set-html-title (tr "title.dashboard.font-providers" tname)))))))
|
|
|
|
(defn- bad-font-family-tmp?
|
|
[font]
|
|
(and (contains? font :font-family-tmp)
|
|
(str/blank? (:font-family-tmp font))))
|
|
|
|
(defn- handle-font-upload
|
|
[{:keys [id] :as item}]
|
|
(->> (df/upload-font-variant item)
|
|
;; This delay fixes the UI problem. When the font is uploaded quickly,
|
|
;; the user only sees a flicker on the button. With a small delay we
|
|
;; have a clear transition from uploading state to uploaded
|
|
(rx/delay-at-least 2000)
|
|
(rx/map (fn [font]
|
|
[id font]))))
|
|
|
|
(mf/defc header*
|
|
{::mf/memo true
|
|
::mf/private true}
|
|
[{:keys [section team]}]
|
|
(use-page-title team section)
|
|
[:header {:class (stl/css :dashboard-header) :data-testid "dashboard-header"}
|
|
[:div#dashboard-fonts-title {:class (stl/css :dashboard-title)}
|
|
[:h1 (tr "labels.fonts")]]])
|
|
|
|
(mf/defc font-variant-display-name*
|
|
{::mf/private true}
|
|
[{:keys [variant]}]
|
|
[:span (cm/font-display-variant (:variant-name variant)
|
|
(:font-weight variant)
|
|
(:font-style variant))])
|
|
|
|
(mf/defc uploaded-fonts*
|
|
{::mf/private true}
|
|
[{:keys [team installed-fonts]}]
|
|
(let [fonts* (mf/use-state {})
|
|
fonts (deref fonts*)
|
|
font-vals (mf/with-memo [fonts]
|
|
(->> fonts
|
|
(into [] (map val))
|
|
(not-empty)))
|
|
|
|
team-id (:id team)
|
|
|
|
input-ref (mf/use-ref)
|
|
|
|
uploading* (mf/use-state #{})
|
|
uploading (deref uploading*)
|
|
|
|
disable-upload-all?
|
|
(some bad-font-family-tmp? fonts)
|
|
|
|
problematic-fonts?
|
|
(some :height-warning? (vals fonts))
|
|
|
|
on-click
|
|
(mf/use-fn #(dom/click (mf/ref-val input-ref)))
|
|
|
|
on-selected
|
|
(mf/use-fn
|
|
(mf/deps team-id installed-fonts)
|
|
(fn [blobs]
|
|
(->> (df/process-upload blobs team-id)
|
|
(rx/subs! (fn [result]
|
|
(swap! fonts* df/merge-and-group-fonts installed-fonts result))
|
|
(fn [error]
|
|
(js/console.error "error" error))))))
|
|
|
|
on-upload
|
|
(mf/use-fn
|
|
(mf/deps fonts)
|
|
(fn [event]
|
|
(let [id (-> (dom/get-current-target event)
|
|
(dom/get-data "id")
|
|
(uuid/parse))
|
|
item (get fonts id)]
|
|
(swap! uploading* conj id)
|
|
(->> (handle-font-upload item)
|
|
(rx/subs! (fn [[previous-id font]]
|
|
(swap! fonts* dissoc previous-id)
|
|
(swap! uploading* disj previous-id)
|
|
(st/emit! (df/add-font font)))
|
|
(fn [cause]
|
|
(st/emit! (ntf/error (tr "errors.bad-font" (first (:names item)))))
|
|
(js/console.error "Unexpected error on uploading font" cause)))))))
|
|
|
|
on-blur-name
|
|
(mf/use-fn
|
|
(mf/deps installed-fonts)
|
|
(fn [event]
|
|
(let [target (dom/get-current-target event)
|
|
id (-> target
|
|
(dom/get-data "id")
|
|
(uuid/parse))
|
|
name (dom/get-value target)]
|
|
(when (and (not (str/blank? name))
|
|
(sm/validate ctf/schema:font-family name))
|
|
(swap! fonts* df/rename-and-regroup id name installed-fonts)))))
|
|
|
|
on-change-name
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(let [target (dom/get-current-target event)
|
|
id (-> target
|
|
(dom/get-data "id")
|
|
(uuid/parse))
|
|
name (dom/get-value target)]
|
|
(swap! fonts* update id assoc :font-family-tmp name))))
|
|
|
|
on-delete
|
|
(mf/use-fn
|
|
(mf/deps team)
|
|
(fn [event]
|
|
(let [id (-> (dom/get-current-target event)
|
|
(dom/get-data "id")
|
|
(uuid/parse))]
|
|
(swap! fonts* dissoc id))))
|
|
|
|
on-upload-all
|
|
(mf/use-fn
|
|
(mf/deps font-vals)
|
|
(fn [_]
|
|
(->> (rx/from font-vals)
|
|
(rx/tap #(swap! uploading* conj (:id %)))
|
|
(rx/mapcat (fn [{:keys [id] :as item}]
|
|
(->> (handle-font-upload item)
|
|
(rx/catch (fn [cause]
|
|
(st/emit! (ntf/error (tr "errors.bad-font" (first (:names item)))))
|
|
(js/console.error "Unexpected error on uploading font" cause)
|
|
(rx/of [id nil]))))))
|
|
|
|
(rx/subs! (fn [[previous-id font]]
|
|
(swap! fonts* dissoc previous-id)
|
|
(swap! uploading* disj previous-id)
|
|
(when font
|
|
(st/emit! (df/add-font font))))))))
|
|
|
|
on-dismis-all
|
|
(mf/use-fn
|
|
(mf/deps fonts)
|
|
(fn [_]
|
|
(run! #(swap! fonts* dissoc (:id %)) (vals fonts))))]
|
|
|
|
[:div {:class (stl/css :dashboard-fonts-upload)}
|
|
[:div {:class (stl/css :dashboard-fonts-hero)}
|
|
[:div {:class (stl/css :desc)}
|
|
[:h2 (tr "labels.upload-custom-fonts")]
|
|
[:> i18n/tr-html* {:content (tr "dashboard.fonts.hero-text1")}]
|
|
|
|
[:button {:class (stl/css :btn-primary)
|
|
:on-click on-click
|
|
:tab-index "0"}
|
|
[:span (tr "labels.add-custom-font")]
|
|
[:& file-uploader {:input-id "font-upload"
|
|
:accept accept-font-types
|
|
:multi true
|
|
:ref input-ref
|
|
:on-selected on-selected}]]
|
|
|
|
(when-let [url cf/terms-of-service-uri]
|
|
[:& context-notification {:content (tr "dashboard.fonts.hero-text2" url)
|
|
:level :default
|
|
:is-html true}])
|
|
|
|
(when problematic-fonts?
|
|
[:& context-notification {:content (tr "dashboard.fonts.warning-text")
|
|
:level :warning
|
|
:is-html true}])]]
|
|
|
|
[:*
|
|
(when (seq fonts)
|
|
[:div {:class (stl/css :font-item :table-row)}
|
|
[:span (tr "dashboard.fonts.fonts-added" (i18n/c (count fonts)))]
|
|
[:div {:class (stl/css :table-field :options)}
|
|
[:button {:class (stl/css-case
|
|
:btn-primary true
|
|
:disabled disable-upload-all?)
|
|
:on-click on-upload-all
|
|
:data-testid "upload-all"
|
|
:disabled disable-upload-all?}
|
|
[:span (tr "dashboard.fonts.upload-all")]]
|
|
[:button {:class (stl/css :btn-secondary)
|
|
:on-click on-dismis-all
|
|
:data-testid "dismiss-all"}
|
|
[:span (tr "dashboard.fonts.dismiss-all")]]]])
|
|
|
|
(for [{:keys [id] :as item} (sort-by :font-family font-vals)]
|
|
(let [uploading? (contains? uploading id)
|
|
disable-upload? (or uploading? (bad-font-family-tmp? item))]
|
|
[:div {:class (stl/css :font-item :table-row)
|
|
:key (dm/str id)}
|
|
[:div {:class (stl/css :table-field :family)}
|
|
[:input {:type "text"
|
|
:data-id (dm/str id)
|
|
:on-blur on-blur-name
|
|
:on-change on-change-name
|
|
:default-value (:font-family item)}]]
|
|
[:div {:class (stl/css :table-field :variants)}
|
|
[:span {:class (stl/css :label)}
|
|
[:> font-variant-display-name* {:variant item}]]]
|
|
|
|
[:div {:class (stl/css :table-field :filenames)}
|
|
(for [item (:names item)]
|
|
[:span {:key (dm/str "name-" item)} item])]
|
|
|
|
[:div {:class (stl/css :table-field :options)}
|
|
(when (:height-warning? item)
|
|
[:span {:class (stl/css :icon :failure)}
|
|
deprecated-icon/msg-neutral])
|
|
|
|
[:button {:on-click on-upload
|
|
:data-id (dm/str id)
|
|
:class (stl/css-case
|
|
:btn-primary true
|
|
:upload-button true
|
|
:disabled disable-upload?)
|
|
:disabled disable-upload?}
|
|
(if ^boolean uploading?
|
|
(tr "labels.uploading")
|
|
(tr "labels.upload"))]
|
|
[:span {:class (stl/css :icon :close)
|
|
:data-id (dm/str id)
|
|
:on-click on-delete}
|
|
deprecated-icon/close]]]))]]))
|
|
|
|
(mf/defc installed-font-context-menu*
|
|
{::mf/private true}
|
|
[{:keys [is-open on-close on-edit on-download on-delete]}]
|
|
(let [options (mf/with-memo [on-edit on-download on-delete]
|
|
[{:name (tr "labels.edit")
|
|
:id "font-edit"
|
|
:handler on-edit}
|
|
{:name (tr "labels.download-simple")
|
|
:id "font-download"
|
|
:handler on-download}
|
|
{:name (tr "labels.delete")
|
|
:id "font-delete"
|
|
:handler on-delete}])]
|
|
[:> context-menu*
|
|
{:on-close on-close
|
|
:show is-open
|
|
:fixed false
|
|
:min-width true
|
|
:top -15
|
|
:left -115
|
|
:options options}]))
|
|
|
|
(mf/defc installed-font*
|
|
{::mf/private true
|
|
::mf/wrap [mf/memo]}
|
|
[{:keys [font-id variants can-edit]}]
|
|
(let [font (first variants)
|
|
|
|
menu-open* (mf/use-state false)
|
|
menu-open? (deref menu-open*)
|
|
edition* (mf/use-state false)
|
|
edition? (deref edition*)
|
|
|
|
state* (mf/use-state (:font-family font))
|
|
font-family (deref state*)
|
|
|
|
variants
|
|
(mf/with-memo [variants]
|
|
(sort-by (fn [item]
|
|
[(:font-weight item)
|
|
(if (= "normal" (:font-style item)) 1 2)])
|
|
variants))
|
|
|
|
on-change
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(reset! state* (dom/get-target-val event))))
|
|
|
|
on-edit
|
|
(mf/use-fn #(reset! edition* true))
|
|
|
|
on-menu-open
|
|
(mf/use-fn #(reset! menu-open* true))
|
|
|
|
on-menu-close
|
|
(mf/use-fn #(reset! menu-open* false))
|
|
|
|
on-save
|
|
(mf/use-fn
|
|
(mf/deps font-family)
|
|
(fn [_]
|
|
(reset! edition* false)
|
|
(when-not (str/blank? font-family)
|
|
(if (sm/validate ctf/schema:font-family font-family)
|
|
(st/emit! (df/update-font {:id font-id :name font-family}))
|
|
(st/emit! (ntf/error (tr "errors.font-family-invalid-chars")))))))
|
|
|
|
on-key-down
|
|
(mf/use-fn
|
|
(mf/deps on-save)
|
|
(fn [event]
|
|
(when (kbd/enter? event)
|
|
(on-save event))))
|
|
|
|
on-cancel
|
|
(mf/use-fn
|
|
(fn [_]
|
|
(reset! edition* false)
|
|
(reset! state* (:font-family font))))
|
|
|
|
on-delete-font
|
|
(mf/use-fn
|
|
(mf/deps font-id)
|
|
(fn []
|
|
(let [options {:type :confirm
|
|
:title (tr "modals.delete-font.title")
|
|
:message (tr "modals.delete-font.message")
|
|
:accept-label (tr "labels.delete")
|
|
:on-accept (fn [_props]
|
|
(st/emit! (df/delete-font font-id)))}]
|
|
(st/emit! (modal/show options)))))
|
|
|
|
on-download
|
|
(mf/use-fn
|
|
(mf/deps variants)
|
|
(fn [_event]
|
|
(let [variant (first variants)
|
|
variant-id (:id variant)
|
|
multiple? (> (count variants) 1)
|
|
cmd (if multiple? :download-font-family :download-font)
|
|
params (if multiple? {:font-id font-id} {:id variant-id})]
|
|
(->> (rp/cmd! cmd params)
|
|
(rx/mapcat (fn [{:keys [name uri]}]
|
|
(->> (http/send! {:uri uri :method :get :response-type :blob})
|
|
(rx/map :body)
|
|
(rx/map (fn [blob] (d/vec2 name blob))))))
|
|
(rx/subs! (fn [[filename blob]]
|
|
(dom/trigger-download filename blob))
|
|
(fn [error]
|
|
(js/console.error "error downloading font" error)
|
|
(st/emit! (ntf/error (tr "errors.generic")))))))))
|
|
|
|
on-delete-variant
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(let [id (-> (dom/get-current-target event)
|
|
(dom/get-data "id")
|
|
(uuid/parse))
|
|
options {:type :confirm
|
|
:title (tr "modals.delete-font-variant.title")
|
|
:message (tr "modals.delete-font-variant.message")
|
|
:accept-label (tr "labels.delete")
|
|
:on-accept (fn [_props]
|
|
(st/emit! (df/delete-font-variant id)))}]
|
|
(st/emit! (modal/show options)))))]
|
|
|
|
[:div {:class (stl/css :font-item :table-row)}
|
|
[:div {:class (stl/css-case :table-field true
|
|
:family true
|
|
:is-edition edition?)}
|
|
(if ^boolean edition?
|
|
[:input {:type "text"
|
|
:auto-focus true
|
|
:default-value font-family
|
|
:on-key-down on-key-down
|
|
:on-change on-change}]
|
|
[:span (:font-family font)])]
|
|
|
|
[:div {:class (stl/css :table-field :variants)}
|
|
(for [{:keys [id] :as item} variants]
|
|
[:div {:class (stl/css-case :variant true
|
|
:inhert-variant (not can-edit))
|
|
:key (dm/str id)}
|
|
[:span {:class (stl/css :label)}
|
|
[:> font-variant-display-name* {:variant item}]]
|
|
(when can-edit
|
|
[:span
|
|
{:class (stl/css :icon :close)
|
|
:data-id (dm/str id)
|
|
:on-click on-delete-variant}
|
|
deprecated-icon/add])])]
|
|
|
|
(if ^boolean edition?
|
|
[:div {:class (stl/css :table-field :options)}
|
|
[:button
|
|
{:disabled (str/blank? font-family)
|
|
:on-click on-save
|
|
:class (stl/css-case :btn-primary true
|
|
:btn-disabled (str/blank? font-family))}
|
|
(tr "labels.save")]
|
|
[:button {:class (stl/css :icon :close)
|
|
:on-click on-cancel}
|
|
deprecated-icon/close]]
|
|
|
|
(when can-edit
|
|
[:div {:class (stl/css :table-field :options)}
|
|
[:span {:class (stl/css :icon)
|
|
:on-click on-menu-open}
|
|
deprecated-icon/menu]
|
|
|
|
[:> installed-font-context-menu*
|
|
{:on-close on-menu-close
|
|
:is-open menu-open?
|
|
:on-delete on-delete-font
|
|
:on-download on-download
|
|
:on-edit on-edit}]]))]))
|
|
|
|
(mf/defc installed-fonts*
|
|
[{:keys [fonts can-edit]}]
|
|
(let [sterm (mf/use-state "")
|
|
|
|
matches?
|
|
#(str/includes? (str/lower (:font-family %)) @sterm)
|
|
|
|
on-change
|
|
(mf/use-fn
|
|
(fn [event]
|
|
(let [val (dom/get-target-val event)]
|
|
(reset! sterm (str/lower val)))))]
|
|
|
|
[:div {:class (stl/css :dashboard-installed-fonts)}
|
|
(cond
|
|
(seq fonts)
|
|
[:*
|
|
[:h3 (tr "labels.installed-fonts")]
|
|
[:div {:class (stl/css :installed-fonts-header)}
|
|
[:div {:class (stl/css :table-field :family)} (tr "labels.font-family")]
|
|
[:div {:class (stl/css :table-field :variants)} (tr "labels.font-variants")]
|
|
[:div {:class (stl/css :table-field :search-input)}
|
|
[:input {:placeholder (tr "labels.search-font")
|
|
:default-value ""
|
|
:on-change on-change}]]]
|
|
(for [[font-id variants] (->> (vals fonts)
|
|
(filter matches?)
|
|
(group-by :font-id))]
|
|
[:> installed-font* {:key (dm/str font-id "-installed")
|
|
:font-id font-id
|
|
:can-edit can-edit
|
|
:variants variants}])]
|
|
|
|
(nil? fonts)
|
|
[:div {:class (stl/css :fonts-placeholder)}
|
|
[:div {:class (stl/css :icon)} deprecated-icon/loader]
|
|
[:div {:class (stl/css :label)} (tr "dashboard.loading-fonts")]]
|
|
|
|
:else
|
|
(if ^boolean can-edit
|
|
[:div {:class (stl/css :fonts-placeholder)}
|
|
[:div {:class (stl/css :icon)} deprecated-icon/text]
|
|
[:div {:class (stl/css :label)} (tr "dashboard.fonts.empty-placeholder")]]
|
|
|
|
[:> empty-placeholder*
|
|
{:title (tr "dashboard.fonts.empty-placeholder-viewer")
|
|
:subtitle (tr "dashboard.fonts.empty-placeholder-viewer-sub")
|
|
:type 2}]))]))
|
|
|
|
(def ^:private ref:fonts
|
|
(l/derived :fonts st/state))
|
|
|
|
(mf/defc fonts-page*
|
|
[{:keys [team]}]
|
|
(let [fonts (mf/deref ref:fonts)
|
|
permissions (:permissions team)
|
|
can-edit (:can-edit permissions)]
|
|
[:*
|
|
[:> header* {:team team :section :fonts}]
|
|
[:section {:class (stl/css :dashboard-container :dashboard-fonts)}
|
|
(when ^boolean can-edit
|
|
[:> uploaded-fonts* {:team team :installed-fonts fonts}])
|
|
[:> installed-fonts*
|
|
{:team team :fonts fonts :can-edit can-edit}]]]))
|
|
|
|
(mf/defc font-providers-page*
|
|
[{:keys [team]}]
|
|
[:*
|
|
[:> header* {:team team :section :providers}]
|
|
[:section {:class (stl/css :dashboard-container)}
|
|
[:span "font providers"]]])
|