From c151c9c7cf36eec97c66c0616855af61c8a7f80f Mon Sep 17 00:00:00 2001 From: charlie Date: Thu, 28 Jan 2021 18:01:03 +0800 Subject: [PATCH] improve(electron): compatible paste assets for multiple platform --- resources/js/preload.js | 33 ++-- src/main/frontend/components/editor.cljs | 186 ++++++++++++----------- src/main/frontend/handler/editor.cljs | 40 ++--- 3 files changed, 137 insertions(+), 122 deletions(-) diff --git a/resources/js/preload.js b/resources/js/preload.js index 71696004867..d6de517e7f9 100644 --- a/resources/js/preload.js +++ b/resources/js/preload.js @@ -5,6 +5,19 @@ const { ipcRenderer, contextBridge, shell, clipboard, BrowserWindow } = require( const IS_MAC = process.platform === 'darwin' const IS_WIN32 = process.platform === 'win32' +function getFilePathFromClipboard () { + if (IS_WIN32) { + const rawFilePath = clipboard.read('FileNameW') + return rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '') + } + + return clipboard.read('public.file-url').replace('file://', '') +} + +function isClipboardHasImage () { + return !clipboard.readImage().isEmpty() +} + contextBridge.exposeInMainWorld('apis', { doAction: async (arg) => { return await ipcRenderer.invoke('main', arg) @@ -65,11 +78,12 @@ contextBridge.exposeInMainWorld('apis', { await fs.promises.mkdir(assetsRoot, { recursive: true }) - from = from || getFilePathFromClipboard() + from = decodeURIComponent(from || getFilePathFromClipboard()) if (from) { // console.debug('copy file: ', from, dest) - return await fs.promises.copyFile(from, dest) + await fs.promises.copyFile(from, dest) + return path.basename(from) } // support image @@ -83,16 +97,6 @@ contextBridge.exposeInMainWorld('apis', { nImg.toPNG() ) } - - // fns - function getFilePathFromClipboard () { - if (IS_WIN32) { - const rawFilePath = clipboard.read('FileNameW') - return rawFilePath.replace(new RegExp(String.fromCharCode(0), 'g'), '') - } - - return clipboard.read('public.file-url').replace('file://', '') - } }, toggleMaxOrMinActiveWindow (isToggleMin = false) { @@ -107,5 +111,8 @@ contextBridge.exposeInMainWorld('apis', { */ async _callApplication (type, ...args) { return await ipcRenderer.invoke('call-application', type, ...args) - } + }, + + getFilePathFromClipboard, + isClipboardHasImage }) diff --git a/src/main/frontend/components/editor.cljs b/src/main/frontend/components/editor.cljs index 970f50f02f6..d8cc441fe38 100644 --- a/src/main/frontend/components/editor.cljs +++ b/src/main/frontend/components/editor.cljs @@ -58,7 +58,7 @@ (editor-handler/insert-command! id command-steps format {:restore? restore-slash?}))) - :class "black"})))) + :class "black"})))) (rum/defc block-commands < rum/reactive [id format] @@ -71,7 +71,7 @@ (editor-handler/insert-command! id (get (into {} matched) chosen) format {:last-pattern commands/angle-bracket})) - :class "black"})))) + :class "black"})))) (rum/defc page-search < rum/reactive {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)} @@ -108,7 +108,7 @@ page-ref-text format {:last-pattern (str "[[" (if @editor-handler/*selected-text "" q)) - :postfix-fn (fn [s] (util/replace-first "]]" s ""))})))) + :postfix-fn (fn [s] (util/replace-first "]]" s ""))})))) non-exist-page-handler (fn [_state] (state/set-editor-show-page-search! false) (if (state/org-mode-file-link? (state/get-current-repo)) @@ -128,9 +128,9 @@ (ui/auto-complete matched-pages {:on-chosen chosen-handler - :on-enter non-exist-page-handler + :on-enter non-exist-page-handler :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a page"] - :class "black"})))))) + :class "black"})))))) (rum/defc block-search < rum/reactive {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)} @@ -156,7 +156,7 @@ (util/format "((%s))" uuid-string) format {:last-pattern (str "((" (if @editor-handler/*selected-text "" q)) - :postfix-fn (fn [s] (util/replace-first "))" s ""))}) + :postfix-fn (fn [s] (util/replace-first "))" s ""))}) ;; Save it so it'll be parsed correctly in the future (editor-handler/set-block-property! (:block/uuid chosen) @@ -170,12 +170,12 @@ (util/cursor-move-forward input 2))] (ui/auto-complete matched-blocks - {:on-chosen chosen-handler - :on-enter non-exist-block-handler - :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a block"] + {:on-chosen chosen-handler + :on-enter non-exist-block-handler + :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a block"] :item-render (fn [{:block/keys [content]}] (subs content 0 64)) - :class "black"})))))) + :class "black"})))))) (rum/defc template-search < rum/reactive {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)} @@ -223,12 +223,12 @@ (state/set-editor-show-template-search! false))] (ui/auto-complete matched-templates - {:on-chosen chosen-handler - :on-enter non-exist-handler - :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a template"] + {:on-chosen chosen-handler + :on-enter non-exist-handler + :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a template"] :item-render (fn [[template _block-db-id]] template) - :class "black"})))))) + :class "black"})))))) (rum/defc mobile-bar < rum/reactive [parent-state parent-id] @@ -252,17 +252,17 @@ {:on-click #(commands/simple-insert! parent-id "[[]]" {:backward-pos 2 - :check-fn (fn [_ _ new-pos] - (reset! commands/*slash-caret-pos new-pos) - (commands/handle-step [:editor/search-page]))})} + :check-fn (fn [_ _ new-pos] + (reset! commands/*slash-caret-pos new-pos) + (commands/handle-step [:editor/search-page]))})} "[[]]"] [:button.font-extrabold.bottom-action.-mt-1 {:on-click #(commands/simple-insert! parent-id "(())" {:backward-pos 2 - :check-fn (fn [_ _ new-pos] - (reset! commands/*slash-caret-pos new-pos) - (commands/handle-step [:editor/search-block]))})} + :check-fn (fn [_ _ new-pos] + (reset! commands/*slash-caret-pos new-pos) + (commands/handle-step [:editor/search-block]))})} "(())"]]) (rum/defcs input < rum/reactive @@ -276,7 +276,7 @@ (let [input-value (get state ::input-value) input-option (get @state/state :editor/show-input)] (when (seq @input-value) - ;; no new line input + ;; no new line input (util/stop e) (let [[_id on-submit] (:rum/args state) {:keys [pos]} @*slash-caret-pos @@ -306,11 +306,11 @@ [:input.form-input.block.w-full.pl-2.sm:text-sm.sm:leading-5 (merge (cond-> - {:key (str "modal-input-" (name id)) - :id (str "modal-input-" (name id)) - :type (or type "text") - :on-change (fn [e] - (swap! input-value assoc id (util/evalue e))) + {:key (str "modal-input-" (name id)) + :id (str "modal-input-" (name id)) + :type (or type "text") + :on-change (fn [e] + (swap! input-value assoc id (util/evalue e))) :auto-complete (if (util/chrome?) "chrome-off" "off")} placeholder (assoc :placeholder placeholder)) @@ -357,31 +357,31 @@ (when-let [pos (rum/react pos)] (ui/css-transition {:class-names "fade" - :timeout {:enter 500 - :exit 300}} + :timeout {:enter 500 + :exit 300}} (absolute-modal cp set-default-width? pos))))) (rum/defc image-uploader < rum/reactive {:did-mount (fn [state] (let [[id format] (:rum/args state)] - (add-watch editor-handler/*image-pending-file ::pending-image + (add-watch editor-handler/*asset-pending-file ::pending-asset (fn [_ _ _ f] (reset! *slash-caret-pos (util/get-caret-pos (gdom/getElement id))) - (editor-handler/upload-image id #js[f] format editor-handler/*image-uploading? true)))) + (editor-handler/upload-image id #js[f] format editor-handler/*asset-uploading? true)))) state) :will-unmount (fn [state] - (remove-watch editor-handler/*image-pending-file ::pending-image))} + (remove-watch editor-handler/*asset-pending-file ::pending-asset))} [id format] [:div.image-uploader [:input - {:id "upload-file" - :type "file" + {:id "upload-file" + :type "file" :on-change (fn [e] (let [files (.-files (.-target e))] - (editor-handler/upload-image id files format editor-handler/*image-uploading? false))) - :hidden true}] - (when-let [uploading? (util/react editor-handler/*image-uploading?)] - (let [processing (util/react editor-handler/*image-uploading-process)] + (editor-handler/upload-image id files format editor-handler/*asset-uploading? false))) + :hidden true}] + (when-let [uploading? (util/react editor-handler/*asset-uploading?)] + (let [processing (util/react editor-handler/*asset-uploading-process)] (transition-cp [:div.flex.flex-row.align-center.rounded-md.shadow-sm.bg-base-2.px-1.py-1 (ui/loading @@ -434,21 +434,21 @@ (profile "Insert block" (editor-handler/insert-new-block! state)))))))))) - ;; up + ;; up 38 (fn [state e] (when (and (not (gobj/get e "ctrlKey")) (not (gobj/get e "metaKey")) (not (editor-handler/in-auto-complete? input))) (editor-handler/on-up-down state e true))) - ;; down + ;; down 40 (fn [state e] (when (and (not (gobj/get e "ctrlKey")) (not (gobj/get e "metaKey")) (not (editor-handler/in-auto-complete? input))) (editor-handler/on-up-down state e false))) - ;; backspace + ;; backspace 8 (fn [state e] (let [node (gdom/getElement input-id) current-pos (:pos (util/get-caret-pos node)) @@ -464,7 +464,7 @@ nil (and (zero? current-pos) - ;; not the top block in a block page + ;; not the top block in a block page (not (and page (util/uuid-string? page) (= (medley/uuid page) block-id)))) @@ -482,7 +482,7 @@ (reset! *angle-bracket-caret-pos nil) (reset! *show-block-commands false)) - ;; pair + ;; pair (and deleted (contains? @@ -505,13 +505,13 @@ :else nil)) - ;; deleting hashtag + ;; deleting hashtag (and (= deleted "#") (state/get-editor-show-page-search-hashtag?)) (state/set-editor-show-page-search-hashtag! false) :else nil))) - ;; tab + ;; tab 9 (fn [state e] (let [input-id (state/get-edit-input-id) input (and input-id (gdom/getElement id)) @@ -615,18 +615,18 @@ (let [k (gobj/get e "key") format (:format (get-state state))] (when-not (state/get-editor-show-input) - (when (and @*show-commands (not= key-code 191)) ; not / + (when (and @*show-commands (not= key-code 191)) ; not / (let [matched-commands (editor-handler/get-matched-commands input)] (if (seq matched-commands) (do (reset! *show-commands true) (reset! *matched-commands matched-commands)) (reset! *show-commands false)))) - (when (and @*show-block-commands (not= key-code 188)) ; not < + (when (and @*show-block-commands (not= key-code 188)) ; not < (let [matched-block-commands (editor-handler/get-matched-block-commands input)] (if (seq matched-block-commands) (cond - (= key-code 9) ;tab + (= key-code 9) ;tab (when @*show-block-commands (util/stop e) (editor-handler/insert-command! input-id @@ -638,41 +638,41 @@ (reset! *matched-block-commands matched-block-commands)) (reset! *show-block-commands false)))) (editor-handler/close-autocomplete-if-outside input)))))))) - {:did-mount (fn [state] - (let [[{:keys [dummy? format block-parent-id]} id] (:rum/args state) - content (get-in @state/state [:editor/content id]) - input (gdom/getElement id)] - (when block-parent-id - (state/set-editing-block-dom-id! block-parent-id)) - (if (= :indent-outdent (state/get-editor-op)) - (when input - (when-let [pos (state/get-edit-pos)] - (util/set-caret-pos! input pos))) - (editor-handler/restore-cursor-pos! id content dummy?)) - - (when input - (dnd/subscribe! - input - :upload-images - {:drop (fn [e files] - (editor-handler/upload-image id files format editor-handler/*image-uploading? true))})) - - ;; Here we delay this listener, otherwise the click to edit event will trigger a outside click event, - ;; which will hide the editor so no way for editing. - (js/setTimeout #(keyboards-handler/esc-save! state) 100) - - (when-let [element (gdom/getElement id)] - (.focus element))) - state) - :did-remount (fn [_old-state state] - (keyboards-handler/esc-save! state) - state) + {:did-mount (fn [state] + (let [[{:keys [dummy? format block-parent-id]} id] (:rum/args state) + content (get-in @state/state [:editor/content id]) + input (gdom/getElement id)] + (when block-parent-id + (state/set-editing-block-dom-id! block-parent-id)) + (if (= :indent-outdent (state/get-editor-op)) + (when input + (when-let [pos (state/get-edit-pos)] + (util/set-caret-pos! input pos))) + (editor-handler/restore-cursor-pos! id content dummy?)) + + (when input + (dnd/subscribe! + input + :upload-images + {:drop (fn [e files] + (editor-handler/upload-image id files format editor-handler/*asset-uploading? true))})) + + ;; Here we delay this listener, otherwise the click to edit event will trigger a outside click event, + ;; which will hide the editor so no way for editing. + (js/setTimeout #(keyboards-handler/esc-save! state) 100) + + (when-let [element (gdom/getElement id)] + (.focus element))) + state) + :did-remount (fn [_old-state state] + (keyboards-handler/esc-save! state) + state) :will-unmount (fn [state] (let [{:keys [id value format block repo dummy? config]} (get-state state) file? (:file? config)] (when-let [input (gdom/getElement id)] - ;; (.removeEventListener input "paste" (fn [event] - ;; (append-paste-doc! format event))) + ;; (.removeEventListener input "paste" (fn [event] + ;; (append-paste-doc! format event))) (let [s (str "cljs-drag-n-drop." :upload-images) a (gobj/get input s) timer (:timer a)] @@ -698,8 +698,8 @@ (editor-handler/save-block! (get-state state) value)))) state)} [state {:keys [on-hide dummy? node format block block-parent-id] - :or {dummy? false} - :as option} id config] + :or {dummy? false} + :as option} id config] (let [content (state/get-edit-content)] [:div.editor-inner {:class (if block "block-editor" "non-block-editor")} (when config/mobile? (mobile-bar state id)) @@ -741,20 +741,28 @@ (when-let [handled (let [pick-one-allowed-item (fn [items] - (when (and items (.-length items)) - (let [files (. (js/Array.from items) (filter #(= (.-kind %) "file"))) - it (gobj/get files 0) ;;; TODO: support multiple files - mime (and it (.-type it))] - (cond - (contains? #{"image/jpeg" "image/png" "image/jpg" "image/gif"} mime) [:image (. it getAsFile)])))) + (if (util/electron?) + + (let [existed-file-path (js/window.apis.getFilePathFromClipboard) + has-file-path? (not (string/blank? existed-file-path)) + has-image? (js/window.apis.isClipboardHasImage)] + (if (or has-image? has-file-path?) + [:asset (js/File. #js[] (if has-file-path? existed-file-path "image.png"))])) + + (when (and items (.-length items)) + (let [files (. (js/Array.from items) (filter #(= (.-kind %) "file"))) + it (gobj/get files 0) ;;; TODO: support multiple files + mime (and it (.-type it))] + (cond + (contains? #{"image/jpeg" "image/png" "image/jpg" "image/gif"} mime) [:asset (. it getAsFile)]))))) clipboard-data (gobj/get e "clipboardData") items (or (.-items clipboard-data) (.-files clipboard-data)) picked (pick-one-allowed-item items)] - (when (and picked (get picked 1)) + (js/console.log (get picked 1)) + (if (get picked 1) (match picked - [:image file] (editor-handler/set-image-pending-file file)) - true))] + [:asset file] (editor-handler/set-asset-pending-file file))))] (util/stop e))) :auto-focus false}) diff --git a/src/main/frontend/handler/editor.cljs b/src/main/frontend/handler/editor.cljs index def6f4537a3..3cae66545ac 100644 --- a/src/main/frontend/handler/editor.cljs +++ b/src/main/frontend/handler/editor.cljs @@ -48,9 +48,9 @@ [lambdaisland.glogi :as log])) ;; FIXME: should support multiple images concurrently uploading -(defonce *image-pending-file (atom nil)) -(defonce *image-uploading? (atom false)) -(defonce *image-uploading-process (atom 0)) +(defonce *asset-pending-file (atom nil)) +(defonce *asset-uploading? (atom false)) +(defonce *asset-uploading-process (atom 0)) (defonce *selected-text (atom nil)) (defn- get-selection-and-format @@ -1561,7 +1561,7 @@ (if (util/electron?) (let [from (.-path file)] (p/then (js/window.apis.copyFileToAssets dir filename from) - #(p/resolved [filename file]))) + #(p/resolved [filename (if (string? %) (js/File. #js[] %) file)]))) (p/then (fs/write-file! repo dir filename (.stream file) nil) #(p/resolved [filename file])))))))) @@ -1600,7 +1600,7 @@ (string/replace #"^assets://" ""))) nil)))) (defn upload-image - [id files format uploading? drop-or-paste?] + [id ^js files format uploading? drop-or-paste?] (let [repo (state/get-current-repo) block (state/get-edit-block)] (if (config/local-db? repo) @@ -1618,32 +1618,32 @@ (p/finally (fn [] (reset! uploading? false) - (reset! *image-uploading? false) - (reset! *image-uploading-process 0)))) + (reset! *asset-uploading? false) + (reset! *asset-uploading-process 0)))) (image/upload - files - (fn [file file-name file-type] + files + (fn [file file-name file-type] (image-handler/request-presigned-url - file file-name file-type - uploading? - (fn [signed-url] + file file-name file-type + uploading? + (fn [signed-url] (insert-command! id (get-image-link format signed-url file-name) format {:last-pattern (if drop-or-paste? "" commands/slash) :restore? true}) - (reset! *image-uploading? false) - (reset! *image-uploading-process 0)) - (fn [e] + (reset! *asset-uploading? false) + (reset! *asset-uploading-process 0)) + (fn [e] (let [process (* (/ (gobj/get e "loaded") (gobj/get e "total")) 100)] - (reset! *image-uploading? false) - (reset! *image-uploading-process process))))))))) + (reset! *asset-uploading? false) + (reset! *asset-uploading-process process))))))))) -(defn set-image-pending-file [file] - (reset! *image-pending-file file)) +(defn set-asset-pending-file [file] + (reset! *asset-pending-file file)) ;; Editor should track some useful information, like editor modes. ;; For example: @@ -1791,7 +1791,7 @@ [input] (or @*show-commands @*show-block-commands - @*image-uploading? + @*asset-uploading? (state/get-editor-show-input) (state/get-editor-show-page-search?) (state/get-editor-show-block-search?)