Browse Source

feat: multiple windows support

Tienson Qin 4 years ago
parent
commit
6fb7121397

+ 9 - 71
src/electron/electron/core.cljs

@@ -12,10 +12,10 @@
             ["path" :as path]
             ["os" :as os]
             ["electron" :refer [BrowserWindow app protocol ipcMain dialog Menu MenuItem session] :as electron]
-            ["electron-window-state" :as windowStateKeeper]
             [clojure.core.async :as async]
             [electron.state :as state]
             [electron.git :as git]
+            [electron.window :as win]
             ["/electron/utils" :as utils]))
 
 (defonce LSP_SCHEME "lsp")
@@ -25,10 +25,6 @@
 (defonce PLUGINS_ROOT (.join path (.homedir os) ".logseq/plugins"))
 
 (def ROOT_PATH (path/join js/__dirname ".."))
-(def MAIN_WINDOW_ENTRY (if dev?
-                         ;;"http://localhost:3001"
-                         (str "file://" (path/join js/__dirname "index.html"))
-                         (str "file://" (path/join js/__dirname "electron.html"))))
 
 (defonce *setup-fn (volatile! nil))
 (defonce *teardown-fn (volatile! nil))
@@ -37,53 +33,6 @@
 ;; Handle creating/removing shortcuts on Windows when installing/uninstalling.
 (when (js/require "electron-squirrel-startup") (.quit app))
 
-(defn create-main-window
-  "Creates main app window"
-  []
-  (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
-                                           :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")))
-        url MAIN_WINDOW_ENTRY
-        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)
-              requestHeaders (.-requestHeaders details)]
-          (if (and
-                (.hasOwnProperty requestHeaders "referer")
-                (not-empty (.-referer requestHeaders)))
-              (callback #js {:cancel false
-                             :requestHeaders requestHeaders})
-              (do
-                (set! (.-referer requestHeaders) origin)
-                (callback #js {:cancel false
-                               :requestHeaders requestHeaders}))))))
-    (.loadURL win url)
-    ;;(when dev? (.. win -webContents (openDevTools)))
-    win))
-
 (defn setup-updater! [^js win]
   ;; manual/auto updater
   (when-not linux?
@@ -257,10 +206,6 @@
          (.removeHandler ipcMain call-app-channel)
          (.removeHandler ipcMain call-win-channel))))
 
-(defn- destroy-window!
-  [^js win]
-  (.destroy win))
-
 (defn main
   []
   (if-not (.requestSingleInstanceLock app)
@@ -269,18 +214,11 @@
       (.quit app))
     (do
       (.registerSchemesAsPrivileged
-        protocol (bean/->js [{:scheme     LSP_SCHEME
-                              :privileges {:standard        true
-                                           :secure          true
-                                           :bypassCSP       true
-                                           :supportFetchAPI true}}]))
-      (.on app "second-instance"
-           (fn [_event _commandLine _workingDirectory]
-             (when-let [win @*win]
-               (when (.isMinimized ^object win)
-                 (.restore win))
-               (.focus win))))
-
+       protocol (bean/->js [{:scheme     LSP_SCHEME
+                             :privileges {:standard        true
+                                          :secure          true
+                                          :bypassCSP       true
+                                          :supportFetchAPI true}}]))
       (.on app "window-all-closed" (fn []
                                      (try
                                        (fs-watcher/close-watcher!)
@@ -291,7 +229,7 @@
       (.on app "ready"
            (fn []
              (let [t0 (setup-interceptor!)
-                   ^js win (create-main-window)
+                   ^js win (win/create-main-window)
                    _ (reset! *win win)
                    *quitting? (atom false)]
                (.. logger (info (str "Logseq App(" (.getVersion app) ") Starting... ")))
@@ -330,14 +268,14 @@
                                       (let [_ (async/<! state/persistent-dbs-chan)]
                                         (if (or @*quitting? (not mac?))
                                           (when-let [win @*win]
-                                            (destroy-window! win)
+                                            (win/destroy-window! win)
                                             (reset! *win nil))
                                           (do (.preventDefault ^js/Event e)
                                               (if (and mac? (.isFullScreen win))
                                                 (do (.once win "leave-full-screen" #(.hide win))
                                                     (.setFullScreen win false))
                                                 (.hide win)))))))))
-               (.on app "before-quit" (fn [_e] (reset! *quitting? true)))
+
                (.on app "activate" #(if @*win (.show win)))))))))
 
 (defn start []

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

@@ -5,6 +5,7 @@
             ["buffer" :as buffer]
             ["fs-extra" :as fs-extra]
             ["path" :as path]
+            ["os" :as os]
             [electron.fs-watcher :as watcher]
             [electron.configs :as cfgs]
             [promesa.core :as p]
@@ -15,7 +16,8 @@
             [clojure.core.async :as async]
             [electron.search :as search]
             [electron.git :as git]
-            [electron.plugin :as plugin]))
+            [electron.plugin :as plugin]
+            [electron.window :as win]))
 
 (defmulti handle (fn [_window args] (keyword (first args))))
 
@@ -153,6 +155,66 @@
 (defmethod handle :getFiles [window [_ path]]
   (get-files path))
 
+(defn- sanitize-graph-name
+  [graph-name]
+  (when graph-name
+    (string/replace graph-name "/" "++")))
+
+(defn- graph-name->path
+  [graph-name]
+  (when graph-name
+    (string/replace graph-name "++" "/")))
+
+(defn- get-graphs-dir
+  []
+  (let [dir (.join path (.homedir os) ".logseq" "graphs")]
+    (fs-extra/ensureDirSync dir)
+    dir))
+
+(defn- get-graphs
+  []
+  (let [dir (get-graphs-dir)
+        graphs-path (.join path (.homedir os) ".logseq" "graphs")]
+    (->> (readdir dir)
+         (remove #{graphs-path})
+         (map #(path/basename % ".transit"))
+         (map graph-name->path))))
+
+(defmethod handle :getGraphs [window [_]]
+  (get-graphs))
+
+(defn- get-graph-path
+  [graph-name]
+  (when graph-name
+    (let [graph-name (sanitize-graph-name graph-name)
+          dir (get-graphs-dir)]
+      (.join path dir (str graph-name ".transit")))))
+
+(defn- get-serialized-graph
+  [graph-name]
+  (when graph-name
+    (when-let [file-path (get-graph-path graph-name)]
+      (utils/read-file file-path))))
+
+(defmethod handle :getSerializedGraph [window [_ graph-name]]
+  (get-serialized-graph graph-name))
+
+(defmethod handle :saveGraph [window [_ graph-name value-str]]
+  (when (and graph-name value-str)
+    (when-let [file-path (get-graph-path graph-name)]
+      (fs/writeFileSync file-path value-str))))
+
+(defmethod handle :deleteGraph [window [_ graph-name]]
+  (when graph-name
+    (when-let [file-path (get-graph-path graph-name)]
+      (when (fs/existsSync file-path)
+        (fs-extra/removeSync file-path)))))
+
+(defmethod handle :openNewWindow [window [_]]
+  (let [win (win/create-main-window)]
+    (win/on-close-save! win)
+    nil))
+
 (defmethod handle :persistent-dbs-saved [window _]
   (async/put! state/persistent-dbs-chan true)
   true)
@@ -182,6 +244,9 @@
 
 (defn clear-cache!
   []
+  (let [graphs-dir (get-graphs-dir)]
+    (fs-extra/removeSync graphs-dir))
+
   (let [path (.getPath ^object app "userData")]
     (doseq [dir ["search" "IndexedDB"]]
       (let [path (path/join path dir)]

+ 74 - 0
src/electron/electron/window.cljs

@@ -0,0 +1,74 @@
+(ns electron.window
+  (:require ["electron-window-state" :as windowStateKeeper]
+            [electron.utils :refer [*win mac? win32? linux? prod? dev? logger open]]
+            [electron.configs :as cfgs]
+            ["electron" :refer [BrowserWindow app protocol ipcMain dialog Menu MenuItem session] :as electron]
+            ["path" :as path]
+            [electron.state :as state]
+            [clojure.core.async :as async]))
+
+(def MAIN_WINDOW_ENTRY (if dev?
+                         ;;"http://localhost:3001"
+                         (str "file://" (path/join js/__dirname "index.html"))
+                         (str "file://" (path/join js/__dirname "electron.html"))))
+
+(defn create-main-window
+  ([]
+   (create-main-window MAIN_WINDOW_ENTRY))
+  ([url]
+   (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
+                      :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))]
+     (.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)
+                                   requestHeaders (.-requestHeaders details)]
+                               (if (and
+                                    (.hasOwnProperty requestHeaders "referer")
+                                    (not-empty (.-referer requestHeaders)))
+                                 (callback #js {:cancel false
+                                                :requestHeaders requestHeaders})
+                                 (do
+                                   (set! (.-referer requestHeaders) origin)
+                                   (callback #js {:cancel false
+                                                  :requestHeaders requestHeaders}))))))
+     (.loadURL win url)
+     ;;(when dev? (.. win -webContents (openDevTools)))
+     win)))
+
+(defn destroy-window!
+  [^js win]
+  (.destroy win))
+
+(defn on-close-save!
+  [^js win]
+  (.on win "close" (fn [e]
+                     (.preventDefault e)
+                     (let [web-contents (. win -webContents)]
+                       (.send web-contents "persistent-dbs"))
+                     (async/go
+                       (let [_ (async/<! state/persistent-dbs-chan)]
+                         (destroy-window! win))))))

+ 3 - 2
src/main/electron/listener.cljs

@@ -9,12 +9,13 @@
             [electron.ipc :as ipc]
             [frontend.handler.notification :as notification]
             [frontend.handler.metadata :as metadata-handler]
-            [frontend.ui :as ui]))
+            [frontend.ui :as ui]
+            [frontend.db.persist :as db-persist]))
 
 (defn persist-dbs!
   []
   (->
-   (p/let [repos (idb/get-nfs-dbs)
+   (p/let [repos (db-persist/get-all-graphs)
            repos (-> repos
                      (conj (state/get-current-repo))
                      (distinct))]

+ 4 - 1
src/main/frontend/components/repo.cljs

@@ -13,6 +13,7 @@
             [frontend.handler.page :as page-handler]
             [frontend.handler.repo :as repo-handler]
             [frontend.handler.route :as route-handler]
+            [frontend.handler.ui :as ui-handler]
             [frontend.handler.web.nfs :as nfs-handler]
             [frontend.modules.shortcut.core :as shortcut]
             [frontend.state :as state]
@@ -260,7 +261,9 @@
                                                         (state/close-modal!)
                                                         (repo-handler/re-index!
                                                          nfs-handler/rebuild-index!
-                                                         page-handler/create-today-journal!)))]]))}}])]
+                                                         page-handler/create-today-journal!)))]]))}}
+                           {:title        (t :open-new-window)
+                            :options {:on-click ui-handler/open-new-window!}}])]
         (when (seq repos)
           (ui/dropdown-with-links
            (fn [{:keys [toggle-fn]}]

+ 3 - 3
src/main/frontend/db.cljs

@@ -8,6 +8,7 @@
             [frontend.db.query-custom]
             [frontend.db.query-react]
             [frontend.db.react]
+            [frontend.db.persist :as db-persist]
             [frontend.idb :as idb]
             [frontend.namespaces :refer [import-vars]]
             [frontend.state :as state]
@@ -20,7 +21,6 @@
   conns
   get-repo-path
   datascript-db
-  remove-db!
   get-conn
   me-tx
   remove-conn!]
@@ -72,7 +72,7 @@
     (when conn
       (let [db (d/db conn)
             db-str (if db (db->string db) "")]
-        (p/let [_ (idb/set-batch! [{:key key :value db-str}])]
+        (p/let [_ (db-persist/save-graph! key db-str)]
           (state/set-last-persist-transact-id! repo false (get-max-tx-id db)))))))
 
 (defonce persistent-jobs (atom {}))
@@ -142,7 +142,7 @@
                  db-conn (d/create-conn db-schema/schema)
                  _ (d/transact! db-conn [{:schema/version db-schema/version}])
                  _ (swap! conns assoc db-name db-conn)
-                 stored (idb/get-item db-name)
+                 stored (db-persist/get-serialized-graph db-name)
                  _ (if stored
                      (let [stored-db (string->db stored)
                            attached-db (d/db-with stored-db (concat

+ 3 - 5
src/main/frontend/db/conn.cljs

@@ -21,11 +21,9 @@
 (defn datascript-db
   [repo]
   (when repo
-    (str config/idb-db-prefix (get-repo-path repo))))
-
-(defn remove-db!
-  [repo]
-  (idb/remove-item! (datascript-db repo)))
+    (let [path (get-repo-path repo)]
+      (str (if (util/electron?) "" config/idb-db-prefix)
+           path))))
 
 (defn get-conn
   ([]

+ 33 - 0
src/main/frontend/db/persist.cljs

@@ -0,0 +1,33 @@
+(ns frontend.db.persist
+  (:require [frontend.util :as util]
+            [frontend.mobile.util :as mobile]
+            [frontend.idb :as idb]
+            [electron.ipc :as ipc]
+            [frontend.db.conn :as db-conn]
+            [promesa.core :as p]))
+
+(defn get-all-graphs
+  []
+  (if (util/electron?)
+    (p/let [result (ipc/ipc "getGraphs")]
+      (vec result))
+    (idb/get-nfs-dbs)))
+
+(defn get-serialized-graph
+  [graph-name]
+  (if (util/electron?)
+    (ipc/ipc "getSerializedGraph" graph-name)
+    (idb/get-item graph-name)))
+
+(defn save-graph!
+  [key value]
+  (if (util/electron?)
+    (ipc/ipc "saveGraph" key value)
+    (idb/set-batch! [{:key key :value value}])))
+
+(defn delete-graph!
+  [graph]
+  (let [key (db-conn/datascript-db graph)]
+    (if (util/electron?)
+     (ipc/ipc "deleteGraph" key)
+     (idb/remove-item! key))))

+ 2 - 0
src/main/frontend/dicts.cljs

@@ -278,6 +278,7 @@
         :delete "Delete"
         :re-index "Re-index"
         :re-index-detail "Rebuild the graph"
+        :open-new-window "Open new window"
         :sync-from-local-files "Refresh"
         :sync-from-local-files-detail "Import changes from local files"
         :unlink "unlink"
@@ -1054,6 +1055,7 @@
            :cancel "取消"
            :new-graph "添加图谱"
            :re-index "重新建立索引"
+           :open-new-window "打开新窗口"
            :sync-from-local-files "刷新(读取本地最新文件)"
            :export-graph "导出图谱"
            :export-page "导出当前页面"

+ 6 - 27
src/main/frontend/handler.cljs

@@ -33,7 +33,8 @@
             [cljs.reader :refer [read-string]]
             [goog.object :as gobj]
             [lambdaisland.glogi :as log]
-            [promesa.core :as p]))
+            [promesa.core :as p]
+            [frontend.db.persist :as db-persist]))
 
 (defn set-global-error-notification!
   []
@@ -169,7 +170,7 @@
   []
   (let [logged? (state/logged?)
         me (state/get-me)]
-    (p/let [nfs-dbs (idb/get-nfs-dbs)
+    (p/let [nfs-dbs (db-persist/get-all-graphs)
             nfs-dbs (map (fn [db]
                            {:url db :nfs? true}) nfs-dbs)]
       (cond
@@ -193,9 +194,9 @@
 
 (defn clear-cache!
   []
-  (p/let [_ (idb/clear-local-storage-and-idb!)
-          _ (when (util/electron?)
-              (ipc/ipc "clearCache"))]
+  (p/let [_ (when (util/electron?)
+              (ipc/ipc "clearCache"))
+          _ (idb/clear-local-storage-and-idb!)]
     (js/setTimeout #(js/window.location.reload %) 3000)))
 
 (defn- register-components-fns!
@@ -244,30 +245,8 @@
 (defn stop! []
   (prn "stop!"))
 
-(defonce triggered? (atom false))
-(when (util/electron?)
-  (.addEventListener js/window "beforeunload"
-                     (fn [e]
-                       (when-not @triggered?
-                         (.preventDefault e)
-                         (state/pub-event! [:modal/show
-                                            [:div
-                                             [:p "Reload Logseq?"]
-                                             (ui/button
-                                              "Yes"
-                                              :autoFocus "on"
-                                              :large? true
-                                              :on-click (fn []
-                                                          (pool/terminate-parser-pool!)
-                                                          (p/let [_ (el/persist-dbs!)]
-                                                            (reset! triggered? true)
-                                                            (js/window.location.reload))))]])
-                         (reset! triggered? false)
-                         (set! (.-returnValue e) "")))))
-
 (defn quit-and-install-new-version!
   []
   (p/let [_ (el/persist-dbs!)
-          _ (reset! triggered? true)
           _ (ipc/invoke "set-quit-dirty-state" false)]
     (ipc/ipc :quitAndInstall)))

+ 4 - 3
src/main/frontend/handler/repo.cljs

@@ -29,7 +29,8 @@
             [promesa.core :as p]
             [shadow.resource :as rc]
             [clojure.set :as set]
-            [frontend.mobile.util :as mobile]))
+            [frontend.mobile.util :as mobile]
+            [frontend.db.persist :as db-persist]))
 
 ;; Project settings should be checked in two situations:
 ;; 1. User changes the config.edn directly in logseq.com (fn: alter-file)
@@ -537,7 +538,7 @@
   ;; (spec/validate :repos/repo repo)
   (let [delete-db-f (fn []
                       (db/remove-conn! url)
-                      (db/remove-db! url)
+                      (db-persist/delete-graph! url)
                       (search/remove-db! url)
                       (state/delete-repo! repo))]
     (if (or (config/local-db? url) (= url "local"))
@@ -640,7 +641,7 @@
     (search/reset-indice! url)
     (db/remove-conn! url)
     (db/clear-query-state!)
-    (-> (p/do! (db/remove-db! url)
+    (-> (p/do! (db-persist/delete-graph! url)
                (clone-and-load-db url))
         (p/catch (fn [error]
                    (prn "Delete repo failed, error: " error))))))

+ 6 - 1
src/main/frontend/handler/ui.cljs

@@ -14,7 +14,8 @@
             [clojure.string :as string]
             [rum.core :as rum]
             [clojure.edn :as edn]
-            [frontend.mobile.util :as mobile]))
+            [frontend.mobile.util :as mobile]
+            [electron.ipc :as ipc]))
 
 ;; sidebars
 (defn close-left-sidebar!
@@ -278,3 +279,7 @@
   (if (:modal/show? @state/state)
     (state/close-modal!)
     (state/pub-event! [:modal/show-cards])))
+
+(defn open-new-window!
+  []
+  (ipc/ipc "openNewWindow"))

+ 1 - 3
src/main/frontend/handler/web/nfs.cljs

@@ -285,9 +285,7 @@
        (when re-index?
          (state/set-graph-syncing? true))
        (->
-        (p/let [handle (-> (idb/get-item handle-path)
-                           (p/catch (fn [_error]
-                                      nil)))]
+        (p/let [handle (when-not electron? (idb/get-item handle-path))]
           (when (or handle electron? mobile-native?)   ; electron doesn't store the file handle
             (p/let [_ (when handle (nfs/verify-permission repo handle true))
                     files-result (fs/get-files (if nfs? handle

+ 31 - 12
src/main/frontend/idbkv.js

@@ -2,6 +2,25 @@
 
 Object.defineProperty(exports, '__esModule', { value: true });
 
+function isElectron() {
+  // Renderer process
+  if (typeof window !== 'undefined' && typeof window.process === 'object' && window.process.type === 'renderer') {
+    return true;
+  }
+
+  // Main process
+  if (typeof process !== 'undefined' && typeof process.versions === 'object' && !!process.versions.electron) {
+    return true;
+  }
+
+  // Detect the user agent when the `nodeIntegration` option is set to true
+  if (typeof navigator === 'object' && typeof navigator.userAgent === 'string' && navigator.userAgent.indexOf('Electron') >= 0) {
+    return true;
+  }
+
+  return false;
+}
+
 class Store {
     constructor(dbName = 'keyval-store', storeName = 'keyval', version = 1) {
         this.storeName = storeName;
@@ -12,18 +31,18 @@ class Store {
         this._init();
     }
     _init() {
-        if (this._dbp) {
-            return;
-        }
-        this._dbp = new Promise((resolve, reject) => {
-            const openreq = indexedDB.open(this._dbName, this._version);
-            openreq.onerror = () => reject(openreq.error);
-            openreq.onsuccess = () => resolve(openreq.result);
-            // First time setup: create an empty object store
-            openreq.onupgradeneeded = () => {
-                openreq.result.createObjectStore(this._storeName);
-            };
-        });
+      if (this._dbp) {
+        return;
+      }
+      this._dbp = new Promise((resolve, reject) => {
+        const openreq = indexedDB.open(this._dbName, this._version);
+        openreq.onerror = () => reject(openreq.error);
+        openreq.onsuccess = () => resolve(openreq.result);
+        // First time setup: create an empty object store
+        openreq.onupgradeneeded = () => {
+          openreq.result.createObjectStore(this._storeName);
+        };
+      });
     }
     _withIDBStore(type, callback) {
         this._init();

+ 19 - 10
src/main/frontend/modules/shortcut/config.cljs

@@ -319,9 +319,10 @@
                                     :fn      (fn [] (state/toggle! :ui/command-palette-open?))
                                     :force?   true}
 
-   :command/run                    {:desc    "Run git command"
-                                    :binding "mod+shift+1"
-                                    :fn      #(state/pub-event! [:command/run])}
+   :command/run                    (when (util/electron?)
+                                     {:desc    "Run git command"
+                                      :binding "mod+shift+1"
+                                      :fn      #(state/pub-event! [:command/run])})
 
    :go/home                        {:desc    "Go to home"
                                     :binding "g h"
@@ -386,18 +387,24 @@
    :ui/toggle-contents              {:desc    "Toggle Contents in sidebar"
                                      :binding "mod+shift+c"
                                      :fn      ui-handler/toggle-contents!}
+   :ui/open-new-window              (when (util/electron?)
+                                      {:desc    "Open another window"
+                                       :binding "mod+n"
+                                       :fn      ui-handler/open-new-window!})
 
    :command/toggle-favorite         {:desc    "Add to/remove from favorites"
                                      :binding "mod+shift+f"
                                      :fn      page-handler/toggle-favorite!}
 
-   :editor/open-file-in-default-app {:desc    "Open file in default app"
-                                     :binding nil
-                                     :fn      page-handler/open-file-in-default-app}
+   :editor/open-file-in-default-app (when (util/electron?)
+                                      {:desc    "Open file in default app"
+                                       :binding nil
+                                       :fn      page-handler/open-file-in-default-app})
 
-   :editor/open-file-in-directory   {:desc    "Open file in parent directory"
-                                     :binding nil
-                                     :fn      page-handler/open-file-in-directory}
+   :editor/open-file-in-directory   (when (util/electron?)
+                                      {:desc    "Open file in parent directory"
+                                       :binding nil
+                                       :fn      page-handler/open-file-in-directory})
 
    :ui/toggle-wide-mode             {:desc    "Toggle wide mode"
                                      :binding "t w"
@@ -547,6 +554,7 @@
                           :ui/toggle-help
                           :ui/toggle-theme
                           :ui/toggle-contents
+                          :ui/open-new-window
                           :editor/open-file-in-default-app
                           :editor/open-file-in-directory
                           :ui/toggle-wide-mode
@@ -601,7 +609,8 @@
     :go/tomorrow
     :go/next-journal
     :go/prev-journal
-    :go/keyboard-shortcuts]
+    :go/keyboard-shortcuts
+    :ui/open-new-window]
 
    :shortcut.category/block-editing
    ^{:doc "Block editing general"}

+ 22 - 21
src/main/frontend/util/drawer.cljs

@@ -92,27 +92,28 @@
 ;; TODO: DRY
 (defn remove-logbook
   [content]
-  (if (contains-logbook? content)
-    (let [lines (string/split-lines content)
-          [title-lines body] (split-with (fn [l]
-                                           (not (string/starts-with? (string/upper-case (string/triml l)) ":LOGBOOK:")))
-                                         lines)
-          body (drop-while (fn [l]
-                             (let [l' (string/lower-case (string/trim l))]
-                               (or
-                                (not (string/starts-with? l' ":end:"))
-                                (string/blank? l))))
-                           body)
-          body (if (and (seq body)
-                        (string/starts-with? (string/lower-case (string/triml (first body))) ":end:"))
-                 (let [line (string/replace (first body) #"(?i):end:\s?" "")]
-                   (if (string/blank? line)
-                     (rest body)
-                     (cons line (rest body))))
-                 body)]
-      (->> (concat title-lines body)
-           (string/join "\n")))
-    content))
+  (when content
+    (if (contains-logbook? content)
+      (let [lines (string/split-lines content)
+            [title-lines body] (split-with (fn [l]
+                                             (not (string/starts-with? (string/upper-case (string/triml l)) ":LOGBOOK:")))
+                                           lines)
+            body (drop-while (fn [l]
+                               (let [l' (string/lower-case (string/trim l))]
+                                 (or
+                                  (not (string/starts-with? l' ":end:"))
+                                  (string/blank? l))))
+                             body)
+            body (if (and (seq body)
+                          (string/starts-with? (string/lower-case (string/triml (first body))) ":end:"))
+                   (let [line (string/replace (first body) #"(?i):end:\s?" "")]
+                     (if (string/blank? line)
+                       (rest body)
+                       (cons line (rest body))))
+                   body)]
+        (->> (concat title-lines body)
+             (string/join "\n")))
+      content)))
 
 (defn get-logbook
   [body]

+ 3 - 3
src/main/logseq/api.cljs

@@ -635,12 +635,12 @@
 
 (defn ^:export force_save_graph
   []
-  (p/let [_ (el/persist-dbs!)
-          _ (reset! handler/triggered? true)]))
+  (p/let [_ (el/persist-dbs!)]
+    true))
 
 (defn ^:export __debug_state
   [path]
   (-> (if (string? path)
         (get @state/state (keyword path))
         @state/state)
-      (bean/->js)))
+      (bean/->js)))

+ 3 - 2
src/test/frontend/db/config.cljs

@@ -1,6 +1,7 @@
 (ns frontend.db.config
   (:require [frontend.db.conn :as conn]
-            [frontend.state :as state]))
+            [frontend.state :as state]
+            [frontend.db.persist :as db-persist]))
 
 (defonce test-db "test-db")
 
@@ -16,6 +17,6 @@
 
 (defn clear-current-repo []
   (let [current-repo (state/get-current-repo)]
-    (conn/remove-db! current-repo)
+    (db-persist/delete-graph! current-repo)
     (destroy-db!)
     (conn/start! nil current-repo)))