container.cljs 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929
  1. (ns frontend.components.container
  2. (:require [cljs-drag-n-drop.core :as dnd]
  3. [clojure.string :as string]
  4. [frontend.version :refer [version]]
  5. [frontend.components.find-in-page :as find-in-page]
  6. [frontend.components.header :as header]
  7. [frontend.components.journal :as journal]
  8. [frontend.components.onboarding :as onboarding]
  9. [frontend.components.plugins :as plugins]
  10. [frontend.components.repo :as repo]
  11. [frontend.components.right-sidebar :as right-sidebar]
  12. [frontend.components.select :as select]
  13. [frontend.components.theme :as theme]
  14. [frontend.components.widgets :as widgets]
  15. [frontend.components.dnd :as dnd-component]
  16. [frontend.components.icon :as icon]
  17. [frontend.components.handbooks :as handbooks]
  18. [frontend.config :as config]
  19. [frontend.context.i18n :refer [t]]
  20. [frontend.db :as db]
  21. [electron.ipc :as ipc]
  22. [frontend.db-mixins :as db-mixins]
  23. [frontend.db.model :as db-model]
  24. [frontend.extensions.pdf.utils :as pdf-utils]
  25. [frontend.storage :as storage]
  26. [frontend.extensions.srs :as srs]
  27. [frontend.handler.common :as common-handler]
  28. [frontend.handler.editor :as editor-handler]
  29. [frontend.handler.page :as page-handler]
  30. [frontend.util.page :as page-util]
  31. [frontend.handler.route :as route-handler]
  32. [frontend.handler.user :as user-handler]
  33. [frontend.handler.whiteboard :as whiteboard-handler]
  34. [frontend.handler.recent :as recent-handler]
  35. [frontend.mixins :as mixins]
  36. [frontend.mobile.action-bar :as action-bar]
  37. [frontend.mobile.footer :as footer]
  38. [frontend.mobile.mobile-bar :refer [mobile-bar]]
  39. [frontend.mobile.util :as mobile-util]
  40. [frontend.modules.shortcut.data-helper :as shortcut-dh]
  41. [frontend.modules.shortcut.utils :as shortcut-utils]
  42. [frontend.state :as state]
  43. [frontend.ui :as ui]
  44. [logseq.shui.ui :as shui]
  45. [logseq.shui.toaster.core :as shui-toaster]
  46. [logseq.shui.dialog.core :as shui-dialog]
  47. [frontend.util :as util]
  48. [frontend.util.cursor :as cursor]
  49. [frontend.components.window-controls :as window-controls]
  50. [medley.core :as medley]
  51. [goog.dom :as gdom]
  52. [goog.object :as gobj]
  53. [logseq.common.path :as path]
  54. [react-draggable]
  55. [reitit.frontend.easy :as rfe]
  56. [rum.core :as rum]))
  57. (rum/defc nav-content-item < rum/reactive
  58. [name {:keys [class count]} child]
  59. (let [collapsed? (state/sub [:ui/navigation-item-collapsed? class])]
  60. [:div.nav-content-item
  61. {:class (util/classnames [class {:is-expand (not collapsed?)
  62. :has-children (and (number? count) (> count 0))}])}
  63. [:div.nav-content-item-inner
  64. [:div.header.items-center
  65. {:on-click (fn [^js/MouseEvent _e]
  66. (state/toggle-navigation-item-collapsed! class))}
  67. [:div.a name]
  68. [:div.b (ui/icon "chevron-left" {:class "more" :size 14})]]
  69. (when child [:div.bd child])]]))
  70. (rum/defc page-name
  71. [name icon recent?]
  72. (let [original-name (db-model/get-page-original-name name)
  73. whiteboard-page? (db-model/whiteboard-page? name)
  74. untitled? (db-model/untitled-page? name)
  75. name (util/safe-page-name-sanity-lc name)
  76. file-rpath (when (util/electron?) (page-util/get-page-file-rpath name))
  77. source-page (db-model/get-alias-source-page (state/get-current-repo) name)
  78. ctx-icon #(shui/tabler-icon %1 {:class "scale-90 pr-1 opacity-80"})
  79. open-in-sidebar #(when-let [page-entity (and (not whiteboard-page?)
  80. (if (empty? source-page)
  81. (db/entity [:block/name name]) source-page))]
  82. (state/sidebar-add-block!
  83. (state/get-current-repo)
  84. (:db/id page-entity)
  85. :page))]
  86. (shui/context-menu
  87. (shui/context-menu-trigger
  88. [:a.flex.items-center
  89. {:on-click
  90. (fn [e]
  91. (let [name (if (empty? source-page) name (:block/name source-page))]
  92. (if (gobj/get e "shiftKey")
  93. (open-in-sidebar)
  94. (if whiteboard-page?
  95. (route-handler/redirect-to-whiteboard! name {:click-from-recent? recent?})
  96. (route-handler/redirect-to-page! name {:click-from-recent? recent?})))))}
  97. [:span.page-icon.ml-3.justify-center (if whiteboard-page? (ui/icon "whiteboard" {:extension? true}) icon)]
  98. [:span.page-title {:class (when untitled? "opacity-50")}
  99. (if untitled? (t :untitled)
  100. (pdf-utils/fix-local-asset-pagename original-name))]]
  101. (shui/context-menu-content
  102. {:class "w-60"}
  103. (when-not recent?
  104. (shui/context-menu-item
  105. {:on-click #(page-handler/<unfavorite-page! original-name)}
  106. (ctx-icon "star-off")
  107. (t :page/unfavorite)
  108. (shui/context-menu-shortcut (some-> (shortcut-dh/shortcut-binding :command/toggle-favorite) (first)
  109. (shortcut-utils/decorate-binding)))))
  110. (when-let [page-fpath (and (util/electron?) file-rpath
  111. (config/get-repo-fpath (state/get-current-repo) file-rpath))]
  112. [:<>
  113. (shui/context-menu-item
  114. {:on-click #(ipc/ipc :openFileInFolder page-fpath)}
  115. (ctx-icon "folder")
  116. (t :page/open-in-finder))
  117. (shui/context-menu-item
  118. {:on-click #(js/window.apis.openPath page-fpath)}
  119. (ctx-icon "file")
  120. (t :page/open-with-default-app))])
  121. (shui/context-menu-item
  122. {:on-click open-in-sidebar}
  123. (ctx-icon "layout-sidebar-right")
  124. (t :content/open-in-sidebar)
  125. (shui/context-menu-shortcut (shortcut-utils/decorate-binding "shift+click"))))))))
  126. ;; Fall back to default if icon is undefined or empty
  127. (rum/defc favorites < rum/reactive
  128. [t]
  129. (let [_favorites-updated? (state/sub :favorites/updated?)
  130. favorite-entities (page-handler/get-favorites)]
  131. (nav-content-item
  132. [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
  133. (ui/icon "star" {:size 16})
  134. [:strong.flex-1.ml-2 (string/upper-case (t :left-side-bar/nav-favorites))]]
  135. {:class "favorites"
  136. :count (count favorite-entities)
  137. :edit-fn
  138. (fn [e]
  139. (rfe/push-state :page {:name "Favorites"})
  140. (util/stop e))}
  141. (when (seq favorite-entities)
  142. (let [favorites (map
  143. (fn [e]
  144. (let [name (:block/name e)
  145. icon (icon/get-page-icon e {})]
  146. {:id (str (:db/id e))
  147. :value name
  148. :content [:li.favorite-item (page-name name icon false)]}))
  149. favorite-entities)]
  150. (dnd-component/items favorites
  151. {:on-drag-end (fn [favorites]
  152. (page-handler/<reorder-favorites! favorites))
  153. :parent-node :ul.favorites.text-sm}))))))
  154. (rum/defc recent-pages < rum/reactive db-mixins/query
  155. [t]
  156. (let [pages (recent-handler/get-recent-pages)]
  157. (nav-content-item
  158. [:a.flex.items-center.text-sm.font-medium.rounded-md.wrap-th
  159. (ui/icon "history" {:size 16})
  160. [:strong.flex-1.ml-2
  161. (string/upper-case (t :left-side-bar/nav-recent-pages))]]
  162. {:class "recent"
  163. :count (count pages)}
  164. [:ul.text-sm
  165. (for [name pages]
  166. (when-let [entity (db/entity [:block/name (util/safe-page-name-sanity-lc name)])]
  167. [:li.recent-item.select-none
  168. {:key name
  169. :title name
  170. :draggable true
  171. :on-drag-start (fn [event] (editor-handler/block->data-transfer! name event))
  172. :data-ref name}
  173. (page-name name (icon/get-page-icon entity {}) true)]))])))
  174. (rum/defcs flashcards < db-mixins/query rum/reactive
  175. {:did-mount (fn [state]
  176. (srs/update-cards-due-count!)
  177. state)}
  178. [_state srs-open?]
  179. (let [num (state/sub :srs/cards-due-count)]
  180. [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md
  181. {:class (util/classnames [{:active srs-open?}])
  182. :on-click #(do
  183. (srs/update-cards-due-count!)
  184. (state/pub-event! [:modal/show-cards]))}
  185. (ui/icon "infinity")
  186. [:span.flex-1 (t :right-side-bar/flashcards)]
  187. [:span.ml-1 (ui/render-keyboard-shortcut
  188. (ui/keyboard-shortcut-from-config :go/flashcards
  189. {:pick-first? true}))]
  190. (when (and num (not (zero? num)))
  191. [:span.ml-1.inline-block.py-0.5.px-3.text-xs.font-medium.rounded-full.fade-in num])]))
  192. (defn get-default-home-if-valid
  193. []
  194. (when-let [default-home (state/get-default-home)]
  195. (let [page (:page default-home)
  196. page (when (and (string? page)
  197. (not (string/blank? page)))
  198. (db/entity [:block/name (util/safe-page-name-sanity-lc page)]))]
  199. (if page
  200. default-home
  201. (dissoc default-home :page)))))
  202. (defn sidebar-item
  203. [{:keys [on-click-handler class title icon icon-extension? active href shortcut]}]
  204. [:div
  205. {:class class}
  206. [:a.item.group.flex.items-center.text-sm.font-medium.rounded-md
  207. {:on-click on-click-handler
  208. :class (when active "active")
  209. :href href}
  210. (ui/icon (str icon) {:extension? icon-extension?})
  211. [:span.flex-1 title]
  212. (when shortcut
  213. [:span.ml-1 (ui/render-keyboard-shortcut (ui/keyboard-shortcut-from-config shortcut))])]])
  214. (defn close-sidebar-on-mobile!
  215. []
  216. (and (util/sm-breakpoint?)
  217. (state/toggle-left-sidebar!)))
  218. (defn create-dropdown
  219. []
  220. (ui/dropdown-with-links
  221. (fn [{:keys [toggle-fn]}]
  222. [:button#create-button
  223. {:on-click toggle-fn}
  224. [:<>
  225. (ui/icon "plus" {:font? "true"})
  226. [:span.mx-1 (t :left-side-bar/create)]]])
  227. (->>
  228. [{:title (t :left-side-bar/new-page)
  229. :class "new-page-link"
  230. :options {:on-click #(do (close-sidebar-on-mobile!)
  231. (state/pub-event! [:go/search]))
  232. :shortcut (ui/keyboard-shortcut-from-config :go/search)}
  233. :icon (ui/type-icon {:name "new-page"
  234. :class "highlight"
  235. :extension? true})}
  236. {:title (t :left-side-bar/new-whiteboard)
  237. :class "new-whiteboard-link"
  238. :options {:on-click #(do (close-sidebar-on-mobile!)
  239. (whiteboard-handler/<create-new-whiteboard-and-redirect!))
  240. :shortcut (ui/keyboard-shortcut-from-config :editor/new-whiteboard)}
  241. :icon (ui/type-icon {:name "new-whiteboard"
  242. :class "highlight"
  243. :extension? true})}])
  244. {}))
  245. (rum/defc ^:large-vars/cleanup-todo sidebar-nav
  246. [route-match close-modal-fn left-sidebar-open? enable-whiteboards? srs-open?
  247. *closing? close-signal touching-x-offset]
  248. (let [[local-closing? set-local-closing?] (rum/use-state false)
  249. [el-rect set-el-rect!] (rum/use-state nil)
  250. ref-el (rum/use-ref nil)
  251. ref-open? (rum/use-ref left-sidebar-open?)
  252. default-home (get-default-home-if-valid)
  253. route-name (get-in route-match [:data :name])
  254. on-contents-scroll #(when-let [^js el (.-target %)]
  255. (let [top (.-scrollTop el)
  256. cls (.-classList el)
  257. cls' "is-scrolled"]
  258. (if (> top 2)
  259. (.add cls cls')
  260. (.remove cls cls'))))
  261. close-fn #(set-local-closing? true)
  262. touching-x-offset (when (number? touching-x-offset)
  263. (if-not left-sidebar-open?
  264. (when (> touching-x-offset 0)
  265. (min touching-x-offset (:width el-rect)))
  266. (when (< touching-x-offset 0)
  267. (max touching-x-offset (- 0 (:width el-rect))))))
  268. offset-ratio (and (number? touching-x-offset)
  269. (some->> (:width el-rect)
  270. (/ touching-x-offset)))]
  271. (rum/use-effect!
  272. #(js/setTimeout
  273. (fn [] (some-> (rum/deref ref-el)
  274. (.getBoundingClientRect)
  275. (.toJSON)
  276. (js->clj :keywordize-keys true)
  277. (set-el-rect!)))
  278. 16)
  279. [])
  280. (rum/use-layout-effect!
  281. (fn []
  282. (when (and (rum/deref ref-open?) local-closing?)
  283. (reset! *closing? true))
  284. (rum/set-ref! ref-open? left-sidebar-open?)
  285. #())
  286. [local-closing? left-sidebar-open?])
  287. (rum/use-effect!
  288. (fn []
  289. (when-not (neg? close-signal)
  290. (close-fn)))
  291. [close-signal])
  292. [:<>
  293. [:div.left-sidebar-inner.flex-1.flex.flex-col.min-h-0
  294. {:ref ref-el
  295. :style (cond-> {}
  296. (and (number? offset-ratio)
  297. (> touching-x-offset 0))
  298. (assoc :transform (str "translate3d(calc(" touching-x-offset "px - 100%), 0, 0)"))
  299. (and (number? offset-ratio)
  300. (< touching-x-offset 0))
  301. (assoc :transform (str "translate3d(" (* offset-ratio 100) "%, 0, 0)")))
  302. :on-transition-end (fn []
  303. (when local-closing?
  304. (reset! *closing? false)
  305. (set-local-closing? false)
  306. (close-modal-fn)))
  307. :on-click #(when-let [^js target (and (util/sm-breakpoint?) (.-target %))]
  308. (when (some (fn [sel] (boolean (.closest target sel)))
  309. [".favorites .bd" ".recent .bd" ".dropdown-wrapper" ".nav-header"])
  310. (close-fn)))}
  311. [:div.flex.flex-col.wrap.gap-1.relative
  312. ;; temporarily remove fake hamburger menu
  313. ;(when (mobile-util/native-platform?)
  314. ; [:div.fake-bar.absolute
  315. ; [:button
  316. ; {:on-click state/toggle-left-sidebar!}
  317. ; (ui/icon "menu-2" {:size ui/icon-size})]])
  318. [:nav.px-4.flex.flex-col.gap-1.cp__menubar-repos
  319. {:aria-label "Navigation menu"}
  320. (repo/repos-dropdown)
  321. [:div.nav-header.flex.flex-col.mt-2
  322. (let [page (:page default-home)]
  323. (if (and page (not (state/enable-journals? (state/get-current-repo))))
  324. (sidebar-item
  325. {:class "home-nav"
  326. :title page
  327. :on-click-handler route-handler/redirect-to-home!
  328. :active (and (not srs-open?)
  329. (= route-name :page)
  330. (= page (get-in route-match [:path-params :name])))
  331. :icon "home"
  332. :shortcut :go/home})
  333. (sidebar-item
  334. {:class "journals-nav"
  335. :active (and (not srs-open?)
  336. (or (= route-name :all-journals) (= route-name :home)))
  337. :title (t :left-side-bar/journals)
  338. :on-click-handler (fn [e]
  339. (if (gobj/get e "shiftKey")
  340. (route-handler/sidebar-journals!)
  341. (route-handler/go-to-journals!)))
  342. :icon "calendar"
  343. :shortcut :go/journals})))
  344. (when enable-whiteboards?
  345. (sidebar-item
  346. {:class "whiteboard"
  347. :title (t :right-side-bar/whiteboards)
  348. :href (rfe/href :whiteboards)
  349. :on-click-handler (fn [_e] (whiteboard-handler/onboarding-show))
  350. :active (and (not srs-open?) (#{:whiteboard :whiteboards} route-name))
  351. :icon "whiteboard"
  352. :icon-extension? true
  353. :shortcut :go/whiteboards}))
  354. (when (state/enable-flashcards? (state/get-current-repo))
  355. [:div.flashcards-nav
  356. (flashcards srs-open?)])
  357. (sidebar-item
  358. {:class "graph-view-nav"
  359. :title (t :right-side-bar/graph-view)
  360. :href (rfe/href :graph)
  361. :active (and (not srs-open?) (= route-name :graph))
  362. :icon "hierarchy"
  363. :shortcut :go/graph-view})
  364. (sidebar-item
  365. {:class "all-pages-nav"
  366. :title (t :right-side-bar/all-pages)
  367. :href (rfe/href :all-pages)
  368. :active (and (not srs-open?) (= route-name :all-pages))
  369. :icon "files"})]]
  370. [:div.nav-contents-container.flex.flex-col.gap-1.pt-1
  371. {:on-scroll on-contents-scroll}
  372. (favorites t)
  373. (when (not config/publishing?)
  374. (recent-pages t))]
  375. [:footer.px-2 {:class "create"}
  376. (when-not config/publishing?
  377. (if enable-whiteboards?
  378. (create-dropdown)
  379. [:a.item.group.flex.items-center.px-2.py-2.text-sm.font-medium.rounded-md.new-page-link
  380. {:on-click (fn []
  381. (and (util/sm-breakpoint?)
  382. (state/toggle-left-sidebar!))
  383. (state/pub-event! [:go/search]))}
  384. (ui/icon "circle-plus" {:style {:font-size 20}})
  385. [:span.flex-1 (t :right-side-bar/new-page)]]))]]]
  386. [:span.shade-mask
  387. (cond-> {:on-click close-fn}
  388. (number? offset-ratio)
  389. (assoc :style {:opacity (cond-> offset-ratio
  390. (neg? offset-ratio)
  391. (+ 1))}))]]))
  392. (rum/defc sidebar-resizer
  393. []
  394. (let [*el-ref (rum/use-ref nil)
  395. ^js el-doc js/document.documentElement
  396. adjust-size! (fn [width]
  397. (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width)
  398. (storage/set :ls-left-sidebar-width width))]
  399. ;; restore size
  400. (rum/use-layout-effect!
  401. (fn []
  402. (when-let [width (storage/get :ls-left-sidebar-width)]
  403. (.setProperty (.-style el-doc) "--ls-left-sidebar-width" width)))
  404. [])
  405. ;; draggable handler
  406. (rum/use-effect!
  407. (fn []
  408. (when-let [el (and (fn? js/window.interact) (rum/deref *el-ref))]
  409. (let [^js sidebar-el (.querySelector el-doc "#left-sidebar")]
  410. (-> (js/interact el)
  411. (.draggable
  412. #js {:listeners
  413. #js {:move (fn [^js/MouseEvent e]
  414. (when-let [offset (.-left (.-rect e))]
  415. (let [width (.toFixed (max (min offset 460) 240) 2)]
  416. (adjust-size! (str width "px")))))}})
  417. (.styleCursor false)
  418. (.on "dragstart" (fn []
  419. (.. sidebar-el -classList (add "is-resizing"))
  420. (.. el-doc -classList (add "is-resizing-buf"))))
  421. (.on "dragend" (fn []
  422. (.. sidebar-el -classList (remove "is-resizing"))
  423. (.. el-doc -classList (remove "is-resizing-buf"))))))
  424. #()))
  425. [])
  426. [:span.left-sidebar-resizer {:ref *el-ref}]))
  427. (rum/defcs left-sidebar < rum/reactive
  428. (rum/local false ::closing?)
  429. (rum/local -1 ::close-signal)
  430. (rum/local nil ::touch-state)
  431. [s {:keys [left-sidebar-open? route-match]}]
  432. (let [close-fn #(state/set-left-sidebar-open! false)
  433. *closing? (::closing? s)
  434. *touch-state (::touch-state s)
  435. *close-signal (::close-signal s)
  436. enable-whiteboards? (state/enable-whiteboards?)
  437. touch-point-fn (fn [^js e] (some-> (gobj/get e "touches") (aget 0) (#(hash-map :x (.-clientX %) :y (.-clientY %)))))
  438. srs-open? (= :srs (state/sub :modal/id))
  439. touching-x-offset (and (some-> @*touch-state :after)
  440. (some->> @*touch-state
  441. ((juxt :after :before))
  442. (map :x) (apply -)))
  443. touch-pending? (> (abs touching-x-offset) 20)]
  444. [:div#left-sidebar.cp__sidebar-left-layout
  445. {:class (util/classnames [{:is-open left-sidebar-open?
  446. :is-closing @*closing?
  447. :is-touching touch-pending?}])
  448. :on-touch-start
  449. (fn [^js e]
  450. (reset! *touch-state {:before (touch-point-fn e)}))
  451. :on-touch-move
  452. (fn [^js e]
  453. (when @*touch-state
  454. (some-> *touch-state (swap! assoc :after (touch-point-fn e)))))
  455. :on-touch-end
  456. (fn []
  457. (when touch-pending?
  458. (cond
  459. (and (not left-sidebar-open?) (> touching-x-offset 40))
  460. (state/set-left-sidebar-open! true)
  461. (and left-sidebar-open? (< touching-x-offset -30))
  462. (reset! *close-signal (inc @*close-signal))))
  463. (reset! *touch-state nil))}
  464. ;; sidebar contents
  465. (sidebar-nav route-match close-fn left-sidebar-open? enable-whiteboards? srs-open? *closing?
  466. @*close-signal (and touch-pending? touching-x-offset))
  467. ;; resizer
  468. (sidebar-resizer)]))
  469. (rum/defc recording-bar
  470. []
  471. [:> react-draggable
  472. {:onStart (fn [_event]
  473. (when-let [pos (some-> (state/get-input) cursor/pos)]
  474. (state/set-editor-last-pos! pos)))
  475. :onStop (fn [_event]
  476. (when-let [block (get @(get @state/state :editor/block) :block/uuid)]
  477. (editor-handler/edit-block! block :max nil)
  478. (when-let [input (state/get-input)]
  479. (when-let [saved-cursor (state/get-editor-last-pos)]
  480. (cursor/move-cursor-to input saved-cursor)))))}
  481. [:div#audio-record-toolbar
  482. {:style {:bottom (+ @util/keyboard-height 45)}}
  483. (footer/audio-record-cp)]])
  484. (rum/defc main <
  485. {:did-mount (fn [state]
  486. (when-let [element (gdom/getElement "main-content-container")]
  487. (dnd/subscribe!
  488. element
  489. :upload-files
  490. {:drop (fn [_e files]
  491. (when-let [id (state/get-edit-input-id)]
  492. (let [format (:block/format (state/get-edit-block))]
  493. (editor-handler/upload-asset id files format editor-handler/*asset-uploading? true))))})
  494. (common-handler/listen-to-scroll! element)
  495. (when (:margin-less-pages? (first (:rum/args state))) ;; makes sure full screen pages displaying without scrollbar
  496. (set! (.. element -scrollTop) 0)))
  497. state)
  498. :will-unmount (fn [state]
  499. (when-let [el (gdom/getElement "main-content-container")]
  500. (dnd/unsubscribe! el :upload-files))
  501. state)}
  502. [{:keys [route-match margin-less-pages? route-name indexeddb-support? db-restoring? main-content show-action-bar? show-recording-bar?]}]
  503. (let [left-sidebar-open? (state/sub :ui/left-sidebar-open?)
  504. onboarding-and-home? (and (or (nil? (state/get-current-repo)) (config/demo-graph?))
  505. (not config/publishing?)
  506. (= :home route-name))
  507. margin-less-pages? (or (and (mobile-util/native-platform?) onboarding-and-home?) margin-less-pages?)]
  508. [:div#main-container.cp__sidebar-main-layout.flex-1.flex
  509. {:class (util/classnames [{:is-left-sidebar-open left-sidebar-open?}])}
  510. ;; desktop left sidebar layout
  511. (left-sidebar {:left-sidebar-open? left-sidebar-open?
  512. :route-match route-match})
  513. [:div#main-content-container.scrollbar-spacing.w-full.flex.justify-center.flex-row.outline-none.relative
  514. {:tabIndex "-1"
  515. :data-is-margin-less-pages margin-less-pages?}
  516. (when show-action-bar?
  517. (action-bar/action-bar))
  518. [:div.cp__sidebar-main-content
  519. {:data-is-margin-less-pages margin-less-pages?
  520. :data-is-full-width (or margin-less-pages?
  521. (contains? #{:all-files :all-pages :my-publishing} route-name))}
  522. (when show-recording-bar?
  523. (recording-bar))
  524. (mobile-bar)
  525. (footer/footer)
  526. (when (and (not (mobile-util/native-platform?))
  527. (contains? #{:page :home} route-name))
  528. (widgets/demo-graph-alert))
  529. (cond
  530. (not indexeddb-support?)
  531. nil
  532. db-restoring?
  533. [:div.mt-20
  534. [:div.ls-center
  535. (ui/loading)]]
  536. :else
  537. [:div
  538. {:class (if (or onboarding-and-home? margin-less-pages?) "" (util/hiccup->class "mx-auto.pb-24"))
  539. :style {:margin-bottom (cond
  540. margin-less-pages? 0
  541. onboarding-and-home? 0
  542. :else 120)}}
  543. main-content])
  544. (when onboarding-and-home?
  545. (onboarding/intro onboarding-and-home?))]]]))
  546. (defonce sidebar-inited? (atom false))
  547. ;; TODO: simplify logic
  548. (rum/defc parsing-progress < rum/static
  549. [state]
  550. (let [finished (or (:finished state) 0)
  551. total (:total state)
  552. width (js/Math.round (* (.toFixed (/ finished total) 2) 100))
  553. display-filename (some-> (:current-parsing-file state)
  554. not-empty
  555. path/filename)
  556. left-label [:div.flex.flex-row.font-bold
  557. (t :parsing-files)
  558. [:div.hidden.md:flex.flex-row
  559. [:span.mr-1 ": "]
  560. [:div.text-ellipsis-wrapper {:style {:max-width 300}}
  561. display-filename]]]]
  562. (ui/progress-bar-with-label width left-label (str finished "/" total))))
  563. (rum/defc main-content < rum/reactive db-mixins/query
  564. {:init (fn [state]
  565. (when-not @sidebar-inited?
  566. (let [current-repo (state/sub :git/current-repo)
  567. default-home (get-default-home-if-valid)
  568. sidebar (:sidebar default-home)
  569. sidebar (if (string? sidebar) [sidebar] sidebar)]
  570. (when-let [pages (->> (seq sidebar)
  571. (remove string/blank?))]
  572. (doseq [page pages]
  573. (let [page (util/safe-page-name-sanity-lc page)
  574. [db-id block-type] (if (= page "contents")
  575. ["contents" :contents]
  576. [(:db/id (db/pull [:block/name page])) :page])]
  577. (state/sidebar-add-block! current-repo db-id block-type)))
  578. (reset! sidebar-inited? true))))
  579. (when (state/mobile?)
  580. (state/set-state! :mobile/show-tabbar? true))
  581. state)}
  582. []
  583. (let [default-home (get-default-home-if-valid)
  584. current-repo (state/sub :git/current-repo)
  585. loading-files? (when current-repo (state/sub [:repo/loading-files? current-repo]))
  586. journals-length (state/sub :journals-length)
  587. latest-journals (db/get-latest-journals (state/get-current-repo) journals-length)
  588. graph-parsing-state (state/sub [:graph/parsing-state current-repo])]
  589. (cond
  590. (or
  591. (:graph-loading? graph-parsing-state)
  592. (not= (:total graph-parsing-state) (:finished graph-parsing-state)))
  593. [:div.flex.items-center.justify-center.full-height-without-header
  594. [:div.flex-1
  595. (parsing-progress graph-parsing-state)]]
  596. :else
  597. [:div
  598. (cond
  599. (and default-home
  600. (= :home (state/get-current-route))
  601. (not (state/route-has-p?))
  602. (:page default-home))
  603. (route-handler/redirect-to-page! (:page default-home))
  604. (and config/publishing?
  605. (not default-home)
  606. (empty? latest-journals))
  607. (route-handler/redirect! {:to :all-pages})
  608. loading-files?
  609. (ui/loading (t :loading-files))
  610. (seq latest-journals)
  611. (journal/journals latest-journals)
  612. ;; FIXME: why will this happen?
  613. :else
  614. [:div])])))
  615. (defn- hide-context-menu-and-clear-selection
  616. [e]
  617. (state/hide-custom-context-menu!)
  618. (let [block (.closest (.-target e) ".ls-block")]
  619. (when-not (or (gobj/get e "shiftKey")
  620. (util/meta-key? e)
  621. (state/get-edit-input-id)
  622. (and block
  623. (or (= block (.-target e))
  624. (.contains block (.-target e)))))
  625. (editor-handler/clear-selection!))))
  626. (rum/defc render-custom-context-menu
  627. [links position]
  628. (let [ref (rum/use-ref nil)]
  629. (rum/use-effect!
  630. #(let [el (rum/deref ref)
  631. {:keys [x y]} (util/calc-delta-rect-offset el js/document.documentElement)]
  632. (set! (.. el -style -transform)
  633. (str "translate3d(" (if (neg? x) x 0) "px," (if (neg? y) (- y 10) 0) "px" ",0)"))))
  634. [:<>
  635. [:div.menu-backdrop {:on-mouse-down (fn [e] (hide-context-menu-and-clear-selection e))}]
  636. [:div#custom-context-menu
  637. {:ref ref
  638. :style {:z-index 999
  639. :left (str (first position) "px")
  640. :top (str (second position) "px")}} links]]))
  641. (rum/defc custom-context-menu < rum/reactive
  642. []
  643. (let [show? (state/sub :custom-context-menu/show?)
  644. links (state/sub :custom-context-menu/links)
  645. position (state/sub :custom-context-menu/position)]
  646. (when (and show? links position)
  647. (ui/css-transition
  648. {:class-names "fade"
  649. :timeout {:enter 500
  650. :exit 300}}
  651. (render-custom-context-menu links position)))))
  652. (rum/defc new-block-mode < rum/reactive
  653. []
  654. (when (state/sub [:document/mode?])
  655. (ui/tippy {:html [:div.p-2
  656. [:p.mb-2 [:b "Document mode"]]
  657. [:ul
  658. [:li
  659. [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :editor/new-line))]
  660. [:p.inline-block "to create new block"]]
  661. [:li
  662. [:p.inline-block.mr-1 "Click `D` or type"]
  663. [:div.inline-block.mr-1 (ui/render-keyboard-shortcut (shortcut-dh/gen-shortcut-seq :ui/toggle-document-mode))]
  664. [:p.inline-block "to toggle document mode"]]]]}
  665. [:a.block.px-1.text-sm.font-medium.bg-base-2.rounded-md.mx-2
  666. {:on-click state/toggle-document-mode!}
  667. "D"])))
  668. (def help-menu-items
  669. [{:title "Handbook" :icon "book-2" :on-click #(handbooks/toggle-handbooks)}
  670. {:title "Keyboard shortcuts" :icon "command" :on-click #(state/sidebar-add-block! (state/get-current-repo) "shortcut-settings" :shortcut-settings)}
  671. {:title "Documentation" :icon "help" :href "https://docs.logseq.com/"}
  672. :hr
  673. {:title "Report bug" :icon "bug" :on-click #(rfe/push-state :bug-report)}
  674. {:title "Request feature" :icon "git-pull-request" :href "https://discuss.logseq.com/c/feature-requests/"}
  675. {:title "Submit feedback" :icon "messages" :href "https://discuss.logseq.com/c/feedback/13"}
  676. :hr
  677. {:title "Ask the community" :icon "brand-discord" :href "https://discord.com/invite/KpN4eHY"}
  678. {:title "Support forum" :icon "message" :href "https://discuss.logseq.com/"}
  679. :hr
  680. {:title "Release notes" :icon "asterisk" :href "https://docs.logseq.com/#/page/changelog"}])
  681. (rum/defc help-menu-popup
  682. []
  683. (rum/use-effect!
  684. (fn []
  685. (state/set-state! :ui/handbooks-open? false))
  686. [])
  687. (rum/use-effect!
  688. (fn []
  689. (let [h #(state/set-state! :ui/help-open? false)]
  690. (.addEventListener js/document.body "click" h)
  691. #(.removeEventListener js/document.body "click" h)))
  692. [])
  693. [:div.cp__sidebar-help-menu-popup
  694. [:div.list-wrap
  695. (for [[idx {:keys [title icon href on-click] :as item}] (medley/indexed help-menu-items)]
  696. (case item
  697. :hr
  698. [:hr.my-2 {:key idx}]
  699. ;; default
  700. [:a.it.flex.items-center.px-4.py-1.select-none
  701. {:key title
  702. :on-click (fn []
  703. (cond
  704. (fn? on-click) (on-click)
  705. (string? href) (util/open-url href))
  706. (state/set-state! :ui/help-open? false))}
  707. [:span.flex.items-center.pr-2.opacity-40 (ui/icon icon {:size 20})]
  708. [:strong.font-normal title]]))]
  709. [:div.ft.pl-11.pb-3
  710. [:span.opacity.text-xs.opacity-30 "Logseq " version]]])
  711. (rum/defc help-button < rum/reactive
  712. []
  713. (let [help-open? (state/sub :ui/help-open?)
  714. handbooks-open? (state/sub :ui/handbooks-open?)]
  715. [:<>
  716. [:div.cp__sidebar-help-btn
  717. [:div.inner
  718. {:title (t :help-shortcut-title)
  719. :on-click #(state/toggle! :ui/help-open?)}
  720. [:svg.scale-125 {:stroke "currentColor", :fill "none", :stroke-linejoin "round", :width "24", :view-box "0 0 24 24", :xmlns "http://www.w3.org/2000/svg", :stroke-linecap "round", :stroke-width "2", :class "icon icon-tabler icon-tabler-help-small", :height "24"}
  721. [:path {:stroke "none", :d "M0 0h24v24H0z", :fill "none"}]
  722. [:path {:d "M12 16v.01"}]
  723. [:path {:d "M12 13a2 2 0 0 0 .914 -3.782a1.98 1.98 0 0 0 -2.414 .483"}]]]]
  724. (when help-open?
  725. (help-menu-popup))
  726. (when handbooks-open?
  727. (handbooks/handbooks-popup))]))
  728. (rum/defcs ^:large-vars/cleanup-todo sidebar <
  729. (mixins/modal :modal/show?)
  730. rum/reactive
  731. (mixins/event-mixin
  732. (fn [state]
  733. (mixins/listen state js/window "click" hide-context-menu-and-clear-selection)
  734. (mixins/listen state js/window "keydown"
  735. (fn [e]
  736. (when (= 27 (.-keyCode e))
  737. (if (and (state/modal-opened?)
  738. (not
  739. (and
  740. ;; FIXME: this does not work on CI tests
  741. util/node-test?
  742. state/*editor-editing-ref)))
  743. (state/close-modal!)
  744. (hide-context-menu-and-clear-selection e)))))))
  745. [state route-match main-content]
  746. (let [{:keys [open-fn]} state
  747. current-repo (state/sub :git/current-repo)
  748. granted? (state/sub [:nfs/user-granted? (state/get-current-repo)])
  749. theme (state/sub :ui/theme)
  750. accent-color (some-> (state/sub :ui/radix-color) (name))
  751. system-theme? (state/sub :ui/system-theme?)
  752. light? (= "light" (state/sub :ui/theme))
  753. sidebar-open? (state/sub :ui/sidebar-open?)
  754. settings-open? (state/sub :ui/settings-open?)
  755. left-sidebar-open? (state/sub :ui/left-sidebar-open?)
  756. wide-mode? (state/sub :ui/wide-mode?)
  757. ls-block-hl-colored? (state/sub :pdf/block-highlight-colored?)
  758. onboarding-state (state/sub :file-sync/onboarding-state)
  759. right-sidebar-blocks (state/sub-right-sidebar-blocks)
  760. route-name (get-in route-match [:data :name])
  761. margin-less-pages? (boolean (#{:graph :whiteboard} route-name))
  762. db-restoring? (state/sub :db/restoring?)
  763. indexeddb-support? (state/sub :indexeddb/support?)
  764. page? (= :page route-name)
  765. home? (= :home route-name)
  766. native-titlebar? (state/sub [:electron/user-cfgs :window/native-titlebar?])
  767. window-controls? (and (util/electron?) (not util/mac?) (not native-titlebar?))
  768. edit? (some? @state/*editor-editing-ref)
  769. default-home (get-default-home-if-valid)
  770. logged? (user-handler/logged-in?)
  771. fold-button-on-right? (state/enable-fold-button-right?)
  772. show-action-bar? (state/sub :mobile/show-action-bar?)
  773. show-recording-bar? (state/sub :mobile/show-recording-bar?)
  774. preferred-language (state/sub [:preferred-language])]
  775. (theme/container
  776. {:t t
  777. :theme theme
  778. :accent-color accent-color
  779. :route route-match
  780. :current-repo current-repo
  781. :edit? edit?
  782. :nfs-granted? granted?
  783. :db-restoring? db-restoring?
  784. :sidebar-open? sidebar-open?
  785. :settings-open? settings-open?
  786. :sidebar-blocks-len (count right-sidebar-blocks)
  787. :system-theme? system-theme?
  788. :onboarding-state onboarding-state
  789. :preferred-language preferred-language
  790. :on-click (fn [e]
  791. (editor-handler/unhighlight-blocks!)
  792. (util/fix-open-external-with-shift! e))}
  793. [:main.theme-inner
  794. {:class (util/classnames [{:ls-left-sidebar-open left-sidebar-open?
  795. :ls-right-sidebar-open sidebar-open?
  796. :ls-wide-mode wide-mode?
  797. :ls-window-controls window-controls?
  798. :ls-fold-button-on-right fold-button-on-right?
  799. :ls-hl-colored ls-block-hl-colored?}])}
  800. [:button#skip-to-main
  801. {:on-click #(ui/focus-element (ui/main-node))
  802. :on-key-up (fn [e]
  803. (when (= (.-key e) "Enter")
  804. (ui/focus-element (ui/main-node))))}
  805. (t :accessibility/skip-to-main-content)]
  806. [:div.#app-container
  807. [:div#left-container
  808. {:class (if (state/sub :ui/sidebar-open?) "overflow-hidden" "w-full")}
  809. (header/header {:open-fn open-fn
  810. :light? light?
  811. :current-repo current-repo
  812. :logged? logged?
  813. :page? page?
  814. :route-match route-match
  815. :default-home default-home
  816. :new-block-mode new-block-mode})
  817. (when (util/electron?)
  818. (find-in-page/search))
  819. (main {:route-match route-match
  820. :margin-less-pages? margin-less-pages?
  821. :logged? logged?
  822. :home? home?
  823. :route-name route-name
  824. :indexeddb-support? indexeddb-support?
  825. :light? light?
  826. :db-restoring? db-restoring?
  827. :main-content main-content
  828. :show-action-bar? show-action-bar?
  829. :show-recording-bar? show-recording-bar?})]
  830. (when window-controls?
  831. (window-controls/container))
  832. (right-sidebar/sidebar)
  833. [:div#app-single-container]]
  834. (ui/notification)
  835. (ui/modal)
  836. (ui/sub-modal)
  837. (shui-toaster/install-toaster)
  838. (shui-dialog/install-modals)
  839. (select/select-modal)
  840. (custom-context-menu)
  841. (plugins/custom-js-installer {:t t
  842. :current-repo current-repo
  843. :nfs-granted? granted?
  844. :db-restoring? db-restoring?})
  845. [:a#download.hidden]
  846. (when (and (not config/mobile?)
  847. (not config/publishing?))
  848. (help-button))])))