core.cljs 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349
  1. (ns electron.core
  2. (:require [electron.handler :as handler]
  3. [electron.search :as search]
  4. [electron.updater :refer [init-updater]]
  5. [electron.utils :refer [*win mac? win32? linux? prod? dev? logger open]]
  6. [electron.configs :as cfgs]
  7. [clojure.string :as string]
  8. [promesa.core :as p]
  9. [cljs-bean.core :as bean]
  10. [electron.fs-watcher :as fs-watcher]
  11. ["fs-extra" :as fs]
  12. ["path" :as path]
  13. ["os" :as os]
  14. ["electron" :refer [BrowserWindow app protocol ipcMain dialog Menu MenuItem session] :as electron]
  15. ["electron-window-state" :as windowStateKeeper]
  16. [clojure.core.async :as async]
  17. [electron.state :as state]
  18. [electron.git :as git]
  19. ["/electron/utils" :as utils]))
  20. (defonce LSP_SCHEME "lsp")
  21. (defonce LSP_PROTOCOL (str LSP_SCHEME "://"))
  22. (defonce PLUGIN_URL (str LSP_PROTOCOL "logseq.io/"))
  23. (defonce STATIC_URL (str LSP_PROTOCOL "logseq.com/"))
  24. (defonce PLUGINS_ROOT (.join path (.homedir os) ".logseq/plugins"))
  25. (def ROOT_PATH (path/join js/__dirname ".."))
  26. (def MAIN_WINDOW_ENTRY (if dev?
  27. ;;"http://localhost:3001"
  28. (str "file://" (path/join js/__dirname "index.html"))
  29. (str "file://" (path/join js/__dirname "electron.html"))))
  30. (defonce *setup-fn (volatile! nil))
  31. (defonce *teardown-fn (volatile! nil))
  32. (defonce *quit-dirty? (volatile! true))
  33. ;; Handle creating/removing shortcuts on Windows when installing/uninstalling.
  34. (when (js/require "electron-squirrel-startup") (.quit app))
  35. (defn create-main-window
  36. "Creates main app window"
  37. []
  38. (let [win-state (windowStateKeeper (clj->js {:defaultWidth 980 :defaultHeight 700}))
  39. win-opts (cond->
  40. {:width (.-width win-state)
  41. :height (.-height win-state)
  42. :frame true
  43. :titleBarStyle "hiddenInset"
  44. :trafficLightPosition {:x 16 :y 16}
  45. :autoHideMenuBar (not mac?)
  46. :webPreferences
  47. {:plugins true ; pdf
  48. :nodeIntegration false
  49. :nodeIntegrationInWorker false
  50. :webSecurity (not dev?)
  51. :contextIsolation true
  52. :spellcheck ((fnil identity true) (cfgs/get-item :spell-check))
  53. ;; Remove OverlayScrollbars and transition `.scrollbar-spacing`
  54. ;; to use `scollbar-gutter` after the feature is implemented in browsers.
  55. :enableBlinkFeatures 'OverlayScrollbars'
  56. :preload (path/join js/__dirname "js/preload.js")}}
  57. linux?
  58. (assoc :icon (path/join js/__dirname "icons/logseq.png")))
  59. url MAIN_WINDOW_ENTRY
  60. win (BrowserWindow. (clj->js win-opts))]
  61. (.manage win-state win)
  62. (.onBeforeSendHeaders (.. session -defaultSession -webRequest)
  63. (clj->js {:urls (array "*://*.youtube.com/*")})
  64. (fn [^js details callback]
  65. (let [url (.-url details)
  66. urlObj (js/URL. url)
  67. origin (.-origin urlObj)
  68. requestHeaders (.-requestHeaders details)]
  69. (if (and
  70. (.hasOwnProperty requestHeaders "referer")
  71. (not-empty (.-referer requestHeaders)))
  72. (callback #js {:cancel false
  73. :requestHeaders requestHeaders})
  74. (do
  75. (set! (.-referer requestHeaders) origin)
  76. (callback #js {:cancel false
  77. :requestHeaders requestHeaders}))))))
  78. (.loadURL win url)
  79. ;;(when dev? (.. win -webContents (openDevTools)))
  80. win))
  81. (defn setup-updater! [^js win]
  82. ;; manual/auto updater
  83. (when-not linux?
  84. (init-updater {:repo "logseq/logseq"
  85. :logger logger
  86. :win win})))
  87. (defn setup-interceptor! []
  88. (.registerFileProtocol
  89. protocol "assets"
  90. (fn [^js request callback]
  91. (let [url (.-url request)
  92. path (string/replace url "assets://" "")
  93. path (js/decodeURIComponent path)]
  94. (callback #js {:path path}))))
  95. (.registerFileProtocol
  96. protocol LSP_SCHEME
  97. (fn [^js request callback]
  98. (let [url (.-url request)
  99. url' ^js (js/URL. url)
  100. [_ ROOT] (if (string/starts-with? url PLUGIN_URL)
  101. [PLUGIN_URL PLUGINS_ROOT]
  102. [STATIC_URL js/__dirname])
  103. path' (.-pathname url')
  104. path' (js/decodeURIComponent path')
  105. path' (.join path ROOT path')]
  106. (callback #js {:path path'}))))
  107. #(do
  108. (.unregisterProtocol protocol LSP_SCHEME)
  109. (.unregisterProtocol protocol "assets")))
  110. (defn- handle-export-publish-assets [_event html custom-css-path repo-path asset-filenames]
  111. (p/let [app-path (. app getAppPath)
  112. asset-filenames (js->clj asset-filenames)
  113. result (. dialog showOpenDialog (clj->js {:properties ["openDirectory" "createDirectory" "promptToCreate", "multiSelections"]}))
  114. result (get (js->clj result) "filePaths")
  115. root-dir (first result)]
  116. (when root-dir
  117. (let [static-dir (path/join root-dir "static")
  118. assets-from-dir (path/join repo-path "assets")
  119. assets-to-dir (path/join root-dir "assets")
  120. index-html-path (path/join root-dir "index.html")]
  121. (p/let [_ (. fs ensureDir static-dir)
  122. _ (. fs ensureDir assets-to-dir)
  123. _ (p/all (concat
  124. [(. fs writeFile index-html-path html)
  125. (. fs copy (path/join app-path "404.html") (path/join root-dir "404.html"))]
  126. (map
  127. (fn [filename]
  128. (-> (. fs copy (path/join assets-from-dir filename) (path/join assets-to-dir filename))
  129. (p/catch
  130. (fn [e]
  131. (println (str "Failed to copy " (path/join assets-from-dir filename) " to " (path/join assets-to-dir filename)))
  132. (js/console.error e)))))
  133. asset-filenames)
  134. (map
  135. (fn [part]
  136. (. fs copy (path/join app-path part) (path/join static-dir part)))
  137. ["css" "fonts" "icons" "img" "js"])))
  138. custom-css (. fs readFile custom-css-path)
  139. _ (. fs writeFile (path/join static-dir "css" "custom.css") custom-css)
  140. js-files ["main.js" "code-editor.js" "excalidraw.js"]
  141. _ (p/all (map (fn [file]
  142. (. fs removeSync (path/join static-dir "js" file)))
  143. js-files))
  144. _ (p/all (map (fn [file]
  145. (. fs moveSync
  146. (path/join static-dir "js" "publishing" file)
  147. (path/join static-dir "js" file)))
  148. js-files))
  149. _ (. fs removeSync (path/join static-dir "js" "publishing"))
  150. ;; remove source map files
  151. ;; TODO: ugly, replace with ls-files and filter with ".map"
  152. _ (p/all (map (fn [file]
  153. (. fs removeSync (path/join static-dir "js" (str file ".map"))))
  154. ["main.js" "code-editor.js" "excalidraw.js" "age-encryption.js"]))]
  155. (. dialog showMessageBox (clj->js {:message (str "Export public pages and publish assets to " root-dir " successfully")})))))))
  156. (defn setup-app-manager!
  157. [^js win]
  158. (let [toggle-win-channel "toggle-max-or-min-active-win"
  159. call-app-channel "call-application"
  160. call-win-channel "call-main-win"
  161. export-publish-assets "export-publish-assets"
  162. quit-dirty-state "set-quit-dirty-state"
  163. web-contents (. win -webContents)]
  164. (doto ipcMain
  165. (.handle quit-dirty-state
  166. (fn [_ dirty?]
  167. (vreset! *quit-dirty? (boolean dirty?))))
  168. (.handle toggle-win-channel
  169. (fn [_ toggle-min?]
  170. (when-let [active-win (.getFocusedWindow BrowserWindow)]
  171. (if toggle-min?
  172. (if (.isMinimized active-win)
  173. (.restore active-win)
  174. (.minimize active-win))
  175. (if (.isMaximized active-win)
  176. (.unmaximize active-win)
  177. (.maximize active-win))))))
  178. (.handle export-publish-assets handle-export-publish-assets)
  179. (.handle call-app-channel
  180. (fn [_ type & args]
  181. (try
  182. (js-invoke app type args)
  183. (catch js/Error e
  184. (js/console.error e)))))
  185. (.handle call-win-channel
  186. (fn [_ type & args]
  187. (try
  188. (js-invoke @*win type args)
  189. (catch js/Error e
  190. (js/console.error e))))))
  191. (.on web-contents "context-menu"
  192. (fn
  193. [_event params]
  194. (let [menu (Menu.)
  195. suggestions (.-dictionarySuggestions ^js params)]
  196. (doseq [suggestion suggestions]
  197. (. menu append
  198. (MenuItem. (clj->js {:label
  199. suggestion
  200. :click
  201. (fn [] (. web-contents replaceMisspelling suggestion))}))))
  202. (when-let [misspelled-word (.-misspelledWord ^js params)]
  203. (. menu append
  204. (MenuItem. (clj->js {:label
  205. "Add to dictionary"
  206. :click
  207. (fn [] (.. web-contents -session (addWordToSpellCheckerDictionary misspelled-word)))}))))
  208. (. menu popup))))
  209. (.on web-contents "new-window"
  210. (fn [e url]
  211. (let [url (if (string/starts-with? url "file:")
  212. (js/decodeURIComponent url) url)
  213. url (if-not win32? (string/replace url "file://" "") url)]
  214. (.. logger (info "new-window" url))
  215. (if (string/includes?
  216. (.normalize path url)
  217. (.join path (. app getAppPath) "index.html"))
  218. (.info logger "pass-window" url)
  219. (open url)))
  220. (.preventDefault e)))
  221. (doto win
  222. (.on "enter-full-screen" #(.send web-contents "full-screen" "enter"))
  223. (.on "leave-full-screen" #(.send web-contents "full-screen" "leave")))
  224. #(do (.removeHandler ipcMain toggle-win-channel)
  225. (.removeHandler ipcMain export-publish-assets)
  226. (.removeHandler ipcMain quit-dirty-state)
  227. (.removeHandler ipcMain call-app-channel)
  228. (.removeHandler ipcMain call-win-channel))))
  229. (defn- destroy-window!
  230. [^js win]
  231. (.destroy win))
  232. (defn main
  233. []
  234. (if-not (.requestSingleInstanceLock app)
  235. (do
  236. (search/close!)
  237. (.quit app))
  238. (do
  239. (.registerSchemesAsPrivileged
  240. protocol (bean/->js [{:scheme LSP_SCHEME
  241. :privileges {:standard true
  242. :secure true
  243. :bypassCSP true
  244. :supportFetchAPI true}}]))
  245. (.on app "second-instance"
  246. (fn [_event _commandLine _workingDirectory]
  247. (when-let [win @*win]
  248. (when (.isMinimized ^object win)
  249. (.restore win))
  250. (.focus win))))
  251. (.on app "window-all-closed" (fn []
  252. (try
  253. (fs-watcher/close-watcher!)
  254. (search/close!)
  255. (catch js/Error e
  256. (js/console.error e)))
  257. (.quit app)))
  258. (.on app "ready"
  259. (fn []
  260. (let [t0 (setup-interceptor!)
  261. ^js win (create-main-window)
  262. _ (reset! *win win)
  263. *quitting? (atom false)]
  264. (.. logger (info (str "Logseq App(" (.getVersion app) ") Starting... ")))
  265. (utils/disableXFrameOptions win)
  266. (when (search/version-changed?)
  267. (search/rm-search-dir!))
  268. (search/ensure-search-dir!)
  269. (search/open-dbs!)
  270. (git/auto-commit-current-graph!)
  271. (vreset! *setup-fn
  272. (fn []
  273. (let [t1 (setup-updater! win)
  274. t2 (setup-app-manager! win)
  275. tt (handler/set-ipc-handler! win)]
  276. (vreset! *teardown-fn
  277. #(doseq [f [t0 t1 t2 tt]]
  278. (and f (f)))))))
  279. ;; setup effects
  280. (@*setup-fn)
  281. ;; main window events
  282. (.on win "close" (fn [e]
  283. (when @*quit-dirty?
  284. (.preventDefault e)
  285. (let [web-contents (. win -webContents)]
  286. (.send web-contents "persistent-dbs"))
  287. (async/go
  288. (let [_ (async/<! state/persistent-dbs-chan)]
  289. (if (or @*quitting? (not mac?))
  290. (when-let [win @*win]
  291. (destroy-window! win)
  292. (reset! *win nil))
  293. (do (.preventDefault ^js/Event e)
  294. (if (and mac? (.isFullScreen win))
  295. (do (.once win "leave-full-screen" #(.hide win))
  296. (.setFullScreen win false))
  297. (.hide win)))))))))
  298. (.on app "before-quit" (fn [_e] (reset! *quitting? true)))
  299. (.on app "activate" #(if @*win (.show win)))))))))
  300. (defn start []
  301. (js/console.log "Main - start")
  302. (when @*setup-fn (@*setup-fn)))
  303. (defn stop []
  304. (js/console.log "Main - stop")
  305. (when @*teardown-fn (@*teardown-fn)))