浏览代码

Enhance/pdf enhancements (#8616)

- Improve interactions about the popup modal from the PDF viewer toolbar
- Persist highlights data with multiple-lines text format (for friendly Git diff)
- Blink the text highlight when scrolling to position
- Open the PDF viewer in the system Window
Charlie 2 年之前
父节点
当前提交
55b5149f4a

+ 1 - 1
src/electron/electron/core.cljs

@@ -300,7 +300,7 @@
       (.on app "ready"
            (fn []
              (let [t0 (setup-interceptor! app)
-                   ^js win (win/create-main-window)
+                   ^js win (win/create-main-window!)
                    _ (reset! *win win)]
                (logger/info (str "Logseq App(" (.getVersion app) ") Starting... "))
 

+ 1 - 1
src/electron/electron/handler.cljs

@@ -605,7 +605,7 @@
 (defn open-new-window!
   "Persist db first before calling! Or may break db persistency"
   []
-  (let [win (win/create-main-window)]
+  (let [win (win/create-main-window!)]
     (win/on-close-actions! win close-watcher-when-orphaned!)
     (win/setup-window-listeners! win)
     win))

+ 72 - 37
src/electron/electron/window.cljs

@@ -8,6 +8,7 @@
             ["path" :as path]
             ["url" :as URL]
             [electron.state :as state]
+            [cljs-bean.core :as bean]
             [clojure.core.async :as async]
             [clojure.string :as string]))
 
@@ -18,44 +19,51 @@
                          (str "file://" (path/join js/__dirname "index.html"))
                          (str "file://" (path/join js/__dirname "electron.html"))))
 
-(defn create-main-window
+(defn create-main-window!
   ([]
-   (create-main-window MAIN_WINDOW_ENTRY))
+   (create-main-window! MAIN_WINDOW_ENTRY nil))
   ([url]
+   (create-main-window! url nil))
+  ([url opts]
    (let [win-state (windowStateKeeper (clj->js {:defaultWidth 980 :defaultHeight 700}))
-         win-opts (cond->
-                    {:width                (.-width win-state)
-                     :height               (.-height win-state)
-                     :frame                true
-                     :titleBarStyle        "hiddenInset"
-                     :trafficLightPosition {:x 16 :y 16}
-                     :autoHideMenuBar      (not mac?)
-                     :webPreferences
-                     {:plugins                 true ; pdf
-                      :nodeIntegration         false
-                      :nodeIntegrationInWorker false
-                      :sandbox                 false
-                      :webSecurity             (not dev?)
-                      :contextIsolation        true
-                      :spellcheck              ((fnil identity true) (cfgs/get-item :spell-check))
-                      ;; Remove OverlayScrollbars and transition `.scrollbar-spacing`
-                      ;; to use `scollbar-gutter` after the feature is implemented in browsers.
-                      :enableBlinkFeatures     'OverlayScrollbars'
-                      :preload                 (path/join js/__dirname "js/preload.js")}}
-                    linux?
-                    (assoc :icon (path/join js/__dirname "icons/logseq.png")))
-         win (BrowserWindow. (clj->js win-opts))]
+         win-opts  (cond->
+                     {:width                (.-width win-state)
+                      :height               (.-height win-state)
+                      :frame                true
+                      :titleBarStyle        "hiddenInset"
+                      :trafficLightPosition {:x 16 :y 16}
+                      :autoHideMenuBar      (not mac?)
+                      :webPreferences
+                      {:plugins                 true        ; pdf
+                       :nodeIntegration         false
+                       :nodeIntegrationInWorker false
+                       :nativeWindowOpen        true
+                       :sandbox                 false
+                       :webSecurity             (not dev?)
+                       :contextIsolation        true
+                       :spellcheck              ((fnil identity true) (cfgs/get-item :spell-check))
+                       ;; Remove OverlayScrollbars and transition `.scrollbar-spacing`
+                       ;; to use `scollbar-gutter` after the feature is implemented in browsers.
+                       :enableBlinkFeatures     'OverlayScrollbars'
+                       :preload                 (path/join js/__dirname "js/preload.js")}}
+
+                     (seq opts)
+                     (merge opts)
+
+                     linux?
+                     (assoc :icon (path/join js/__dirname "icons/logseq.png")))
+         win       (BrowserWindow. (clj->js win-opts))]
      (.manage win-state win)
      (.onBeforeSendHeaders (.. session -defaultSession -webRequest)
                            (clj->js {:urls (array "*://*.youtube.com/*")})
                            (fn [^js details callback]
-                             (let [url (.-url details)
-                                   urlObj (js/URL. url)
-                                   origin (.-origin urlObj)
+                             (let [url            (.-url details)
+                                   urlObj         (js/URL. url)
+                                   origin         (.-origin urlObj)
                                    requestHeaders (.-requestHeaders details)]
                                (if (and
-                                     (.hasOwnProperty requestHeaders "referer")
-                                     (not-empty (.-referer requestHeaders)))
+                                    (.hasOwnProperty requestHeaders "referer")
+                                    (not-empty (.-referer requestHeaders)))
                                  (callback #js {:cancel         false
                                                 :requestHeaders requestHeaders})
                                  (do
@@ -121,8 +129,8 @@
   [^js win]
   (when win
     (let [web-contents (. win -webContents)
-          new-win-handler
-          (fn [e url]
+          open-external!
+          (fn [url]
             (let [url (if (string/starts-with? url "file:")
                         (utils/safe-decode-uri-component url) url)
                   url (if-not win32? (string/replace url "file://" "") url)]
@@ -132,8 +140,7 @@
                           (.join path (. app getAppPath) %))
                         ["index.html" "electron.html"])
                 (logger/info "pass-window" url)
-                (open-default-app! url open)))
-            (.preventDefault e))
+                (open-default-app! url open))))
 
           will-navigate-handler
           (fn [e url]
@@ -141,13 +148,42 @@
             (open-default-app! url open))
 
           context-menu-handler
-          (context-menu/setup-context-menu! win)]
+          (context-menu/setup-context-menu! win)
+
+          window-open-handler
+          (fn [^js details]
+            (let [url         (.-url details)
+                  fullscreen? (.isFullScreen win)
+                  features    (string/split (.-features details) ",")
+                  features    (when (seq features)
+                                (reduce (fn [a b]
+                                          (let [[k v] (string/split b "=")]
+                                            (if (string? v)
+                                              (assoc a (keyword k) (parse-long (string/trim v)))
+                                              a))) {} features))]
+              (-> (if (= url "about:blank")
+                    (merge {:action "allow"
+                            :overrideBrowserWindowOptions
+                            {:frame                true
+                             :titleBarStyle        "default"
+                             :trafficLightPosition {:x 16 :y 16}
+                             :autoHideMenuBar      (not mac?)
+                             :fullscreenable       (not fullscreen?)
+                             :webPreferences
+                             {:plugins          true
+                              :nodeIntegration  false
+                              :webSecurity      (not dev?)
+                              :preload          (path/join js/__dirname "js/preload.js")
+                              :nativeWindowOpen true}}}
+                           features)
+                    (do (open-external! url) {:action "deny"}))
+                  (bean/->js))))]
 
       (doto web-contents
-        (.on "new-window" new-win-handler)
         (.on "will-navigate" will-navigate-handler)
         (.on "did-start-navigation" #(.send web-contents "persist-zoom-level" (.getZoomLevel web-contents)))
-        (.on "page-title-updated" #(.send web-contents "restore-zoom-level")))
+        (.on "page-title-updated" #(.send web-contents "restore-zoom-level"))
+        (.setWindowOpenHandler window-open-handler))
 
       (doto win
         (.on "enter-full-screen" #(.send web-contents "full-screen" "enter"))
@@ -157,7 +193,6 @@
       (fn []
         (doto web-contents
           (.off "context-menu" context-menu-handler)
-          (.off "new-window" new-win-handler)
           (.off "will-navigate" will-navigate-handler))
 
         (.off win "enter-full-screen")

+ 2 - 2
src/main/frontend/components/theme.cljs

@@ -1,5 +1,5 @@
 (ns frontend.components.theme
-  (:require [frontend.extensions.pdf.highlights :as pdf]
+  (:require [frontend.extensions.pdf.core :as pdf]
             [frontend.config :as config]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.handler.plugin-config :as plugin-config-handler]
@@ -105,4 +105,4 @@
       :on-click on-click}
      child
 
-     (pdf/playground)]))
+     (pdf/default-embed-playground)]))

+ 7 - 3
src/main/frontend/extensions/pdf/assets.cljs

@@ -1,6 +1,7 @@
 (ns frontend.extensions.pdf.assets
   (:require [cljs.reader :as reader]
             [clojure.string :as string]
+            [cljs.pprint :as pprint]
             [frontend.config :as config]
             [frontend.db.model :as db-model]
             [frontend.db.utils :as db-utils]
@@ -16,6 +17,7 @@
             [frontend.state :as state]
             [frontend.util :as util]
             [frontend.extensions.pdf.utils :as pdf-utils]
+            [frontend.extensions.pdf.windows :as pdf-windows]
             [logseq.graph-parser.config :as gp-config]
             [logseq.graph-parser.util.block-ref :as block-ref]
             [medley.core :as medley]
@@ -66,7 +68,7 @@
   (when hls-file
     (let [repo-cur (state/get-current-repo)
           repo-dir (config/get-repo-dir repo-cur)
-          data     (pr-str {:highlights highlights :extra extra})]
+          data     (with-out-str (pprint/pprint {:highlights highlights :extra extra}))]
       (fs/write-file! repo-cur repo-dir hls-file data {:skip-compare? true}))))
 
 (defn resolve-hls-data-by-key$
@@ -212,9 +214,11 @@
       (editor-handler/delete-block-aux! block true))))
 
 (defn copy-hl-ref!
-  [highlight]
+  [highlight ^js viewer]
   (when-let [ref-block (ensure-ref-block! (state/get-current-pdf) highlight)]
-    (util/copy-to-clipboard! (block-ref/->block-ref (:block/uuid ref-block)))))
+    (util/copy-to-clipboard!
+     (block-ref/->block-ref (:block/uuid ref-block)) nil
+     (pdf-windows/resolve-own-window viewer))))
 
 (defn open-block-ref!
   [block]

+ 91 - 62
src/main/frontend/extensions/pdf/highlights.cljs → src/main/frontend/extensions/pdf/core.cljs

@@ -1,4 +1,4 @@
-(ns frontend.extensions.pdf.highlights
+(ns frontend.extensions.pdf.core
   (:require [cljs-bean.core :as bean]
             [clojure.string :as string]
             [frontend.components.svg :as svg]
@@ -7,6 +7,7 @@
             [frontend.extensions.pdf.assets :as pdf-assets]
             [frontend.extensions.pdf.utils :as pdf-utils]
             [frontend.extensions.pdf.toolbar :refer [pdf-toolbar *area-dashed? *area-mode? *highlight-mode? *highlights-ctx*]]
+            [frontend.extensions.pdf.windows :as pdf-windows]
             [frontend.handler.notification :as notification]
             [frontend.config :as config]
             [frontend.modules.shortcut.core :as shortcut]
@@ -18,11 +19,13 @@
             [promesa.core :as p]
             [rum.core :as rum]))
 
-(defn dd [& args]
-  (apply js/console.debug (cons (str :debug-pdf) args)))
+(declare pdf-container system-embed-playground)
 
 (def *highlight-last-color (atom :yellow))
 
+(defn open-external-win! [pdf-current]
+  (pdf-windows/open-pdf-in-new-window! system-embed-playground pdf-current))
+
 (defn reset-current-pdf!
   []
   (state/set-state! :pdf/current nil))
@@ -64,13 +67,14 @@
 (rum/defc pdf-resizer
   "Watches for changes in the pdf container's width and adjusts the viewer."
   [^js viewer]
-  (let [el-ref (rum/use-ref nil)
+  (let [el-ref   (rum/use-ref nil)
         adjust-main-size!
-               (util/debounce
-                200 (fn [width]
-                      (let [root-el js/document.documentElement]
-                        (.setProperty (.-style root-el) "--ph-view-container-width" width)
-                        (pdf-utils/adjust-viewer-size! viewer))))]
+                 (util/debounce
+                  200 (fn [width]
+                        (let [root-el js/document.documentElement]
+                          (.setProperty (.-style root-el) "--ph-view-container-width" width)
+                          (pdf-utils/adjust-viewer-size! viewer))))
+        group-id (.-$groupIdentity viewer)]
 
     ;; draggable handler
     (rum/use-effect!
@@ -85,7 +89,7 @@
                    (let [width     js/document.documentElement.clientWidth
                          offset    (.-left (.-rect e))
                          el-ratio  (.toFixed (/ offset width) 6)
-                         target-el (js/document.getElementById "pdf-layout-container")]
+                         target-el (js/document.getElementById (str "pdf-layout-container_" group-id))]
                      (when target-el
                        (let [width (str (min (max (* el-ratio 100) 20) 80) "vw")]
                          (.setProperty (.-style target-el) "width" width)
@@ -106,9 +110,10 @@
 
   (rum/use-effect!
    (fn []
-     (let [cb #(clear-ctx-menu!)]
-       (js/setTimeout #(js/document.addEventListener "click" cb))
-       #(js/document.removeEventListener "click" cb)))
+     (let [cb  #(clear-ctx-menu!)
+           doc (pdf-windows/resolve-own-document viewer)]
+       (js/setTimeout #(.addEventListener doc "click" cb))
+       #(.removeEventListener doc "click" cb)))
    [])
 
   ;; TODO: precise position
@@ -131,12 +136,13 @@
                               content   (:content highlight)]
                           (case action
                             "ref"
-                            (pdf-assets/copy-hl-ref! highlight)
+                            (pdf-assets/copy-hl-ref! highlight viewer)
 
                             "copy"
                             (do
                               (util/copy-to-clipboard!
-                               (or (:text content) (pdf-utils/fix-selection-text-breakline (.toString selection))))
+                               (or (:text content) (pdf-utils/fix-selection-text-breakline (.toString selection))) nil
+                               (pdf-windows/resolve-own-window viewer))
                               (pdf-utils/clear-all-selection))
 
                             "link"
@@ -160,7 +166,7 @@
                                                         :properties properties})]
                                   (add-hl! highlight)
                                   (pdf-utils/clear-all-selection)
-                                  (pdf-assets/copy-hl-ref! highlight))
+                                  (pdf-assets/copy-hl-ref! highlight viewer))
 
                                 ;; update highlight
                                 (upd-hl! (assoc highlight :properties properties)))
@@ -172,7 +178,7 @@
     (rum/use-effect!
      (fn []
        (if (and @*highlight-mode? new?)
-         ;; TODO: wait for selection cleared ...
+         ;; wait for selection cleared ...
          (js/setTimeout #(action-fn! @*highlight-last-color true) 300)
          (let [^js el (rum/deref *el)
                {:keys [x y]} (util/calc-delta-rect-offset el (.closest el ".extensions__pdf-viewer"))]
@@ -239,7 +245,8 @@
             (.setData dt "text/plain" (str "((" id "))"))))]
 
     [:div.extensions__pdf-hls-text-region
-     {:on-click        open-ctx-menu!
+     {:id              (str "hl_" id)
+      :on-click        open-ctx-menu!
       :on-context-menu open-ctx-menu!}
 
      (map-indexed
@@ -352,7 +359,8 @@
     (when-let [vw-bounding (get-in vw-hl [:position :bounding])]
       (let [{:keys [color]} (:properties hl)]
         [:div.extensions__pdf-hls-area-region
-         {:ref             *el
+         {:id              id
+          :ref             *el
           :style           vw-bounding
           :data-color      color
           :draggable       "true"
@@ -553,7 +561,7 @@
      (fn []
        (let [fn-selection-ok
              (fn [^js/MouseEvent e]
-               (let [^js/Selection selection (js/document.getSelection)
+               (let [^js/Selection selection (.getSelection doc)
                      ^js/Range sel-range     (.getRangeAt selection 0)]
 
                  (cond
@@ -573,33 +581,33 @@
                (let [*dirty   (volatile! false)
                      fn-dirty #(vreset! *dirty true)]
 
-                 (js/document.addEventListener "selectionchange" fn-dirty)
-                 (js/document.addEventListener "mouseup"
-                                               (fn [^js e]
-                                                 (and @*dirty (fn-selection-ok e))
-                                                 (js/document.removeEventListener "selectionchange" fn-dirty))
-                                               #js {:once true})))
+                 (.addEventListener doc "selectionchange" fn-dirty)
+                 (.addEventListener doc "mouseup"
+                                    (fn [^js e]
+                                      (and @*dirty (fn-selection-ok e))
+                                      (.removeEventListener doc "selectionchange" fn-dirty))
+                                    #js {:once true})))
 
              fn-resize
              (partial pdf-utils/adjust-viewer-size! viewer)]
 
          ;;(doto (.-eventBus viewer))
 
-         (doto el
-           (.addEventListener "mousedown" fn-selection))
+         (when el
+           (.addEventListener el "mousedown" fn-selection))
 
-         (doto win
-           (.addEventListener "resize" fn-resize))
+         (when win
+           (.addEventListener win "resize" fn-resize))
 
          ;; destroy
          #(do
             ;;(doto (.-eventBus viewer))
 
-            (doto el
-              (.removeEventListener "mousedown" fn-selection))
+            (when el
+              (.removeEventListener el "mousedown" fn-selection))
 
-            (doto win
-              (.removeEventListener "resize" fn-resize)))))
+            (when win
+              (.removeEventListener win "resize" fn-resize)))))
 
      [viewer])
 
@@ -616,11 +624,6 @@
                                                   vw-pos       {:bounding bounding :rects sel-rects :page page}
                                                   sc-pos       (pdf-utils/vw-to-scaled-pos viewer vw-pos)]
 
-                                              ;; TODO: debug
-                                              ;;(dd "[VW x SC] ====>" vw-pos sc-pos)
-                                              ;;(dd "[Range] ====> [" page-info "]" (.toString sel-range) point)
-                                              ;;(dd "[Rects] ====>" sel-rects " [Bounding] ====>" bounding)
-
                                               {:id         nil
                                                :page       page
                                                :position   sc-pos
@@ -690,8 +693,8 @@
        :add-hl!         add-hl!
        })]))
 
-(rum/defc pdf-viewer
-  [_url ^js pdf-document {:keys [initial-hls initial-page initial-error]} ops]
+(rum/defc ^:large-vars/data-var pdf-viewer
+  [_url ^js pdf-document {:keys [identity filename initial-hls initial-page initial-error]} ops]
 
   (let [*el-ref (rum/create-ref)
         [state, set-state!] (rum/use-state {:viewer nil :bus nil :link nil :el nil})
@@ -713,8 +716,11 @@
                                                         #js {:linkService link-service :eventBus event-bus})
                                     :textLayerMode     2
                                     :annotationMode    2
-                                    :removePageBorders true})]
+                                    :removePageBorders true})
+             in-system-win?   (boolean (.closest el ".is-system-window"))]
 
+         (set! (.-$groupIdentity viewer) identity)
+         (set! (.-$inSystemWindow viewer) in-system-win?)
          (. link-service setDocument pdf-document)
          (. link-service setViewer viewer)
 
@@ -733,8 +739,8 @@
          (p/then (. viewer setDocument pdf-document)
                  #(set-state! {:viewer viewer :bus event-bus :link link-service :el el}))
 
-         ;; TODO: debug
-         (set! (. js/window -lsPdfViewer) viewer)
+         ;; TODO: set as active viewer
+         (set! (. js/window -lsActivePdfViewer) viewer)
 
          ;; set initial page
          (js/setTimeout
@@ -743,10 +749,19 @@
          ;; destroy
          (fn []
            (.destroy pdf-document)
-           (set! (. js/window -lsPdfViewer) nil)
+           (set! (. js/window -lsActivePdfViewer) nil)
            (.cleanup viewer))))
      [])
 
+    ;; update window title
+    (rum/use-effect!
+     (fn []
+       (when-let [^js viewer (:viewer state)]
+         (when (pdf-windows/check-viewer-in-system-win? viewer)
+           (some-> (pdf-windows/resolve-own-document viewer)
+                   (set! -title filename)))))
+     [(:viewer state)])
+
     ;; interaction events
     (rum/use-effect!
      (fn []
@@ -765,7 +780,8 @@
      [(:viewer state)
       (:loaded-pages ano-state)])
 
-    (let [^js viewer (:viewer state)]
+    (let [^js viewer        (:viewer state)
+          in-system-window? (some-> viewer (.-$inSystemWindow))]
       [:div.extensions__pdf-viewer-cnt
        [:div.extensions__pdf-viewer
         {:ref *el-ref :class (util/classnames [{:is-area-dashed area-dashed?}])}
@@ -783,11 +799,12 @@
              ops) "pdf-highlights")])]
 
        (when (and page-ready? viewer)
-         [(rum/with-key (pdf-resizer viewer) "pdf-resizer")
-          (rum/with-key (pdf-toolbar viewer) "pdf-toolbar")])])))
+         [(when-not in-system-window?
+            (rum/with-key (pdf-resizer viewer) "pdf-resizer"))
+          (rum/with-key (pdf-toolbar viewer {:on-external-window! #(open-external-win! (state/get-current-pdf))}) "pdf-toolbar")])])))
 
 (rum/defc ^:large-vars/data-var pdf-loader
-  [{:keys [url hls-file] :as pdf-current}]
+  [{:keys [url hls-file identity filename] :as pdf-current}]
   (let [*doc-ref       (rum/use-ref nil)
         [loader-state, set-loader-state!] (rum/use-state {:error nil :pdf-document nil :status nil})
         [hls-state, set-hls-state!] (rum/use-state {:initial-hls nil :latest-hls nil :extra nil :loaded false :error nil})
@@ -839,12 +856,13 @@
     ;; load document
     (rum/use-effect!
      (fn []
-       (let [get-doc$ (fn [^js opts] (.-promise (js/pdfjsLib.getDocument opts)))
-             opts     {:url           url
-                       :ownerDocument js/document
-                       :cMapUrl       "./cmaps/"
-                       ;;:cMapUrl       "https://cdn.jsdelivr.net/npm/[email protected]/cmaps/"
-                       :cMapPacked    true}]
+       (let [^js loader-el (rum/deref *doc-ref)
+             get-doc$      (fn [^js opts] (.-promise (js/pdfjsLib.getDocument opts)))
+             opts          {:url           url
+                            :ownerDocument (.-ownerDocument loader-el)
+                            :cMapUrl       "./cmaps/"
+                            ;;:cMapUrl       "https://cdn.jsdelivr.net/npm/[email protected]/cmaps/"
+                            :cMapPacked    true}]
 
          (set-loader-state! {:status :loading})
 
@@ -857,7 +875,7 @@
     (rum/use-effect!
      (fn []
        (when-let [error (:error loader-state)]
-         (dd "[ERROR loader]" (:error loader-state))
+         (js/console.error "[PDF loader]" (:error loader-state))
          (case (.-name error)
            "MissingPDFException"
            (do
@@ -901,7 +919,9 @@
           (when-let [pdf-document (and (:loaded hls-state) (:pdf-document loader-state))]
             [(rum/with-key (pdf-viewer
                             url pdf-document
-                            {:initial-hls   initial-hls
+                            {:identity      identity
+                             :filename      filename
+                             :initial-hls   initial-hls
                              :initial-page  initial-page
                              :initial-error initial-error}
                             {:set-dirty-hls! set-dirty-hls!
@@ -927,7 +947,8 @@
        #(set-ready! false))
      [identity])
 
-    [:div#pdf-layout-container.extensions__pdf-container
+    [:div.extensions__pdf-container
+     {:id (str "pdf-layout-container_" identity)}
      (when (and prepared identity ready)
        (pdf-loader pdf-current))]))
 
@@ -945,16 +966,24 @@
    [active])
   nil)
 
-(rum/defcs playground
+(rum/defcs default-embed-playground
   < rum/static rum/reactive
     (shortcut/mixin :shortcut.handler/pdf)
   []
-  (let [pdf-current (state/sub :pdf/current)]
+  (let [pdf-current (state/sub :pdf/current)
+        system-win? (state/sub :pdf/system-win?)]
     [:div.extensions__pdf-playground
 
-     (playground-effects (not (nil? pdf-current)))
+     (playground-effects (and (not system-win?)
+                              (not (nil? pdf-current))))
 
-     (when pdf-current
+     (when (and (not system-win?) pdf-current)
        (js/ReactDOM.createPortal
         (pdf-container pdf-current)
         (js/document.querySelector "#app-single-container")))]))
+
+(rum/defcs system-embed-playground
+  < rum/reactive
+  []
+  (let [pdf-current (state/sub :pdf/current)]
+    (pdf-container pdf-current)))

+ 31 - 2
src/main/frontend/extensions/pdf/pdf.css

@@ -56,7 +56,7 @@ input::-webkit-inner-spin-button {
     position: relative;
   }
 
-&-header {
+  &-header {
     display: flex;
     justify-content: flex-end;
     position: absolute;
@@ -721,7 +721,7 @@ input::-webkit-inner-spin-button {
   }
 }
 
-#pdf-layout-container {
+.extensions__pdf-container {
   background-color: transparent;
 
   .extensions__pdf-toolbar .buttons {
@@ -980,6 +980,17 @@ body.is-pdf-active {
   }
 }
 
+html.is-system-window {
+  .extensions__pdf-container {
+    width: 100vw;
+    height: 100vh;
+  }
+
+  .extensions__pdf-loader {
+    @apply w-full;
+  }
+}
+
 /* overrides for pdf_viewer.css from PDF.JS web viewer */
 
 .textLayer {
@@ -1016,3 +1027,21 @@ body.is-pdf-active {
     border: 2px dashed #ff3434;
   }
 }
+
+.hl-flash {
+  animation-name: hl-flash;
+  animation-duration: 0.3s;
+  animation-timing-function: ease;
+  animation-iteration-count: 2;
+  animation-direction: alternate;
+  animation-play-state: running;
+}
+
+@keyframes hl-flash {
+  from {
+    opacity: 0;
+  }
+  to {
+    opacity: 1;
+  }
+}

+ 155 - 134
src/main/frontend/extensions/pdf/toolbar.cljs

@@ -13,7 +13,8 @@
             [frontend.extensions.pdf.assets :as pdf-assets]
             [frontend.handler.editor :as editor-handler]
             [frontend.extensions.pdf.utils :as pdf-utils]
-            [frontend.handler.notification :as notification]))
+            [frontend.handler.notification :as notification]
+            [frontend.extensions.pdf.windows :refer [resolve-own-container] :as pdf-windows]))
 
 (declare make-docinfo-in-modal)
 
@@ -52,12 +53,19 @@
          (storage/set "ls-pdf-hl-block-is-colored" b)))
      [hl-block-colored?])
 
-    [:div.extensions__pdf-settings.hls-popup-overlay.visible
-     {:on-click (fn [^js/MouseEvent e]
-                  (let [target (.-target e)]
-                    (when-not (.contains (rum/deref *el-popup) target)
-                      (hide-settings!))))}
+    (rum/use-effect!
+     (fn []
+       (let [cb  #(let [^js target (.-target %)]
+                    (when (and (not (some-> (rum/deref *el-popup) (.contains target)))
+                               (nil? (.closest target ".ui__modal")))
+                      (hide-settings!)))
+             doc (resolve-own-container viewer)]
+         (js/setTimeout
+          #(.addEventListener doc "click" cb))
+         #(.removeEventListener doc "click" cb)))
+     [])
 
+    [:div.extensions__pdf-settings.hls-popup-overlay.visible
      [:div.extensions__pdf-settings-inner.hls-popup-box
       {:ref       *el-popup
        :tab-index -1}
@@ -68,7 +76,6 @@
                {:key it :class it :on-click #(select-theme! it)}
                (when (= theme it) (svg/check))])
             ["light", "warm", "dark"])]
-
       [:div.extensions__pdf-settings-item.toggle-input
        [:label (t :pdf/toggle-dashed)]
        (ui/toggle area-dashed? #(set-area-dashed? (not area-dashed?)) true)]
@@ -87,7 +94,6 @@
          (t :pdf/doc-metadata)
          (svg/icon-info)]]]]]))
 
-
 (rum/defc docinfo-display
   [info close-fn!]
   [:div#pdf-docinfo.extensions__pdf-doc-info
@@ -155,15 +161,15 @@
 
     (rum/use-effect!
      (fn []
-       (when-let [^js el-viewer (and viewer (js/document.querySelector "#pdf-layout-container"))]
+       (when-let [^js doc (resolve-own-container viewer)]
          (let [handler (fn [^js e]
                          (when-let [^js target (and (string/blank? (.-value (rum/deref *el-input)))
                                                     (.-target e))]
                            (when (and (not= "Search" (.-title target))
-                                      (not (.contains (rum/deref *el-finder) target)))
+                                      (not (some-> (rum/deref *el-finder) (.contains target))))
                              (close-finder!))))]
-           (.addEventListener el-viewer "click" handler)
-           #(.removeEventListener el-viewer "click" handler))))
+           (.addEventListener doc "click" handler)
+           #(.removeEventListener doc "click" handler))))
      [viewer])
 
     (rum/use-effect!
@@ -245,8 +251,6 @@
             (str " matches (\"" (:query find-state) "\")")]
            [:div.px-3.py-3.text-xs.opacity-80.text-red-600 "Not found."]))]]]))
 
-
-
 (rum/defc pdf-outline-item
   [^js viewer
    {:keys [title items parent dest expanded]}
@@ -347,11 +351,11 @@
        (for [{:keys [id content properties page] :as hl} hls
              :let [goto-ref! #(pdf-assets/goto-block-ref! hl)]]
          [:div.extensions__pdf-highlights-list-item
-          {:key      id
-           :class (when (= active id) "active")
-           :on-click (fn []
-                       (pdf-utils/scroll-to-highlight viewer hl)
-                       (set-active! id))
+          {:key             id
+           :class           (when (= active id) "active")
+           :on-click        (fn []
+                              (pdf-utils/scroll-to-highlight viewer hl)
+                              (set-active! id))
            :on-double-click goto-ref!}
           [:h6.flex
            [:span.flex.items-center
@@ -359,7 +363,7 @@
             [:strong "Page " page]]
 
            [:button
-            {:title (t :pdf/linked-ref)
+            {:title    (t :pdf/linked-ref)
              :on-click goto-ref!}
             (ui/icon "external-link")]]
 
@@ -374,27 +378,27 @@
 
 (rum/defc pdf-outline-&-highlights
   [^js viewer visible? set-visible!]
-  (let [*el-container (rum/create-ref)
+  (let [*el-container        (rum/use-ref nil)
         [active-tab, set-active-tab!] (rum/use-state "contents")
         set-outline-visible! #(set-active-tab! "contents")
-        contents? (= active-tab "contents")]
+        contents?            (= active-tab "contents")]
 
     (rum/use-effect!
      (fn []
-       (when-let [^js el-viewer (and viewer (js/document.querySelector "#pdf-layout-container"))]
-         (let [handler (fn [^js e]
-                         (when-let [^js target (.-target e)]
-                           (when (and
-                                  (not= "Outline" (.-title target))
-                                  (not (.contains (rum/deref *el-container) target)))
-                             (set-visible! false)
-                             (set-outline-visible!))))]
-           (.addEventListener el-viewer "click" handler)
-           #(.removeEventListener el-viewer "click" handler))))
-     [viewer *el-container])
+       (when-let [^js doc (resolve-own-container viewer)]
+         (let [cb (fn [^js e]
+                    (when-let [^js target (.-target e)]
+                      (when (and
+                             (not= "Outline" (.-title target))
+                             (not (some-> (rum/deref *el-container) (.contains target))))
+                        (set-visible! false)
+                        (set-outline-visible!))))]
+           (.addEventListener doc "click" cb)
+           #(.removeEventListener doc "click" cb))))
+     [viewer])
 
     [:div.extensions__pdf-outline-wrap.hls-popup-overlay
-     {:class    (util/classnames [{:visible visible?}])}
+     {:class (util/classnames [{:visible visible?}])}
 
      [:div.extensions__pdf-outline.hls-popup-box
       {:ref       *el-container
@@ -402,9 +406,9 @@
 
       [:div.extensions__pdf-outline-tabs
        [:div.inner
-        [:button {:class (when contents? "active")
+        [:button {:class    (when contents? "active")
                   :on-click #(set-active-tab! "contents")} "Contents"]
-        [:button {:class (when-not contents? "active")
+        [:button {:class    (when-not contents? "active")
                   :on-click #(set-active-tab! "highlights")} "Highlights"]]]
 
       [:div.extensions__pdf-outline-panels
@@ -413,21 +417,24 @@
          (pdf-highlights-list viewer))]]]))
 
 (rum/defc ^:large-vars/cleanup-todo pdf-toolbar
-  [^js viewer]
+  [^js viewer {:keys [on-external-window!]}]
   (let [[area-mode?, set-area-mode!] (use-atom *area-mode?)
         [outline-visible?, set-outline-visible!] (rum/use-state false)
         [finder-visible?, set-finder-visible!] (rum/use-state false)
         [highlight-mode?, set-highlight-mode!] (use-atom *highlight-mode?)
         [settings-visible?, set-settings-visible!] (rum/use-state false)
-        *page-ref (rum/use-ref nil)
+        *page-ref         (rum/use-ref nil)
         [current-page-num, set-current-page-num!] (rum/use-state 1)
         [total-page-num, set-total-page-num!] (rum/use-state 1)
-        [viewer-theme, set-viewer-theme!] (rum/use-state (or (storage/get "ls-pdf-viewer-theme") "light"))]
+        [viewer-theme, set-viewer-theme!] (rum/use-state (or (storage/get "ls-pdf-viewer-theme") "light"))
+        group-id          (.-$groupIdentity viewer)
+        in-system-window? (.-$inSystemWindow viewer)
+        doc               (pdf-windows/resolve-own-document viewer)]
 
     ;; themes hooks
     (rum/use-effect!
      (fn []
-       (when-let [^js el (js/document.getElementById "pdf-layout-container")]
+       (when-let [^js el (some-> doc (.getElementById (str "pdf-layout-container_" group-id)))]
          (set! (. (. el -dataset) -theme) viewer-theme)
          (storage/set "ls-pdf-viewer-theme" viewer-theme)
          #(js-delete (. el -dataset) "theme")))
@@ -463,97 +470,111 @@
      [current-page-num])
 
     [:div.extensions__pdf-header
-      [:div.extensions__pdf-toolbar
-       [:div.inner
-        [:div.r.flex.buttons
-
-         ;; appearance
-         [:a.button
-          {:title    "More settings"
-           :on-click #(set-settings-visible! (not settings-visible?))}
-          (svg/adjustments 18)]
-
-         ;; selection
-         [:a.button
-          {:title    (str "Area highlight (" (if util/mac? "⌘" "Shift") ")")
-           :class    (when area-mode? "is-active")
-           :on-click #(set-area-mode! (not area-mode?))}
-          (svg/icon-area 18)]
-
-         [:a.button
-          {:title    "Highlight mode"
-           :class    (when highlight-mode? "is-active")
-           :on-click #(set-highlight-mode! (not highlight-mode?))}
-          (svg/highlighter 16)]
-
-         ;; zoom
-         [:a.button
-          {:title    "Zoom out"
-           :on-click (partial pdf-utils/zoom-out-viewer viewer)}
-          (svg/zoom-out 18)]
-
-         [:a.button
-          {:title    "Zoom in"
-           :on-click (partial pdf-utils/zoom-in-viewer viewer)}
-          (svg/zoom-in 18)]
-
-         [:a.button
-          {:title    "Outline"
-           :on-click #(set-outline-visible! (not outline-visible?))}
-          (svg/view-list 16)]
-
-         ;; search
-         [:a.button
-          {:title    "Search"
-           :on-click #(set-finder-visible! (not finder-visible?))}
-          (svg/search2 19)]
-
-         ;; annotations
-         [:a.button
-          {:title    "Annotations page"
-           :on-click #(pdf-assets/goto-annotations-page! (:pdf/current @state/state))}
-          (svg/annotations 16)]
-
-         ;; pager
-         [:div.pager.flex.items-center.ml-1
-
-          [:span.nu.flex.items-center.opacity-70
-           [:input {:ref            *page-ref
-                    :type           "number"
-                    :min            1
-                    :max            total-page-num
-                    :class          (util/classnames [{:is-long (> (util/safe-parse-int current-page-num) 999)}])
-                    :default-value  current-page-num
-                    :on-mouse-enter #(.select ^js (.-target %))
-                    :on-key-up      (fn [^js e]
-                                      (let [^js input (.-target e)
-                                            value     (util/safe-parse-int (.-value input))]
-                                        (set-current-page-num! value)
-                                        (when (and (= (.-keyCode e) 13) value (> value 0))
-                                          (->> (if (> value total-page-num) total-page-num value)
-                                               (set! (. viewer -currentPageNumber))))))}]
-           [:small "/ " total-page-num]]
-
-          [:span.ct.flex.items-center
-           [:a.button {:on-click #(. viewer previousPage)} (svg/up-narrow)]
-           [:a.button {:on-click #(. viewer nextPage)} (svg/down-narrow)]]]
-
-         [:a.button
-          {:on-click #(state/set-state! :pdf/current nil)}
-          (t :close)]]]
-
-       ;; contents outline
-       (pdf-outline-&-highlights viewer outline-visible? set-outline-visible!)
-
-       ;; finder
-       (when finder-visible?
-         (pdf-finder viewer {:hide-finder! #(set-finder-visible! false)}))
-
-       ;; settings
-       (when settings-visible?
-         (pdf-settings
-          viewer
-          viewer-theme
-          {:t              t
-           :hide-settings! #(set-settings-visible! false)
-           :select-theme!  #(set-viewer-theme! %)}))]]))
+     [:div.extensions__pdf-toolbar
+      [:div.inner
+       [:div.r.flex.buttons
+
+        ;; appearance
+        [:a.button
+         {:title    "More settings"
+          :on-click #(set-settings-visible! (not settings-visible?))}
+         (svg/adjustments 18)]
+
+        ;; selection
+        [:a.button
+         {:title    (str "Area highlight (" (if util/mac? "⌘" "Shift") ")")
+          :class    (when area-mode? "is-active")
+          :on-click #(set-area-mode! (not area-mode?))}
+         (svg/icon-area 18)]
+
+        [:a.button
+         {:title    "Highlight mode"
+          :class    (when highlight-mode? "is-active")
+          :on-click #(set-highlight-mode! (not highlight-mode?))}
+         (svg/highlighter 16)]
+
+        ;; zoom
+        [:a.button
+         {:title    "Zoom out"
+          :on-click (partial pdf-utils/zoom-out-viewer viewer)}
+         (svg/zoom-out 18)]
+
+        [:a.button
+         {:title    "Zoom in"
+          :on-click (partial pdf-utils/zoom-in-viewer viewer)}
+         (svg/zoom-in 18)]
+
+        [:a.button
+         {:title    "Outline"
+          :on-click #(set-outline-visible! (not outline-visible?))}
+         (svg/view-list 16)]
+
+        ;; search
+        [:a.button
+         {:title    "Search"
+          :on-click #(set-finder-visible! (not finder-visible?))}
+         (svg/search2 19)]
+
+        ;; annotations
+        [:a.button
+         {:title    "Annotations page"
+          :on-click #(pdf-assets/goto-annotations-page! (:pdf/current @state/state))}
+         (svg/annotations 16)]
+
+        ;; system window
+        [:a.button
+         {:title    (if in-system-window?
+                      "Open in app window"
+                      "Open in external window")
+          :on-click #(if in-system-window?
+                       (pdf-windows/exit-pdf-in-system-window! true)
+                       (on-external-window!))}
+         (ui/icon (if in-system-window?
+                    "window-minimize"
+                    "window-maximize"))]
+
+        ;; pager
+        [:div.pager.flex.items-center.ml-1
+
+         [:span.nu.flex.items-center.opacity-70
+          [:input {:ref            *page-ref
+                   :type           "number"
+                   :min            1
+                   :max            total-page-num
+                   :class          (util/classnames [{:is-long (> (util/safe-parse-int current-page-num) 999)}])
+                   :default-value  current-page-num
+                   :on-mouse-enter #(.select ^js (.-target %))
+                   :on-key-up      (fn [^js e]
+                                     (let [^js input (.-target e)
+                                           value     (util/safe-parse-int (.-value input))]
+                                       (set-current-page-num! value)
+                                       (when (and (= (.-keyCode e) 13) value (> value 0))
+                                         (->> (if (> value total-page-num) total-page-num value)
+                                              (set! (. viewer -currentPageNumber))))))}]
+          [:small "/ " total-page-num]]
+
+         [:span.ct.flex.items-center
+          [:a.button {:on-click #(. viewer previousPage)} (svg/up-narrow)]
+          [:a.button {:on-click #(. viewer nextPage)} (svg/down-narrow)]]]
+
+        [:a.button
+         {:on-click #(if in-system-window?
+                       (pdf-windows/exit-pdf-in-system-window! false)
+                       (state/set-current-pdf! nil))}
+         (t :close)]]]
+
+      ;; contents outline
+      (pdf-outline-&-highlights viewer outline-visible? set-outline-visible!)
+
+      ;; finder
+      (when finder-visible?
+        (pdf-finder viewer {:hide-finder! #(set-finder-visible! false)}))
+
+      ;; settings
+      (when settings-visible?
+        (pdf-settings
+         viewer
+         viewer-theme
+         {:t              t
+          :hide-settings! #(set-settings-visible! false)
+          :select-theme!  #(set-viewer-theme! %)}))]]))

+ 2 - 2
src/main/frontend/extensions/pdf/utils.cljs

@@ -192,13 +192,13 @@
 (defn next-page
   []
   (try
-    (js-invoke js/window.lsPdfViewer "nextPage")
+    (js-invoke js/window.lsActivePdfViewer "nextPage")
     (catch :default _e nil)))
 
 (defn prev-page
   []
   (try
-    (js-invoke js/window.lsPdfViewer "previousPage")
+    (js-invoke js/window.lsActivePdfViewer "previousPage")
     (catch :default _e nil)))
 
 (defn open-finder

+ 11 - 0
src/main/frontend/extensions/pdf/utils.js

@@ -117,6 +117,17 @@ export const scrollToHighlight = (viewer, highlight) => {
     ],
     ignoreDestinationZoom: true
   })
+
+  setTimeout(blinkHighlight, 200)
+
+  // blink highlight
+  function blinkHighlight () {
+    const id = highlight?.id
+    const el = document.getElementById(`hl_${id}`)
+    if (!el) return
+    el.classList.add('hl-flash')
+    setTimeout(() => el?.classList.remove('hl-flash'), 1200)
+  }
 }
 
 export const optimizeClientRects = (clientRects) => {

+ 113 - 0
src/main/frontend/extensions/pdf/windows.cljs

@@ -0,0 +1,113 @@
+(ns frontend.extensions.pdf.windows
+  (:require [frontend.state :as state]
+            [rum.core :as rum]
+            [frontend.storage :as storage]))
+
+(def *active-win (atom nil))
+(def *exit-pending? (atom false))
+
+(defn resolve-styles!
+  [^js doc]
+  (doseq [r ["./css/style.css"]]
+    (let [^js link (js/document.createElement "link")]
+      (set! (.-rel link) "stylesheet")
+      (set! (.-href link) r)
+      (.appendChild (.-head doc) link))))
+
+(defn resolve-own-document
+  [^js viewer]
+  (some-> viewer (.-viewer) (.-ownerDocument)))
+
+(defn resolve-own-container
+  [^js viewer]
+  (some-> (resolve-own-document viewer)
+          (.querySelector "body")))
+
+(defn resolve-own-window
+  [^js viewer]
+  (some-> (resolve-own-document viewer)
+          (.-defaultView)))
+
+(defn check-viewer-in-system-win?
+  [^js viewer]
+  (some-> viewer (.-$inSystemWindow)))
+
+;(defn get-base-root
+;  [^js el]
+;  (when-let [^js doc (and el (.-ownerDocument el))]
+;    (when-let [base-uri (.-baseURI doc)]
+;      (try
+;        (let [^js url   (js/URL. base-uri)
+;              hash-str  (.-hash url)
+;              base-root (string/replace base-uri hash-str "")
+;              base-root (subs base-root 0 (string/last-index-of base-root "/"))]
+;          base-root)
+;        (catch js/Error e
+;          (js/console.error e))))))
+
+(defn resolve-classes!
+  [^js doc]
+  (let [^js html (.-documentElement doc)]
+    (doto (.-classList html)
+      (.add "is-system-window"))))
+
+(defn close-pdf-in-new-window!
+  ([] (close-pdf-in-new-window! true))
+  ([reset-current?]
+   (when (and reset-current? (not @*exit-pending?))
+     (state/set-state! :pdf/current nil))
+   (state/set-state! :pdf/system-win? false)
+   (reset! *active-win nil)
+   (reset! *exit-pending? false)))
+
+(defn exit-pdf-in-system-window!
+  ([] (exit-pdf-in-system-window! true))
+  ([restore?]
+   (when-let [^js win @*active-win]
+     (when restore? (reset! *exit-pending? true))
+     (.close win))))
+
+(defn open-pdf-in-new-window!
+  [pdf-playground pdf-current]
+  (when pdf-current
+    (let [setup-win!
+          (fn []
+            (let [layouts (storage/get :ls-pdf-system-win-layout)
+                  layouts (if (and (map? layouts)
+                                   (contains? layouts :width)
+                                   (contains? layouts :height))
+                            (reduce (fn [a [k v]] (str a (name k) "=" v ",")) "" layouts)
+                            "width=700,height=800")]
+              (when-let [^js win (and (:key pdf-current)
+                                      (js/window.open "about:blank" "_blank" layouts))]
+                (let [^js doc    (.-document win)
+                      ^js doc-el (.-documentElement doc)
+                      ^js base   (js/document.createElement "base")
+                      ^js main   (js/document.createElement "main")
+                      theme-mode (:ui/theme @state/state)]
+                  (set! (.-href base) js/location.href)
+                  (.appendChild (.-head doc) base)
+                  (set! (.-title doc) (or (:filename pdf-current) "Logseq"))
+                  (set! (.-dataset doc-el) -theme (str theme-mode))
+                  (resolve-classes! doc)
+                  (resolve-styles! doc)
+                  (.appendChild (.-body doc) main)
+                  (rum/mount (pdf-playground pdf-current) main)
+
+                  ;; events
+                  (.addEventListener win "beforeunload" #(close-pdf-in-new-window!))
+                  (.addEventListener win "resize" #(storage/set :ls-pdf-system-win-layout
+                                                                {:height (.-clientHeight doc-el)
+                                                                 :width  (.-clientWidth doc-el)
+                                                                 :x      (.-screenX win)
+                                                                 :y      (.-screenY win)})))
+
+                (reset! *active-win win)
+                (state/set-state! :pdf/system-win? true))))]
+
+      (js/setTimeout
+       (fn []
+         (if-let [win @*active-win]
+           (.focus win)
+           (setup-win!)))
+       16))))

+ 1 - 0
src/main/frontend/state.cljs

@@ -207,6 +207,7 @@
      :plugin/focused-settings               nil ;; plugin id
 
      ;; pdf
+     :pdf/system-win?                       false
      :pdf/current                           nil
      :pdf/ref-highlight                     nil
      :pdf/block-highlight-colored?          (or (storage/get "ls-pdf-hl-block-is-colored") true)

+ 7 - 1
src/main/frontend/util.cljc

@@ -777,7 +777,13 @@
      ([s]
       (utils/writeClipboard (clj->js {:text s})))
      ([s html]
-      (utils/writeClipboard (clj->js {:text s :html html})))))
+      (utils/writeClipboard (clj->js {:text s :html html})))
+     ([s html owner-window]
+      (-> (cond-> {:text s}
+            (not (string/blank? html))
+            (assoc :html html))
+          (bean/->js)
+          (utils/writeClipboard owner-window)))))
 
 (defn drop-nth [n coll]
   (keep-indexed #(when (not= %1 n) %2) coll))

+ 4 - 1
src/main/frontend/utils.js

@@ -245,11 +245,14 @@ export const getClipText = (cb, errorHandler) => {
   })
 }
 
-export const writeClipboard = ({text, html}) => {
+export const writeClipboard = ({text, html}, ownerWindow) => {
     if (Capacitor.isNativePlatform()) {
         CapacitorClipboard.write({ string: text });
         return
     }
+
+    const navigator = (ownerWindow || window).navigator
+
     navigator.permissions.query({
         name: "clipboard-write"
     }).then((result) => {