فهرست منبع

wip, zotero poc

Weihua Lu 4 سال پیش
والد
کامیت
ab21d88786
64فایلهای تغییر یافته به همراه1784 افزوده شده و 711 حذف شده
  1. 1 0
      deps.edn
  2. 10 1
      externs.js
  3. 7 6
      package.json
  4. 1 1
      postcss.config.js
  5. 2 2
      resources/css/common.css
  6. 3 0
      resources/electron-dev.html
  7. 3 0
      resources/electron.html
  8. BIN
      resources/img/screenshot.png
  9. BIN
      resources/img/tutorial-thumb.jpg
  10. 1 0
      resources/js/excalidraw-assets/vendor-3525c448906ddcdcb701.js
  11. 10 1
      shadow-cljs.edn
  12. 1 1
      src/electron/electron/core.cljs
  13. 139 57
      src/main/frontend/components/block.cljs
  14. 19 6
      src/main/frontend/components/editor.cljs
  15. 5 0
      src/main/frontend/components/editor.css
  16. 36 22
      src/main/frontend/components/header.cljs
  17. 9 49
      src/main/frontend/components/header.css
  18. 6 1
      src/main/frontend/components/hierarchy.cljs
  19. 1 1
      src/main/frontend/components/journal.cljs
  20. 10 4
      src/main/frontend/components/onboarding.cljs
  21. 216 37
      src/main/frontend/components/page.cljs
  22. 30 0
      src/main/frontend/components/page.css
  23. 3 4
      src/main/frontend/components/repo.cljs
  24. 6 26
      src/main/frontend/components/right_sidebar.cljs
  25. 24 6
      src/main/frontend/components/search.cljs
  26. 4 3
      src/main/frontend/components/sidebar.cljs
  27. 6 1
      src/main/frontend/components/sidebar.css
  28. 25 5
      src/main/frontend/components/svg.cljs
  29. 4 1
      src/main/frontend/components/theme.cljs
  30. 2 2
      src/main/frontend/db.cljs
  31. 4 1
      src/main/frontend/db/default.cljs
  32. 36 2
      src/main/frontend/db/model.cljs
  33. 2 1
      src/main/frontend/db_schema.cljs
  34. 3 1
      src/main/frontend/dicts.cljs
  35. 4 3
      src/main/frontend/error.cljs
  36. 90 0
      src/main/frontend/extensions/graph.cljs
  37. 21 0
      src/main/frontend/extensions/graph.css
  38. 126 0
      src/main/frontend/extensions/graph/pixi.cljs
  39. 0 34
      src/main/frontend/extensions/graph_2d.cljs
  40. 7 1
      src/main/frontend/format/block.cljs
  41. 3 1
      src/main/frontend/fs/nfs.cljs
  42. 0 161
      src/main/frontend/graph.cljs
  43. 0 9
      src/main/frontend/graph.css
  44. 7 3
      src/main/frontend/handler.cljs
  45. 1 1
      src/main/frontend/handler/config.cljs
  46. 1 1
      src/main/frontend/handler/editor.cljs
  47. 29 1
      src/main/frontend/handler/events.cljs
  48. 1 3
      src/main/frontend/handler/file.cljs
  49. 130 101
      src/main/frontend/handler/graph.cljs
  50. 2 4
      src/main/frontend/handler/search.cljs
  51. 11 0
      src/main/frontend/handler/ui.cljs
  52. 7 2
      src/main/frontend/modules/shortcut/config.cljs
  53. 3 2
      src/main/frontend/modules/shortcut/data_helper.cljs
  54. 46 0
      src/main/frontend/modules/zotero/api.cljs
  55. 1 0
      src/main/frontend/modules/zotero/core.cljs
  56. 5 3
      src/main/frontend/page.cljs
  57. 18 12
      src/main/frontend/rum.cljs
  58. 34 6
      src/main/frontend/state.cljs
  59. 60 19
      src/main/frontend/ui.cljs
  60. 1 1
      src/main/frontend/version.cljs
  61. 59 1
      src/workspaces/workspaces/cards.cljs
  62. 1 0
      tailwind.config.js
  63. 3 10
      templates/tutorial-en.md
  64. 484 90
      yarn.lock

+ 1 - 0
deps.edn

@@ -23,6 +23,7 @@
                                :sha "5704fbf48d3478eedcf24d458c8964b3c2fd59a9"}
   cljs-drag-n-drop/cljs-drag-n-drop
   {:mvn/version "0.1.0"}
+  cljs-http/cljs-http {:mvn/version "0.1.46"}
   borkdude/sci                {:mvn/version "0.1.1-alpha.6"}
   hickory/hickory             {:mvn/version "0.7.1"}
   hiccups/hiccups             {:mvn/version "0.3.0"}

+ 10 - 1
externs.js

@@ -65,7 +65,16 @@ dummy.transaction = function() {};
 dummy.getPath = function() {};
 dummy.getDoc = function() {};
 dummy.setValue = function() {};
-
+dummy.data = function() {};
+dummy.triangle = function() {};
+dummy.vee = function() {};
+dummy.destroy = function() {};
+dummy.changeData = function() {};
+dummy.layout = function() {};
+dummy.render = function() {};
+dummy.get = function() {};
+dummy.addItem = function() {};
+dummy.removeItem = function() {};
 
 /**
  * @typedef {{

+ 7 - 6
package.json

@@ -4,7 +4,6 @@
     "private": true,
     "main": "static/electron.js",
     "devDependencies": {
-        "@tailwindcss/jit": "^0.1.1",
         "@tailwindcss/ui": "0.7.2",
         "@types/gulp": "^4.0.7",
         "cross-env": "^7.0.3",
@@ -20,10 +19,10 @@
         "postcss-import-ext-glob": "^2.0.1",
         "postcss-nested": "5.0.5",
         "purgecss": "4.0.2",
-        "shadow-cljs": "^2.12.5",
+        "shadow-cljs": "2.12.5",
         "stylelint": "^13.8.0",
         "stylelint-config-standard": "^20.0.0",
-        "tailwindcss": "2.0.3"
+        "tailwindcss": "2.2.4"
     },
     "scripts": {
         "watch": "run-p gulp:watch cljs:watch",
@@ -45,13 +44,13 @@
         "gulp:build": "cross-env NODE_ENV=production gulp build",
         "css:build": "postcss tailwind.all.css -o static/css/style.css --verbose --env production",
         "css:watch": "postcss tailwind.all.css -o static/css/style.css --verbose --watch",
-        "cljs:watch": "clojure -M:cljs watch app publishing electron",
+        "cljs:watch": "clojure -M:cljs watch app publishing electron cards",
         "cljs:electron-watch": "clojure -M:cljs watch app electron",
         "cljs:release": "clojure -M:cljs release app publishing electron",
         "cljs:electron-release": "clojure -M:cljs release app publishing electron --config-merge '{:asset-path \"./js\"}'",
         "cljs:test": "clojure -M:test compile test",
         "cljs:run-test": "node static/tests.js",
-        "cljs:watch-app": "clojure -M:cljs watch app",
+        "cljs:watch-app": "clojure -M:cljs watch app cards",
         "cljs:release-app": "clojure -M:cljs release app",
         "cljs:release-publishing": "clojure -M:cljs release publishing",
         "cljs:dev-release-app": "clojure -M:cljs release app --config-merge '{:closure-defines {frontend.config/DEV-RELEASE true}}'",
@@ -69,12 +68,14 @@
         "codemirror": "^5.58.1",
         "cypress-clojurescript-preprocessor": "^0.1.4",
         "cypress-real-events": "^1.5.0",
+        "d3-force": "^3.0.0",
         "diff": "5.0.0",
         "diff-match-patch": "^1.0.5",
         "electron": "^13.0.0",
         "fs": "^0.0.1-security",
         "fs-extra": "^9.1.0",
         "fuse.js": "^6.4.6",
+        "graphology": "^0.20.0",
         "gulp-cached": "^1.1.1",
         "highlight.js": "^10.4.1",
         "ignore": "^5.1.8",
@@ -82,6 +83,7 @@
         "jszip": "^3.5.0",
         "mldoc": "0.8.4",
         "path": "^0.12.7",
+        "pixi-graph-fork": "^0.0.9",
         "posthog-js": "^1.10.2",
         "react": "^17.0.2",
         "react-dom": "^17.0.2",
@@ -92,7 +94,6 @@
         "react-tippy": "^1.4.0",
         "react-transition-group": "^4.3.0",
         "reakit": "^0.11.1",
-        "url": "^0.11.0",
         "yargs-parser": "^20.2.4"
     }
 }

+ 1 - 1
postcss.config.js

@@ -3,6 +3,6 @@ module.exports = (ctx) => ({
     require('postcss-nested')({}),
     require('postcss-import-ext-glob')({}),
     require('postcss-import')({}),
-    require('@tailwindcss/jit')('tailwind.config.js'),
+    require('tailwindcss')('tailwind.config.js'),
   ],
 })

+ 2 - 2
resources/css/common.css

@@ -86,7 +86,7 @@ html[data-theme='dark'] {
 
 .white-theme,
 html[data-theme='light'] {
-  --ls-primary-background-color: white;
+  --ls-primary-background-color: #f6f6f6;
   --ls-secondary-background-color: #f7f6f4;
   --ls-tertiary-background-color: #f1eee8;
   --ls-quaternary-background-color: #e8e5de;
@@ -625,7 +625,7 @@ video {
   background-color: rgba(91, 33, 182);
 }
 
-.bg-purple-900	{
+.bg-purple-900  {
   background-color: rgba(76, 29, 149);
 }
 

+ 3 - 0
resources/electron-dev.html

@@ -32,6 +32,9 @@
 <div id="root"></div>
 <script>window.__LSP__HOST__ = true</script>
 <script>window.user = null</script>
+<script>
+ window.EXCALIDRAW_ASSET_PATH = "./js/";
+</script>
 <script src="./js/magic_portal.js"></script>
 <script>let worker = new Worker('./js/worker.js')
 const portal = new MagicPortal(worker);

+ 3 - 0
resources/electron.html

@@ -32,6 +32,9 @@
 <div id="root"></div>
 <script>window.__LSP__HOST__ = true</script>
 <script>window.user = null</script>
+<script>
+ window.EXCALIDRAW_ASSET_PATH = "./js/";
+</script>
 <script src="./js/magic_portal.js"></script>
 <script>let worker = new Worker('./js/worker.js')
 const portal = new MagicPortal(worker);

BIN
resources/img/screenshot.png


BIN
resources/img/tutorial-thumb.jpg


تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 1 - 0
resources/js/excalidraw-assets/vendor-3525c448906ddcdcb701.js


+ 10 - 1
shadow-cljs.edn

@@ -6,7 +6,12 @@
  {:app
   {:target :browser
    :module-loader true
+   ;; handle `require(xxx.css)`
+   :js-options {:ignore-asset-requires true}
    :modules {:main {:init-fn frontend.core/init}
+             ;; :graph
+             ;; {:entries [frontend.extensions.graph.force]
+             ;;  :depends-on #{:main}}
              :code-editor
              {:entries [frontend.extensions.code]
               :depends-on #{:main}}
@@ -21,7 +26,7 @@
    :asset-path "/static/js"
    :release {:asset-path "https://asset.logseq.com/static/js"}
    :compiler-options {:infer-externs :auto
-                      :output-feature-set :es-next
+                      :output-feature-set :es-next-in
                       :source-map true
                       :externs ["datascript/externs.js"
                                 "externs.js"]
@@ -62,7 +67,11 @@
   :publishing
   {:target :browser
    :module-loader true
+   :js-options {:ignore-asset-requires true}
    :modules {:main {:init-fn frontend.publishing/init}
+             ;; :graph
+             ;; {:entries [frontend.extensions.graph.force]
+             ;;  :depends-on #{:main}}
              :code-editor
              {:entries [frontend.extensions.code]
               :depends-on #{:main}}

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

@@ -30,7 +30,7 @@
                    :height        (.-height win-state)
                    :frame         true
                    :titleBarStyle "hiddenInset"
-                   :trafficLightPosition {:x 15 :y 15}
+                   :trafficLightPosition {:x 16 :y 16}
                    :autoHideMenuBar (not mac?)
                    :webPreferences
                    {:plugins                 true ; pdf

+ 139 - 57
src/main/frontend/components/block.cljs

@@ -114,7 +114,7 @@
   [url]
   (match url
     ["File" s]
-    (string/replace s "file:" "")
+    (string/replace s "file://" "")
 
     ["Complex" m]
     (let [{:keys [link protocol]} m]
@@ -371,58 +371,73 @@
        :else
        (get page-entity :block/original-name page-name)))])
 
+(defn- use-delayed-open [open? page-name]
+  "A react hook to debounce open? value.
+  If open? changed from false to open, there will be a `timeout` delay.
+  Otherwise, the value will be changed to false immediately"
+  (let [[deval set-deval!] (rum/use-state nil)]
+    (rum/use-effect!
+      (fn []
+        (if open? (let [timer (js/setTimeout #(set-deval! open?) 1000)]
+                    #(js/clearTimeout timer))
+                  (set-deval! open?))) ;; immediately change
+      [open? page-name])
+    deval))
+
+(rum/defc page-preview-trigger
+  [{:keys [children sidebar? tippy-position tippy-distance fixed-position? open?] :as config} page-name]
+  (let [redirect-page-name (model/get-redirect-page-name page-name (:block/alias? config))
+        page-original-name (model/get-page-original-name redirect-page-name)
+        debounced-open? (use-delayed-open open? page-name)
+        manual? (not (nil? open?))
+        html-template (fn []
+                        [:div.tippy-wrapper.overflow-y-auto.p-4
+                         {:style {:width          600
+                                  :text-align     "left"
+                                  :font-weight    500
+                                  :max-height     600
+                                  :padding-bottom 64}}
+                         (if (and (string? page-original-name) (string/includes? page-original-name "/"))
+                           [:div.my-2
+                            (->>
+                              (for [page (string/split page-original-name #"/")]
+                                (when (and (string? page) page)
+                                  (page-reference false page {} nil)))
+                              (interpose [:span.mx-2.opacity-30 "/"]))]
+                           [:h2.font-bold.text-lg (if (= page-name redirect-page-name)
+                                                    page-original-name
+                                                    [:span
+                                                     [:span.text-sm.mr-2 "Alias:"]
+                                                     page-original-name])])
+                         (let [page (db/entity [:block/name (string/lower-case redirect-page-name)])]
+                           (editor-handler/insert-first-page-block-if-not-exists! redirect-page-name)
+                           (when-let [f (state/get-page-blocks-cp)]
+                             (f (state/get-current-repo) page {:sidebar? sidebar? :preview? true})))])]
+    (if (and manual? open?)
+      (ui/tippy {:html            html-template
+                 :interactive     true
+                 :open?           debounced-open?
+                 :delay           [1000, 100]
+                 :fixed-position? fixed-position?
+                 :position        (or tippy-position "top")
+                 :distance        (or tippy-distance 10)}
+                children)
+      children)))
+
 (rum/defc page-cp
   [{:keys [html-export? label children contents-page? sidebar? preview?] :as config} page]
   (when-let [page-name (:block/name page)]
-    (let [page (string/lower-case page-name)
-          page-entity (db/entity [:block/name page])
-          redirect-page-name (cond
-                               (:block/alias? config)
-                               page
-
-                               (db/page-empty-or-dummy? (state/get-current-repo) (:db/id page-entity))
-                               (let [source-page (model/get-alias-source-page (state/get-current-repo)
-                                                                              (string/lower-case page-name))]
-                                 (or (when source-page (:block/name source-page))
-                                     page))
-
-                               :else
-                               page)
-          page-original-name (model/get-page-original-name redirect-page-name)
+    (let [page-name (string/lower-case page-name)
+          page-entity (db/entity [:block/name page-name])
+          redirect-page-name (model/get-redirect-page-name page-name (:block/alias? config))
           href (if html-export?
-                 (util/encode-str page)
+                 (util/encode-str page-name)
                  (rfe/href :page {:name redirect-page-name}))
           inner (page-inner config
                             page-name
                             href redirect-page-name page-entity contents-page? children html-export? label)]
       (if (and (not (util/mobile?)) (not preview?))
-        (ui/tippy {:html        (fn []
-                                  [:div.tippy-wrapper.overflow-y-auto.p-4
-                                   {:style {:width          600
-                                            :text-align     "left"
-                                            :font-weight    500
-                                            :max-height     600
-                                            :padding-bottom 64}}
-                                   (if (and (string? page-original-name) (string/includes? page-original-name "/"))
-                                     [:div.my-2
-                                      (->>
-                                       (for [page (string/split page-original-name #"/")]
-                                         (when (and (string? page) page)
-                                           (page-reference false page {} nil)))
-                                       (interpose [:span.mx-2.opacity-30 "/"]))]
-                                     [:h2.font-bold.text-lg (if (= page redirect-page-name)
-                                                              page-original-name
-                                                              [:span
-                                                               [:span.text-sm.mr-2 "Alias:"]
-                                                               page-original-name])])
-
-                                   (let [page (db/entity [:block/name (string/lower-case redirect-page-name)])]
-                                     (editor-handler/insert-first-page-block-if-not-exists! redirect-page-name)
-                                     (when-let [f (state/get-page-blocks-cp)]
-                                       (f (state/get-current-repo) page {:sidebar? sidebar? :preview? true})))])
-                   :interactive true
-                   :delay       [1000, 100]}
-                  inner)
+        (page-preview-trigger (assoc config :children inner) page-name)
         inner))))
 
 (rum/defc asset-reference
@@ -494,10 +509,18 @@
 
 (declare blocks-container)
 
+(defn- edit-parent-block [e config]
+  (when-not (state/editing?)
+    (.stopPropagation e)
+    (editor-handler/edit-block! config :max (:block/format config) (:block/uuid config))))
+
 (rum/defc block-embed < rum/reactive db-mixins/query
   [config id]
   (let [blocks (db/get-block-and-children (state/get-current-repo) id)]
-    [:div.color-level.embed-block.bg-base-2 {:style {:z-index 2}}
+    [:div.color-level.embed-block.bg-base-2
+     {:style {:z-index 2}
+      :on-double-click #(edit-parent-block % config)
+      :on-mouse-down (fn [e] (.stopPropagation e))}
      [:div.px-3.pt-1.pb-2
       (blocks-container blocks (assoc config
                                       :id (str id)
@@ -510,7 +533,9 @@
   (let [page-name (string/trim (string/lower-case page-name))
         current-page (state/get-current-page)]
     [:div.color-level.embed.embed-page.bg-base-2
-     {:class (if (:sidebar? config) "in-sidebar")}
+     {:class (if (:sidebar? config) "in-sidebar")
+      :on-double-click #(edit-parent-block % config)
+      :on-mouse-down #(.stopPropagation %)}
      [:section.flex.items-center.p-1.embed-header
       [:div.mr-3 svg/page]
       (page-cp config {:block/name page-name})]
@@ -639,6 +664,29 @@
                 (not (= (:id config) "contents")))
        [:span.text-gray-500 "]]"])]))
 
+(rum/defcs tutorial-video  <
+  (rum/local true)
+  [state]
+  (let [lite-mode? (:rum/local state)]
+    [:div.tutorial-video-container.relative
+     {:style {:height 367 :width 653}}
+     (if @lite-mode?
+       [:div
+        [:img.w-full.h-full.absolute
+         {:src (if (util/electron?)
+                 "img/tutorial-thumb.jpg"
+                 "https://img.youtube.com/vi/Afmqowr0qEQ/maxresdefault.jpg")}]
+        [:button
+         {:class "absolute bg-red-300 w-16 h-16 -m-8 top-1/2 left-1/2 rounded-full"
+          :on-click (fn [_] (swap! lite-mode? not))}
+         (svg/play)]]
+       [:iframe.w-full.h-full
+        {:allow-full-screen "allowfullscreen"
+         :allow
+         "accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope"
+         :frame-border "0"
+         :src "https://www.youtube.com/embed/Afmqowr0qEQ?autoplay=1"}])]))
+
 (declare custom-query)
 
 (defn- show-link?
@@ -658,7 +706,20 @@
        (true? (boolean metadata-show))))
 
      ;; markdown
-     (string/starts-with? (string/triml full-text) "!"))))
+     (string/starts-with? (string/triml full-text) "!")
+
+     ;; image http link
+     (and (or (string/starts-with? full-text "http://")
+              (string/starts-with? full-text "https://"))
+          (text/image-link? img-formats s)))))
+
+(defn- relative-assets-path->absolute-path
+  [path]
+  (if (util/absolute-path? path)
+    path
+    (.. util/node-path
+        (join (config/get-repo-dir (state/get-current-repo))
+              (config/get-local-asset-absolute-path path)))))
 
 (defn inline
   [{:keys [html-export?] :as config} item]
@@ -770,6 +831,20 @@
                (show-link? config metadata s full_text))
           (asset-reference (second (first label)) s)
 
+          ;; open file externally if s is "../assets/<...>"
+          (and (util/electron?)
+               (config/local-asset? s))
+          (let [path (relative-assets-path->absolute-path s)]
+            (->elem
+             :a
+             (cond->
+                 {:href      (str "file://" path)
+                  :data-href path
+                  :target    "_blank"}
+               title
+               (assoc :title title))
+             (map-inline config label)))
+
           :else
           (page-reference (:html-export? config) s config label))
 
@@ -807,15 +882,19 @@
                    (page-cp config page)
                    [:span.text-gray-500 "]]"]]
 
-                  (->elem
-                   :a
-                   (cond->
-                     {:href      (str "file://" href)
-                      :data-href href
-                      :target    "_blank"}
-                     title
-                     (assoc :title title))
-                   (map-inline config label)))))
+                  (let [href*
+                        (if (util/electron?)
+                          (relative-assets-path->absolute-path href)
+                          href)]
+                    (->elem
+                     :a
+                     (cond->
+                         {:href      (str "file://" href*)
+                          :data-href href*
+                          :target    "_blank"}
+                       title
+                       (assoc :title title))
+                     (map-inline config label))))))
 
             ;; image
             (show-link? config metadata href full_text)
@@ -933,6 +1012,9 @@
                     :height height
                     :width width}])))))
 
+        (= name "tutorial-video")
+        (tutorial-video)
+
         (= name "vimeo")
         (when-let [url (first arguments)]
           (let [Vimeo-regex #"^((?:https?:)?//)?((?:www).)?((?:player.vimeo.com|vimeo.com)?)((?:/video/)?)([\w-]+)(\S+)?$"]
@@ -2058,7 +2140,7 @@
              [:ul#query-pages.mt-1
               (for [{:block/keys [name original-name] :as page-entity} result]
                 [:li.mt-1
-                 [:a {:href (rfe/href :page {:name name})
+                 [:a.page-ref {:href (rfe/href :page {:name name})
                       :on-click (fn [e]
                                   (util/stop e)
                                   (if (gobj/get e "shiftKey")

+ 19 - 6
src/main/frontend/components/editor.cljs

@@ -6,6 +6,7 @@
             [frontend.components.datetime :as datetime-comp]
             [frontend.components.search :as search]
             [frontend.components.svg :as svg]
+            [frontend.components.block :as block]
             [frontend.config :as config]
             [frontend.db :as db]
             [frontend.handler.editor :as editor-handler :refer [get-state]]
@@ -74,6 +75,9 @@
                                                      {:last-pattern commands/angle-bracket}))
         :class     "black"}))))
 
+(defn- in-sidebar? [el]
+  (not (.contains (.getElementById js/document "left-container") el)))
+
 (rum/defc page-search < rum/reactive
   {:will-unmount (fn [state] (reset! editor-handler/*selected-text nil) state)}
   [id format]
@@ -84,6 +88,7 @@
         (let [current-pos (cursor/pos input)
               edit-content (or (state/sub [:editor/content id]) "")
               edit-block (state/sub :editor/block)
+              sidebar? (in-sidebar? input)
               q (or
                  @editor-handler/*selected-text
                  (when (state/sub :editor/show-page-search-hashtag?)
@@ -93,12 +98,20 @@
               matched-pages (when-not (string/blank? q)
                               (editor-handler/get-matched-pages q))]
           (ui/auto-complete
-           matched-pages
-           {:on-chosen (page-handler/on-chosen-handler input id q pos format)
-            :on-enter #(page-handler/page-not-exists-handler input id q current-pos)
-            :item-render (fn [item] [:div.py-2 (search/highlight-exact-query item q)])
-            :empty-div [:div.text-gray-500.pl-4.pr-4 "Search for a page"]
-            :class     "black"}))))))
+            matched-pages
+            {:on-chosen   (page-handler/on-chosen-handler input id q pos format)
+             :on-enter    #(page-handler/page-not-exists-handler input id q current-pos)
+             :item-render (fn [page-name chosen?]
+                            [:div.py-2.preview-trigger-wrapper
+                             (block/page-preview-trigger
+                               {:children        [:div (search/highlight-exact-query page-name q)]
+                                :open?           chosen?
+                                :fixed-position? true
+                                :tippy-distance  24
+                                :tippy-position  (if sidebar? "left" "right")}
+                               page-name)])
+             :empty-div   [:div.text-gray-500.pl-4.pr-4 "Search for a page"]
+             :class       "black"}))))))
 
 (rum/defcs block-search-auto-complete < rum/reactive
   {:init (fn [state]

+ 5 - 0
src/main/frontend/components/editor.css

@@ -63,3 +63,8 @@ pre {
   background: #f6f8fa;
   background: var(--ls-secondary-background-color);
 }
+
+/* Fix autocomplete preview  */
+.preview-trigger-wrapper > [data-tooltipped] {
+  display: block !important;
+}

+ 36 - 22
src/main/frontend/components/header.cljs

@@ -26,16 +26,18 @@
             [frontend.handler.migrate :as migrate]))
 
 (rum/defc logo < rum/reactive
-  [{:keys [white?]}]
+  [{:keys [white? electron-mac?]}]
   [:a.cp__header-logo
    {:href     (rfe/href :home)
     :on-click (fn []
                 (util/scroll-to-top)
                 (state/set-journals-length! 2))}
-   (if-let [logo (and config/publishing?
-                      (get-in (state/get-config) [:project :logo]))]
-     [:img.cp__header-logo-img {:src logo}]
-     (svg/logo (not white?)))])
+   (if electron-mac?
+     svg/home
+     (if-let [logo (and config/publishing?
+                       (get-in (state/get-config) [:project :logo]))]
+      [:img.cp__header-logo-img {:src logo}]
+      (svg/logo (not white?))))])
 
 (rum/defc login
   [logged?]
@@ -45,10 +47,11 @@
 
       (ui/dropdown-with-links
        (fn [{:keys [toggle-fn]}]
-         [:a.fade-link {:on-click toggle-fn}
-          [:span.ml-1 (t :login)]])
-       (let [list [{:title (t :login-google)
-                    :url (str config/website "/login/google")}
+         [:a.fade-link.block.p-2 {:on-click toggle-fn}
+          [:span (t :login)]])
+       (let [list [
+                   ;; {:title (t :login-google)
+                   ;;  :url (str config/website "/login/google")}
                    {:title (t :login-github)
                     :url (str config/website "/login/github")}]]
          (mapv
@@ -79,7 +82,7 @@
         logged? (state/logged?)]
     (ui/dropdown-with-links
      (fn [{:keys [toggle-fn]}]
-       [:a.cp__right-menu-button
+       [:a.cp__right-menu-button.block.p-2
         {:on-click toggle-fn}
         (svg/horizontal-dots nil)])
      (->>
@@ -150,12 +153,23 @@
      ;;                  [:div.px-2.py-2 (login logged?)])}
      )))
 
+(rum/defc back-and-forward
+  [electron-mac?]
+  [:div.flex.flex-row
+   [:a.opacity-60.hover:opacity-100.it.navigation.nav-left.block.p-2
+    {:title "Go Back" :on-click #(js/window.history.back)}
+    svg/arrow-narrow-left]
+   [:a.opacity-60.hover:opacity-100.it.navigation.nav-right.block.p-2
+    {:title "Go Forward" :on-click #(js/window.history.forward)}
+    svg/arrow-narrow-right]])
+
 (rum/defc header < rum/reactive
   [{:keys [open-fn current-repo white? logged? page? route-match me default-home new-block-mode]}]
   (let [local-repo? (= current-repo config/local-repo)
         repos (->> (state/sub [:me :repos])
                    (remove #(= (:url %) config/local-repo)))
-        electron-mac? (and util/mac? (util/electron?))]
+        electron-mac? (and util/mac? (util/electron?))
+        electron-not-mac? (and (util/electron?) (not electron-mac?))]
     (rum/with-context [[t] i18n/*tongue-context*]
       [:div.cp__header#head
        {:class (when electron-mac? "electron-mac")
@@ -168,21 +182,21 @@
                                       (open-fn)
                                       (state/set-left-sidebar-open! true))})
 
-       (logo {:white? white?})
-
-       (when (util/electron?)
-         [:a.opacity-60.hover:opacity-100.mr-1.it.navigation.nav-left
-          {:title "Go Back" :on-click #(js/window.history.back)} svg/arrow-narrow-left])
+       (when-not electron-mac?
+         (logo {:white? white?}))
 
-       (when (util/electron?)
-         [:a.opacity-60.hover:opacity-100.it.navigation.nav-right
-          {:style {:margin-right 5}
-           :title "Go Forward" :on-click #(js/window.history.forward)} svg/arrow-narrow-right])
+       (when electron-not-mac? (back-and-forward))
 
        (if current-repo
          (search/search)
          [:div.flex-1])
 
+       (when electron-mac?
+         (logo {:white? white?
+                :electron-mac? true}))
+
+       (when electron-mac? (back-and-forward true))
+
        (new-block-mode)
 
        (when-not (util/electron?)
@@ -198,7 +212,7 @@
 
        (when (and (nfs/supported?) (empty? repos)
                   (not config/publishing?))
-         [:a.text-sm.font-medium.opacity-70.hover:opacity-100.ml-3.block.open-button
+         [:a.text-sm.font-medium.opacity-70.hover:opacity-100.block.p-2
           {:on-click (fn []
                        (page-handler/ls-dir-files!))}
           [:div.flex.flex-row.text-center.open-button__inner
@@ -208,7 +222,7 @@
               (t :open)])]])
 
        (if config/publishing?
-         [:a.text-sm.font-medium.ml-3 {:href (rfe/href :graph)}
+         [:a.text-sm.font-medium.block.p-2 {:href (rfe/href :graph)}
           (t :graph)])
 
        (dropdown-menu {:me me

+ 9 - 49
src/main/frontend/components/header.css

@@ -2,7 +2,7 @@
   @apply shadow z-10 h-12;
   -webkit-app-region: drag;
 
-  padding-right: 1rem;
+  padding: 0 0.5rem;
   display: flex;
   align-items: center;
   flex: 0 0 auto;
@@ -14,6 +14,7 @@
   right: 0;
   user-select: none;
   transition: width 0.3s;
+  line-height: 1;
 
   .it svg {
     transform: scale(0.8);
@@ -26,44 +27,10 @@
       min-width: 14rem;
     }
   }
-
-  &.electron-mac {
-    height: 38px;
-
-    .refresh {
-      margin: 0;
-    }
-
-    .open-button__icon-wrapper {
-      margin-right: 7px;
-    }
-
-    svg {
-      width: 18px;
-      height: 18px;
-    }
-
-    .navigation svg {
-      width: 24px;
-      height: 24px;
-    }
-
-    .open-button {
-      &__inner {
-        @apply items-center;
-
-        svg {
-          width: 18px;
-          height: 18px;
-        }
-      }
-    }
-
-  }
 }
 
 .is-electron.is-mac .cp__header {
-    padding-left: 72px;
+    padding-left: 78px;
     -moz-transition: padding-left .3s ease-in;
     -o-transition: padding-left  .3s ease-in;
     -webkit-transition: padding-left  .3s ease-in;
@@ -96,11 +63,9 @@
 }
 
 .cp__header-logo {
-  @apply px-4;
-  height: 100%;
+  @apply p-2;
 }
 
-.cp__header-logo,
 .cp__right-menu-button {
   opacity: 0.3;
 }
@@ -119,14 +84,6 @@
   height: 24px;
 }
 
-.cp__right-menu-button {
-  @apply ml-3;
-}
-
-.cp__right-menu-button {
-  display: block;
-}
-
 @screen sm {
   .cp__header {
     @apply shadow-none;
@@ -137,11 +94,14 @@
   }
 
   .cp__header-logo {
-    display: flex;
-    align-items: center;
+    display: block;
   }
 }
 
+.cp__header-logo svg {
+    transform: scale(0.9);
+}
+
 #repo-name {
   @apply md:max-w-none;
   vertical-align: middle;

+ 6 - 1
src/main/frontend/components/hierarchy.cljs

@@ -9,6 +9,7 @@
             [frontend.state :as state]
             [frontend.text :as text]))
 
+;; FIXME: use block/namespace to get the relation
 (defn get-relation
   [page]
   (when (text/namespace-page? page)
@@ -22,7 +23,11 @@
 (rum/defc structures
   [page]
   (let [namespaces (get-relation page)]
-    (when (seq namespaces)
+    (when (and (seq namespaces)
+               (not (and (= 1
+                            (count namespaces)
+                            (count (first namespaces)))
+                         (not (string/includes? (ffirst namespaces) "/")))))
       [:div.page-hierachy.mt-6
        (ui/foldable
         [:h2.font-bold.opacity-30 "Hierarchy"]

+ 1 - 1
src/main/frontend/components/journal.cljs

@@ -51,7 +51,7 @@
                                 data-page-tags
                                 (assoc :data-page-tags data-page-tags))
      (ui/foldable
-      [:a.initial-color.title
+      [:a.initial-color.title.journal-title
        {:href     (rfe/href :page {:name page})
         :on-click (fn [e]
                     (when (gobj/get e "shiftKey")

+ 10 - 4
src/main/frontend/components/onboarding.cljs

@@ -6,6 +6,7 @@
             [frontend.extensions.latex :as latex]
             [frontend.handler.route :as route-handler]
             [frontend.ui :as ui]
+            [frontend.util :as util]
             [rum.core :as rum]))
 
 (rum/defc intro
@@ -45,7 +46,9 @@
 
       [:img.shadow-2xl
        {:src
-        "https://cdn.logseq.com/%2F8b9a461d-437e-4ca5-a2da-18b51077b5142020_07_25_Screenshot%202020-07-25%2013-29-49%20%2B0800.png?Expires=4749255017&Signature=Qbx6jkgAytqm6nLxVXQQW1igfcf~umV1OcG6jXUt09TOVhgXyA2Z5jHJ3AGJASNcphs31pZf4CjFQ5mRCyVKw6N8wb8Nn-MxuTJl0iI8o-jLIAIs9q1v-2cusCvuFfXH7bq6ir8Lpf0KYAprzuZ00FENin3dn6RBW35ENQwUioEr5Ghl7YOCr8bKew3jPV~OyL67MttT3wJig1j3IC8lxDDT8Ov5IMG2GWcHERSy00F3mp3tJtzGE17-OUILdeuTFz6d-NDFAmzB8BebiurYz0Bxa4tkcdLUpD5ToFHU08jKzZExoEUY8tvaZ1-t7djmo3d~BAXDtlEhC2L1YC2aVQ__&Key-Pair-Id=APKAJE5CCD6X7MP6PTEA"
+        (if (util/electron?)
+          "img/screenshot.png"
+          "https://cdn.logseq.com/%2F8b9a461d-437e-4ca5-a2da-18b51077b5142020_07_25_Screenshot%202020-07-25%2013-29-49%20%2B0800.png?Expires=4749255017&Signature=Qbx6jkgAytqm6nLxVXQQW1igfcf~umV1OcG6jXUt09TOVhgXyA2Z5jHJ3AGJASNcphs31pZf4CjFQ5mRCyVKw6N8wb8Nn-MxuTJl0iI8o-jLIAIs9q1v-2cusCvuFfXH7bq6ir8Lpf0KYAprzuZ00FENin3dn6RBW35ENQwUioEr5Ghl7YOCr8bKew3jPV~OyL67MttT3wJig1j3IC8lxDDT8Ov5IMG2GWcHERSy00F3mp3tJtzGE17-OUILdeuTFz6d-NDFAmzB8BebiurYz0Bxa4tkcdLUpD5ToFHU08jKzZExoEUY8tvaZ1-t7djmo3d~BAXDtlEhC2L1YC2aVQ__&Key-Pair-Id=APKAJE5CCD6X7MP6PTEA")
         :alt "screenshot"}]
 
       [:div.flex.flex-col.ls-block.intro-docs
@@ -166,7 +169,10 @@
               :target "_blank"} "isomorphic-git"]
          (t :on-boarding/isomorphic-git-desc)]]
 
-       [:img {:src "https://asset.logseq.com/static/img/credits.png"
+       [:img {:src
+              (if (util/electron?)
+                "img/credits.png"
+                "https://asset.logseq.com/static/img/credits.png")
               :style {:margin "12px 0 0 0"}}]]]]))
 
 (defn help
@@ -207,11 +213,11 @@
             :target "_blank"}
         (t :help/docs)]]
       [:li
-       [:a {:href "/blog/privacy-policy"
+       [:a {:href "https://logseq.com/blog/privacy-policy"
             :target "_blank"}
         (t :help/privacy)]]
       [:li
-       [:a {:href "/blog/terms"
+       [:a {:href "https://logseq.com/blog/terms"
             :target "_blank"}
         (t :help/terms)]]
       [:li

+ 216 - 37
src/main/frontend/components/page.cljs

@@ -12,6 +12,7 @@
             [frontend.handler.graph :as graph-handler]
             [frontend.handler.notification :as notification]
             [frontend.handler.editor :as editor-handler]
+            [frontend.handler.config :as config-handler]
             [frontend.state :as state]
             [clojure.string :as string]
             [frontend.components.block :as block]
@@ -20,7 +21,7 @@
             [frontend.components.reference :as reference]
             [frontend.components.svg :as svg]
             [frontend.components.export :as export]
-            [frontend.extensions.graph-2d :as graph-2d]
+            [frontend.extensions.graph :as graph]
             [frontend.components.hierarchy :as hierarchy]
             [frontend.ui :as ui]
             [frontend.components.content :as content]
@@ -34,7 +35,6 @@
             [goog.object :as gobj]
             [frontend.utf8 :as utf8]
             [frontend.date :as date]
-            [frontend.graph :as graph]
             [frontend.format.mldoc :as mldoc]
             [cljs-time.coerce :as tc]
             [cljs-time.core :as t]
@@ -43,7 +43,8 @@
             [reitit.frontend.easy :as rfe]
             [frontend.text :as text]
             [frontend.modules.shortcut.core :as shortcut]
-            [frontend.handler.block :as block-handler]))
+            [frontend.handler.block :as block-handler]
+            [cljs-bean.core :as bean]))
 
 (defn- get-page-name
   [state]
@@ -62,16 +63,18 @@
 (defn- open-first-block!
   [state]
   (let [blocks (nth (:rum/args state) 1)
-        block (first blocks)]
+        block (first blocks)
+        preview? (nth (:rum/args state) 4)]
     (when (and (= (count blocks) 1)
-               (string/blank? (:block/content block)))
+               (string/blank? (:block/content block))
+               (not preview?))
       (editor-handler/edit-block! block :max (:block/format block) (:block/uuid block))))
   state)
 
 (rum/defc page-blocks-inner <
   {:did-mount open-first-block!
    :did-update open-first-block!}
-  [page-name page-blocks hiccup sidebar?]
+  [page-name page-blocks hiccup sidebar? preview?]
   [:div.page-blocks-inner
    (rum/with-key
      (content/content page-name
@@ -129,7 +132,7 @@
                              config)
               hiccup-config (common-handler/config-with-document-mode hiccup-config)
               hiccup (block/->hiccup page-blocks hiccup-config {})]
-          (page-blocks-inner page-name page-blocks hiccup sidebar?))))))
+          (page-blocks-inner page-name page-blocks hiccup sidebar? preview?))))))
 
 (defn contents-page
   [page]
@@ -291,7 +294,7 @@
                        (not block?))
               [:div.flex.flex-row.space-between
                [:div.flex-1.flex-row
-                [:a {:on-click (fn [e]
+                [:a.page-title {:on-click (fn [e]
                                  (.preventDefault e)
                                  (when (gobj/get e "shiftKey")
                                    (when-let [page (db/pull repo '[*] [:block/name page-name])]
@@ -425,45 +428,221 @@
              [:div {:key "page-unlinked-references"}
               (reference/unlinked-references route-page-name)])])))))
 
-(defonce layout (atom [js/window.outerWidth js/window.outerHeight]))
+(defonce layout (atom [js/window.innerWidth js/window.innerHeight]))
 
-(defonce graph-ref (atom nil))
 (defonce show-journal? (atom false))
 
+;; scrollHeight
+(rum/defcs graph-filter-section < (rum/local false ::open?)
+  [state title content {:keys [search-filters]}]
+  (let [open? (get state ::open?)]
+    (when (and (seq search-filters) (not @open?))
+      (reset! open? true))
+    [:li.relative
+     [:div
+      [:button.w-full.px-4.py-2.text-left.focus:outline-none {:on-click #(swap! open? not)}
+       [:div.flex.items-center.justify-between
+        title
+        (if @open? (svg/caret-down) (svg/caret-right))]]
+      (content open?)]]))
+
+(rum/defc filter-expand-area
+  [open? content]
+  [:div.relative.overflow-hidden.transition-all.max-h-0.duration-700
+   {:style {:max-height (if @open? 400 0)}}
+   content])
+
+(defonce *n-hops (atom nil))
+(defonce *focus-nodes (atom []))
+(defonce *graph-reset? (atom false))
+
+(rum/defc graph-filters < rum/reactive
+  [graph settings n-hops]
+  (let [{:keys [layout journal? orphan-pages? builtin-pages?]
+         :or {layout "gForce"
+              orphan-pages? true}} settings
+        set-setting! (fn [key value]
+                       (let [new-settings (assoc settings key value)]
+                         (config-handler/set-config! :graph/settings new-settings)))
+        search-graph-filters (state/sub :search/graph-filters)
+        focus-nodes (rum/react *focus-nodes)]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      [:div.absolute.top-4.right-4.graph-filters
+       [:div.flex.flex-col
+        [:div.shadow-xl.rounded-sm
+         [:ul
+          (graph-filter-section
+           [:span.font-medium "Nodes"]
+           (fn [open?]
+             (filter-expand-area
+              open?
+              [:div
+               [:p.text-sm.opacity-70.px-4
+                (let [c1 (count (:nodes graph))
+                      s1 (if (> c1 1) "s" "")
+                      ;; c2 (count (:links graph))
+                      ;; s2 (if (> c2 1) "s" "")
+                      ]
+                  ;; (util/format "%d page%s, %d link%s" c1 s1 c2 s2)
+                  (util/format "%d page%s" c1 s1)
+                  )]
+               [:div.p-6
+               ;; [:div.flex.items-center.justify-between.mb-2
+               ;;  [:span "Layout"]
+               ;;  (ui/select
+               ;;    (mapv
+               ;;     (fn [item]
+               ;;       (if (= (:label item) layout)
+               ;;         (assoc item :selected "selected")
+               ;;         item))
+               ;;     [{:label "gForce"}
+               ;;      {:label "dagre"}])
+               ;;    (fn [value]
+               ;;      (set-setting! :layout value))
+               ;;    "graph-layout")]
+               [:div.flex.items-center.justify-between.mb-2
+                [:span "Journals"]
+                ;; FIXME: why it's not aligned well?
+                [:div.mt-1
+                 (ui/toggle journal?
+                            #(set-setting! :journal? (not journal?))
+                            true)]]
+               [:div.flex.items-center.justify-between.mb-2
+                [:span "Orphan pages"]
+                [:div.mt-1
+                 (ui/toggle orphan-pages?
+                            #(set-setting! :orphan-pages? (not orphan-pages?))
+                            true)]]
+               [:div.flex.items-center.justify-between.mb-2
+                [:span "Built-in pages"]
+                [:div.mt-1
+                 (ui/toggle builtin-pages?
+                            #(set-setting! :builtin-pages? (not builtin-pages?))
+                            true)]]
+               (when (seq focus-nodes)
+                 [:div.flex.flex-col.mb-2
+                  [:p {:title "N hops from selected nodes"}
+                   "N hops from selected nodes"]
+                  (ui/tippy {:html [:div.pr-3 n-hops]}
+                            (ui/slider (or n-hops 10)
+                                       {:min 1
+                                        :max 10
+                                        :on-change #(reset! *n-hops (int %))}))])
+
+               [:a.opacity-70.opacity-100 {:on-click (fn []
+                                                       (swap! *graph-reset? not)
+                                                       (reset! *focus-nodes [])
+                                                       (reset! *n-hops nil)
+                                                       (state/clear-search-filters!))}
+                "Reset Graph"]]])))
+          (graph-filter-section
+           [:span.font-medium "Search"]
+           (fn [open?]
+             (filter-expand-area
+              open?
+              [:div.p-6
+               (if (seq search-graph-filters)
+                 [:div
+                  (for [q search-graph-filters]
+                    [:div.flex.flex-row.justify-between.items-center.mb-2
+                     [:span.font-medium q]
+                     [:a.search-filter-close.opacity-70.opacity-100 {:on-click #(state/remove-search-filter! q)}
+                      svg/close]])
+
+                  [:a.opacity-70.opacity-100 {:on-click state/clear-search-filters!}
+                   "Clear All"]]
+                 [:a.opacity-70.opacity-100 {:on-click #(route-handler/go-to-search! :graph)}
+                  "Click to search"])]))
+           {:search-filters search-graph-filters})]]]])))
+
+(defn- graph-register-handlers
+  [graph focus-nodes n-hops]
+  (.on graph "nodeClick"
+       (fn [event node]
+         (graph/on-click-handler graph node event focus-nodes n-hops))))
+
+(rum/defc global-graph-inner < rum/reactive
+  [graph settings theme]
+  (let [[width height] (rum/react layout)
+        dark? (= theme "dark")
+        n-hops (rum/react *n-hops)
+        reset? (rum/react *graph-reset?)
+        focus-nodes (when n-hops (rum/react *focus-nodes))
+        graph (if (and (integer? n-hops)
+                       (seq focus-nodes)
+                       (not (:orphan-pages? settings)))
+                (graph-handler/n-hops graph focus-nodes n-hops)
+                graph)
+        graph (update graph :links (fn [links]
+                                     (let [nodes (set (map :id (:nodes graph)))]
+                                       (remove (fn [link]
+                                                 (and (not (nodes (:source link)))
+                                                      (not (nodes (:target link)))))
+                                               links))))]
+    (rum/with-context [[t] i18n/*tongue-context*]
+      [:div.relative#global-graph
+       (graph/graph-2d {:nodes (:nodes graph)
+                        :links (:links graph)
+                        :width (- width 24)
+                        :height (- height 48)
+                        :dark? dark?
+                        :register-handlers-fn
+                        (fn [graph]
+                          (graph-register-handlers graph *focus-nodes *n-hops))
+                        :reset? reset?})
+       (graph-filters graph settings n-hops)])))
+
+(defn- filter-graph-nodes
+  [nodes filters]
+  (if (seq filters)
+    (let [filter-patterns (map #(re-pattern (str "(?i)" (util/regex-escape %))) filters)]
+      (filter (fn [node] (some #(re-find % (:id node)) filter-patterns)) nodes))
+    nodes))
+
 (rum/defcs global-graph < rum/reactive
   (mixins/event-mixin
    (fn [state]
      (mixins/listen state js/window "resize"
                     (fn [e]
-                      (reset! layout [js/window.outerWidth js/window.outerHeight])))))
+                      (reset! layout [js/window.innerWidth js/window.innerHeight])))))
+  {:will-mount (fn [state]
+                 (state/set-search-mode! :graph)
+                 state)
+   :will-unmount (fn [state]
+                   (reset! *n-hops nil)
+                   (reset! *focus-nodes [])
+                   (state/set-search-mode! :global)
+                   state)}
   [state]
-  (let [theme (state/sub :ui/theme)
-        sidebar-open? (state/sub :ui/sidebar-open?)
-        [width height] (rum/react layout)
+  (let [settings (state/sub-graph-config)
+        theme (state/sub :ui/theme)
+        graph (graph-handler/build-global-graph theme settings)
+        search-graph-filters (state/sub :search/graph-filters)
+        graph (update graph :nodes #(filter-graph-nodes % search-graph-filters))
+        reset? (rum/react *graph-reset?)]
+    (global-graph-inner graph settings theme)))
+
+(rum/defc page-graph < db-mixins/query rum/reactive
+  []
+  (let [page (or
+              (and (= :page (state/sub [:route-match :data :name]))
+                   (state/sub [:route-match :path-params :name]))
+              (date/today))
+        theme (:ui/theme @state/state)
         dark? (= theme "dark")
-        graph (graph-handler/build-global-graph theme (rum/react show-journal?))]
-    (rum/with-context [[t] i18n/*tongue-context*]
-      [:div.relative#global-graph
-       (if (seq (:nodes graph))
-         (graph-2d/graph
-          (graph/build-graph-opts
-           graph
-           dark?
-           {:width (if (and (> width 1280) sidebar-open?)
-                     (- width 24 600)
-                     (- width 24))
-            :height height
-            :ref (fn [v] (reset! graph-ref v))
-            :ref-atom graph-ref}))
-         [:div.ls-center.mt-20
-          [:p.opacity-70.font-medium "Empty"]])
-       [:div.absolute.top-10.left-5
-        [:div.flex.flex-col
-         [:a.text-sm.font-medium
-          {:on-click (fn [_e]
-                       (swap! show-journal? not))}
-          (str (t :page/show-journals)
-               (if @show-journal? " (ON)"))]]]])))
+        graph (if (util/uuid-string? page)
+                (graph-handler/build-block-graph (uuid page) theme)
+                (graph-handler/build-page-graph page theme))]
+    (when (seq (:nodes graph))
+      [:div.sidebar-item.flex-col
+       (graph/graph-2d {:nodes (:nodes graph)
+                        :links (:links graph)
+                        :width 600
+                        :height 600
+                        :dark? dark?
+                        :register-handlers-fn
+                        (fn [graph]
+                          (graph-register-handlers graph (atom nil) (atom nil)))})])))
 
 (rum/defc all-pages < rum/reactive
   ;; {:did-mount (fn [state]

+ 30 - 0
src/main/frontend/components/page.css

@@ -42,3 +42,33 @@
     width: 20px;
     height: 20px;
 }
+
+.graph-filters {
+    width: 200px;
+    background: var(--ls-secondary-background-color);
+}
+
+.graph-filters ul {
+    margin-left: 0;
+}
+
+.graph-filters li {
+    list-style: none;
+    margin: 0;
+}
+
+.graph-layout {
+    background: var(--ls-secondary-background-color);
+}
+
+.search-filter-close svg {
+    transform: scale(0.7);
+}
+
+/* Change to another cursor style if Shift key is active */
+[data-active-keystroke*="Shift" i]
+:is(.journal-title, .page-title,
+    .block-ref, .page-ref, a.tag, 
+    .bullet-container.cursor) {
+  cursor: e-resize;
+}

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

@@ -99,8 +99,8 @@
       (when-not (= repo config/local-repo)
         (if (and nfs-repo? (nfs-handler/supported?))
           (let [syncing? (state/sub :graph/syncing?)]
-            [:div.ml-2.mr-1.mt-1.opacity-60.refresh.hover:opacity-100 {:class (if syncing? "loader" "initial")}
-             [:a
+            [:div.opacity-60.refresh.hover:opacity-100 {:class (if syncing? "loader" "initial")}
+             [:a.block.p-2
               {:on-click #(nfs-handler/refresh! repo refresh-cb)
                :title (str "Import files from the local directory: " (config/get-local-dir repo) ".\nVersion: "
                            version/version)}
@@ -202,8 +202,7 @@
         (when (seq repos)
           (ui/dropdown-with-links
            (fn [{:keys [toggle-fn]}]
-             [:a#repo-switch.fade-link {:on-click toggle-fn}
-
+             [:a#repo-switch.fade-link.block.pr-2 {:on-click toggle-fn}
               [:span
                [:span.repo-plus svg/plus]
                (let [repo-name (get-repo-name current-repo)

+ 6 - 26
src/main/frontend/components/right_sidebar.cljs

@@ -4,7 +4,7 @@
             [frontend.components.svg :as svg]
             [frontend.components.page :as page]
             [frontend.components.block :as block]
-            [frontend.extensions.graph-2d :as graph-2d]
+            [frontend.extensions.graph :as graph]
             [frontend.components.onboarding :as onboarding]
             [frontend.handler.route :as route-handler]
             [frontend.handler.page :as page-handler]
@@ -19,7 +19,6 @@
             [frontend.extensions.slide :as slide]
             [cljs-bean.core :as bean]
             [goog.object :as gobj]
-            [frontend.graph :as graph]
             [frontend.context.i18n :as i18n]
             [reitit.frontend.easy :as rfe]
             [frontend.db-mixins :as db-mixins]
@@ -28,7 +27,7 @@
 (rum/defc toggle
   []
   (when-not (util/mobile?)
-    [:a.opacity-60.hover:opacity-100.ml-4 {:on-click state/toggle-sidebar-open?!}
+    [:a.opacity-60.hover:opacity-100.block.p-2 {:on-click state/toggle-sidebar-open?!}
     (svg/menu)]))
 
 (rum/defc block-cp < rum/reactive
@@ -45,25 +44,6 @@
               :sidebar?   true
               :repo       repo}))
 
-(rum/defc page-graph < db-mixins/query rum/reactive
-  []
-  (let [page (or
-              (and (= :page (state/sub [:route-match :data :name]))
-                   (state/sub [:route-match :path-params :name]))
-              (date/today))
-        theme (:ui/theme @state/state)
-        dark? (= theme "dark")
-        graph (if (util/uuid-string? page)
-                (graph-handler/build-block-graph (uuid page) theme)
-                (graph-handler/build-page-graph page theme))]
-    (when (seq (:nodes graph))
-      [:div.sidebar-item.flex-col
-       (graph-2d/graph
-        (graph/build-graph-opts
-         graph dark?
-         {:width  600
-          :height 600}))])))
-
 (defn recent-pages
   []
   (let [pages (->> (db/get-key-value :recent/pages)
@@ -72,7 +52,7 @@
     [:div.recent-pages.text-sm.flex-col.flex.ml-3.mt-2
      (if (seq pages)
        (for [page pages]
-         [:a.mb-1 {:key      (str "recent-page-" page)
+         [:a.page-ref.mb-1 {:key      (str "recent-page-" page)
                    :href     (rfe/href :page {:name page})
                    :on-click (fn [e]
                                (when (gobj/get e "shiftKey")
@@ -107,7 +87,7 @@
 
     :page-graph
     [(str (t :right-side-bar/page-graph))
-     (page-graph)]
+     (page/page-graph)]
 
     :block-ref
     (when-let [block (db/entity repo [:block/uuid (:block/uuid (:block block-data))])]
@@ -133,7 +113,7 @@
     :page
     (let [page-name (or (:block/name block-data)
                         db-id)]
-      [[:a {:href     (rfe/href :page {:name page-name})
+      [[:a.page-title {:href     (rfe/href :page {:name page-name})
             :on-click (fn [e]
                         (when (gobj/get e "shiftKey")
                           (.preventDefault e)))}
@@ -264,7 +244,7 @@
 
           (sidebar-resizer)
           [:div.cp__right-sidebar-scrollable
-           [:div.cp__right-sidebar-topbar.flex.flex-row.justify-between.items-center.px-4.h-12
+           [:div.cp__right-sidebar-topbar.flex.flex-row.justify-between.items-center.pl-4.pr-2.h-12
            [:div.cp__right-sidebar-settings.hide-scrollbar {:key "right-sidebar-settings"}
             [:div.ml-4.text-sm
              [:a.cp__right-sidebar-settings-btn {:on-click (fn [e]

+ 24 - 6
src/main/frontend/components/search.cljs

@@ -143,7 +143,7 @@
     (let [pages (when-not all? (map (fn [page] {:type :page :data page}) pages))
           files (when-not all? (map (fn [file] {:type :file :data file}) files))
           blocks (map (fn [block] {:type :block :data block}) blocks)
-          search-mode (state/get-search-mode)
+          search-mode (state/sub :search/mode)
           new-page (if (or
                         (and (seq pages)
                              (= (string/lower-case search-q)
@@ -154,7 +154,10 @@
                      [{:type :new-page}])
           result (if config/publishing?
                    (concat pages files blocks)
-                   (concat new-page pages files blocks))]
+                   (concat new-page pages files blocks))
+          result (if (= search-mode :graph)
+                   [{:type :graph-add-filter}]
+                   result)]
       [:div.rounded-md.shadow-lg.search-ac
        {:style (merge
                 {:top 48
@@ -170,6 +173,9 @@
                       (search-handler/clear-search!)
                       (leave-focus)
                       (case type
+                        :graph-add-filter
+                        (state/add-graph-search-filter! search-q)
+
                         :new-page
                         (page-handler/create! search-q)
 
@@ -211,11 +217,20 @@
                                  :block
                                  block))
 
-                              nil)
-                            (search-handler/clear-search!))
+                              :new-page
+                              (page-handler/create! search-q)
+
+                              :file
+                              (route/redirect! {:to :file
+                                                :path-params {:path data}})
+
+                              nil))
          :item-render (fn [{:keys [type data]}]
                         (let [search-mode (state/get-search-mode)]
                           [:div {:class "py-2"} (case type
+                                                  :graph-add-filter
+                                                  [:b search-q]
+
                                                   :new-page
                                                   [:div.text.font-bold (str (t :new-page) ": ")
                                                    [:span.ml-1 (str "\"" search-q "\"")]]
@@ -270,7 +285,7 @@
                   :else
                   300)]
     (rum/with-context [[t] i18n/*tongue-context*]
-      [:div#search.flex-1.flex
+      [:div#search.flex-1.flex.p-2
        [:div.inner
         [:label.sr-only {:for "search-field"} (t :search)]
         [:div#search-wrapper.relative.w-full.text-gray-400.focus-within:text-gray-600
@@ -278,7 +293,10 @@
           svg/search]
          [:input#search-field.block.w-full.h-full.pr-3.py-2.rounded-md.focus:outline-none.placeholder-gray-500.focus:placeholder-gray-400.sm:text-sm.sm:bg-transparent
           {:style {:padding-left "2rem"}
-           :placeholder (if (= search-mode :page)
+           :placeholder (case search-mode
+                          :graph
+                          (t :graph-search)
+                          :page
                           (t :page-search)
                           (t :search))
            :auto-complete (if (util/chrome?) "chrome-off" "off") ; off not working here

+ 4 - 3
src/main/frontend/components/sidebar.cljs

@@ -270,9 +270,9 @@
                       [:ul
                        [:li "Shift + Enter to create new block"]
                        [:li "Click `D` or type `t d` to toggle document mode"]]]}
-     [:a.px-1.text-sm.font-medium.bg-base-2.mr-4.rounded-md
-      {:on-click state/toggle-document-mode!}
-      "D"])))
+              [:a.block.px-1.text-sm.font-medium.bg-base-2.rounded-md.mx-2
+               {:on-click state/toggle-document-mode!}
+               "D"])))
 
 (rum/defc help-button < rum/reactive
   []
@@ -345,6 +345,7 @@
           :close-fn    close-fn
           :route-match route-match})
         [:div.#app-container.h-screen.flex
+         {:class (if (state/sub :ui/sidebar-open?) "w-full" "overflow-hidden")}
          [:div.flex-1.h-full.w-full.flex.flex-col#left-container.relative
           [:div.scrollbar-spacing#main-container
            (header/header {:open-fn        open-fn

+ 6 - 1
src/main/frontend/components/sidebar.css

@@ -143,7 +143,6 @@
 
     &-btn {
       display: block;
-      padding: 13px 5px;
       white-space: nowrap;
     }
   }
@@ -156,6 +155,12 @@
       right: 0;
       background-color: var(--ls-secondary-background-color, #d8e1e8);
       z-index: 999;
+      user-select: none;
+      -webkit-app-region: drag;
+
+      a, svg {
+          -webkit-app-region: no-drag;
+      }
   }
 
   .page {

+ 25 - 5
src/main/frontend/components/svg.cljs

@@ -72,10 +72,10 @@
      :d "M10 19L3 12M3 12L10 5M3 12L21 12"}]])
 
 (def arrow-narrow-left
-  [:svg.h-6.w-6 {:xmlns "http://www.w3.org/2000/svg" :fill "none" :viewbox "0 0 24 24" :stroke "currentColor"} [:path {:stroke-linecap "round" :stroke-linejoin "round" :stroke-width "2" :d "M7 16l-4-4m0 0l4-4m-4 4h18"}]])
+  [:svg.h-6.w-6 {:xmlns "http://www.w3.org/2000/svg" :fill "none" :view-box "0 0 24 24" :stroke "currentColor"} [:path {:stroke-linecap "round" :stroke-linejoin "round" :stroke-width "2" :d "M7 16l-4-4m0 0l4-4m-4 4h18"}]])
 
 (def arrow-narrow-right
-  [:svg.h-6.w-6 {:xmlns "http://www.w3.org/2000/svg" :fill "none" :viewbox "0 0 24 24" :stroke "currentColor"} [:path {:stroke-linecap "round" :stroke-linejoin "round" :stroke-width "2" :d "M17 8l4 4m0 0l-4 4m4-4H3"}]])
+  [:svg.h-6.w-6 {:xmlns "http://www.w3.org/2000/svg" :fill "none" :view-box "0 0 24 24" :stroke "currentColor"} [:path {:stroke-linecap "round" :stroke-linejoin "round" :stroke-width "2" :d "M17 8l4 4m0 0l-4 4m4-4H3"}]])
 
 (defonce arrow-right-v2
   [:svg.h-3.w-3
@@ -386,8 +386,7 @@
 (rum/defc logo
   [dark?]
   [:svg.svg-shadow
-   {:fill (if dark? "currentColor" "#002B36"), :view-box "0 0 21 21", :height "21", :width "21"
-    :style {:margin-top 2}}
+   {:fill (if dark? "currentColor" "#002B36"), :view-box "0 0 21 21", :height "21", :width "21"}
    [:ellipse
     {:transform
      "matrix(0.987073 0.160274 -0.239143 0.970984 11.7346 2.59206)"
@@ -559,5 +558,26 @@
 
 
 (defn info []
-  [:svg {:class "info" :viewbox "0 0 16 16" :width "16px" :height "16px"}
+  [:svg {:class "info" :view-box "0 0 16 16" :width "16px" :height "16px"}
    [:g [:path {:style {:transform "scale(0.25)"} :d "m32 2c-16.568 0-30 13.432-30 30s13.432 30 30 30 30-13.432 30-30-13.432-30-30-30m5 49.75h-10v-24h10v24m-5-29.5c-2.761 0-5-2.238-5-5s2.239-5 5-5c2.762 0 5 2.238 5 5s-2.238 5-5 5"}]]])
+
+(defn play []
+  [:svg
+   {:stroke "currentColor"
+    :view-box "0 0 24 24"
+    :preserve-aspect-ratio "none"
+    :fill "none"}
+   [:path
+    {:d
+     "M14.752 11.168l-3.197-2.132A1 1 0 0010 9.87v4.263a1 1 0 001.555.832l3.197-2.132a1 1 0 000-1.664z"
+     :stroke-width "2"
+     :stroke-linejoin "round"
+     :stroke-linecap "round"}]
+   [:path
+    {:d "M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
+     :stroke-width "2"
+     :stroke-linejoin "round"
+     :stroke-linecap "round"}]])
+
+(def home
+  [:svg.h-6.w-6 {:fill "none" :viewbox "0 0 24 24" :stroke "currentColor"} [:path {:stroke-linecap "round" :stroke-linejoin "round" :stroke-width "2" :d "M3 12l2-2m0 0l7-7 7 7M5 10v10a1 1 0 001 1h3m10-11l2 2m-2-2v10a1 1 0 01-1 1h-3m-6 0a1 1 0 001-1v-4a1 1 0 011-1h2a1 1 0 011 1v4a1 1 0 001 1m-6 0h6"}]])

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

@@ -2,6 +2,7 @@
   (:require [rum.core :as rum]
             [frontend.util :as util]
             [frontend.ui :as ui]
+            [frontend.handler.ui :as ui-handler]
             [frontend.handler.route :as route-handler]
             [frontend.handler.plugin :as plugin-handler]
             [frontend.components.svg :as svg]))
@@ -23,7 +24,9 @@
    [sidebar-open?])
 
   (rum/use-effect!
-    #(plugin-handler/hook-plugin-app :current-graph-changed {})
+    (fn []
+      (ui-handler/add-style-if-exists!)
+      (plugin-handler/hook-plugin-app :current-graph-changed {}))
     [current-repo])
 
   (rum/use-effect!

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

@@ -47,10 +47,10 @@
   get-latest-journals get-matched-blocks get-page get-page-alias get-page-alias-names get-page-blocks get-page-linked-refs-refed-pages
   get-page-blocks-count get-page-blocks-no-cache get-page-file get-page-format get-page-properties
   get-page-referenced-blocks get-page-referenced-pages get-page-unlinked-references get-page-referenced-blocks-no-cache
-  get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
+  get-all-pages get-pages get-pages-relation get-pages-that-mentioned-page get-public-pages get-tag-pages
   journal-page? local-native-fs? mark-repo-as-cloned! page-alias-set page-blocks-transform pull-block
   set-file-last-modified-at! transact-files-db! with-block-refs-count get-modified-pages page-empty? page-empty-or-dummy? get-alias-source-page
-  set-file-content! has-children? get-namespace-pages]
+  set-file-content! has-children? get-namespace-pages get-all-namespace-relation]
 
  [frontend.db.react
   get-current-marker get-current-page get-current-priority set-key-value

+ 4 - 1
src/main/frontend/db/default.cljs

@@ -1,10 +1,13 @@
 (ns frontend.db.default
   (:require [clojure.string :as string]))
 
+(defonce built-in-pages-names
+  #{"NOW" "LATER" "DOING" "DONE" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C"})
+
 (def built-in-pages
   (mapv (fn [p]
           {:block/name (string/lower-case p)
            :block/original-name p
            :block/journal? false
            :block/uuid (random-uuid)})
-        #{"NOW" "LATER" "DOING" "DONE" "IN-PROGRESS" "TODO" "WAIT" "WAITING" "A" "B" "C"}))
+        built-in-pages-names))

+ 36 - 2
src/main/frontend/db/model.cljs

@@ -118,7 +118,16 @@
          [?page :block/tags ?e]
          [?e :block/name ?tag]
          [?page :block/name ?page-name]]
-       (conn/get-conn repo)))
+    (conn/get-conn repo)))
+
+(defn get-all-namespace-relation
+  [repo]
+  (d/q '[:find ?page-name ?parent
+         :where
+         [?page :block/name ?page-name]
+         [?page :block/namespace ?e]
+         [?e :block/name ?parent]]
+    (conn/get-conn repo)))
 
 (defn get-pages
   [repo]
@@ -130,6 +139,14 @@
         (conn/get-conn repo))
        (map first)))
 
+(defn get-all-pages
+  [repo]
+  (d/q
+    '[:find [(pull ?page [*]) ...]
+      :where
+      [?page :block/name]]
+    (conn/get-conn repo)))
+
 (defn get-modified-pages
   [repo]
   (-> (d/q
@@ -710,6 +727,23 @@
    (vector? block)
    (= "Heading" (first block))))
 
+(defn get-redirect-page-name
+  ([page-name] (get-redirect-page-name page-name false))
+  ([page-name alias?]
+   (let [page-entity (db-utils/entity [:block/name page-name])]
+     (cond
+       alias?
+       page-name
+
+       (page-empty-or-dummy? (state/get-current-repo) (:db/id page-entity))
+       (let [source-page (get-alias-source-page (state/get-current-repo)
+                                                      (string/lower-case page-name))]
+         (or (when source-page (:block/name source-page))
+             page-name))
+
+       :else
+       page-name))))
+
 (defn get-page-original-name
   [page-name]
   (when page-name
@@ -1255,7 +1289,7 @@
 (defn get-namespace-pages
   [repo namespace]
   (assert (string? namespace))
-  (let [db (conn/get-conn repo)]
+  (when-let [db (conn/get-conn repo)]
     (when-not (string/blank? namespace)
       (let [namespace (string/lower-case (string/trim namespace))
             ids (->> (d/datoms db :aevt :block/name)

+ 2 - 1
src/main/frontend/db_schema.cljs

@@ -1,10 +1,11 @@
 (ns frontend.db-schema)
 
 (defonce version "0.0.2")
-
+(defonce ast-version "0.0.1")
 ;; A page is a special block, a page can corresponds to multiple files with the same ":block/name".
 (def schema
   {:schema/version  {}
+   :ast/version     {}
    :db/type         {}
    :db/ident        {:db/unique :db.unique/identity}
 

+ 3 - 1
src/main/frontend/dicts.cljs

@@ -242,6 +242,7 @@
                   "Search"
                   "Search or Create Page")
         :page-search "Search in the current page"
+        :graph-search "Search graph"
         :new-page "New page"
         :new-file "New file"
         :new-graph "Add new graph"
@@ -277,7 +278,7 @@
         :parsing-files "Parsing files"
         :loading-files "Loading files"
         :login-github "Login with Github"
-        :login-google "Login with Google"
+        ;; :login-google "Login with Google"
         :login "Login"
         :go-to "Go to "
         :or "or"
@@ -910,6 +911,7 @@
                      "搜索"
                      "搜索或者创建新页面")
            :page-search "在当前页面搜索"
+           :graph-search "搜索图谱"
            :new-page "新页面"
            :new-file "新文件"
            :graph "图谱"

+ 4 - 3
src/main/frontend/error.cljs

@@ -1,8 +1,9 @@
 (ns frontend.error
   (:require [clojure.string :as string]))
 
-(defonce ignored
-  #{"ResizeObserver loop limit exceeded"})
+(def ignored
+  #{"ResizeObserver loop limit exceeded"
+    "Uncaught TypeError:"})
 
 (defn ignored?
   [message]
@@ -10,5 +11,5 @@
     (boolean
      (some
       ;; TODO: some cases might need regex check
-      #(= (string/lower-case message) (string/lower-case %))
+      #(string/starts-with? (string/lower-case message) (string/lower-case %))
       ignored))))

+ 90 - 0
src/main/frontend/extensions/graph.cljs

@@ -0,0 +1,90 @@
+(ns frontend.extensions.graph
+  (:require [rum.core :as rum]
+            [frontend.rum :as r]
+            [frontend.ui :as ui]
+            [shadow.lazy :as lazy]
+            [frontend.handler.route :as route-handler]
+            [clojure.string :as string]
+            [cljs-bean.core :as bean]
+            [goog.object :as gobj]
+            [frontend.state :as state]
+            [frontend.db :as db]
+            [promesa.core :as p]
+            [clojure.set :as set]
+            [cljs-bean.core :as bean]
+            [frontend.extensions.graph.pixi :as pixi]
+            [frontend.util :as util :refer [profile]]
+            [cljs-bean.core :as bean]))
+
+(defonce clicked-page-timestamps (atom nil))
+
+(defn- highlight-node!
+  [^js graph node]
+  (.resetNodeStyle graph node
+                   (bean/->js {:color "#6366F1"
+                               :border {:width 2
+                                        :color "#6366F1"}})))
+
+(defn- highlight-neighbours!
+  [^js graph node]
+  (.forEachNeighbor
+   (.-graph graph) node
+   (fn [node attributes]
+     (let [attributes (bean/->clj attributes)
+           attributes (assoc attributes
+                             :color "#6366F1"
+                             :border {:width 2
+                                      :color "#6366F1"})]
+       (.resetNodeStyle graph node (bean/->js attributes))))))
+
+(defn- highlight-edges!
+  [^js graph node]
+  (.forEachEdge
+   (.-graph graph) node
+   (fn [edge attributes]
+     (.resetEdgeStyle graph edge (bean/->js {:width 1
+                                             :color "#A5B4FC"})))))
+
+(defn on-click-handler [graph node event *focus-nodes *n-hops]
+  (let [page-name (string/lower-case node)]
+    (when-not @*n-hops
+      ;; Don't trigger re-render
+      (swap! *focus-nodes
+            (fn [v]
+              (vec (distinct (conj v node))))))
+    ;; highlight current node
+    (let [node-attributes (-> (.getNodeAttributes (.-graph graph) node)
+                              (bean/->clj))]
+      (.setNodeAttribute (.-graph graph) node "parent" "ls-selected-nodes"))
+    (highlight-neighbours! graph node)
+    (highlight-edges! graph node)
+
+    ;; shift+click to select the page
+    (when-not (gobj/get event "shiftKey")
+      ;; double click to go to the page
+      (let [last-time (get @clicked-page-timestamps page-name)
+            new-time (util/time-ms)]
+        (swap! clicked-page-timestamps assoc page-name new-time)
+        (when (and last-time
+                   (< (- new-time last-time) 300))
+          (route-handler/redirect! {:to :page
+                                    :path-params {:name page-name}}))))))
+
+(defn reset-graph!
+  [^js graph]
+  (.resetView graph))
+
+(rum/defcs graph-2d <
+  (rum/local nil :ref)
+  {:did-update pixi/render!
+   :will-unmount (fn [state]
+                   (when-let [graph (:graph state)]
+                     (.destroy graph))
+                   (reset! clicked-page-timestamps nil)
+                   state)}
+  [state opts]
+  [:div.graph {:style {:height "100vh"}
+               :ref (fn [value]
+                      (let [ref (get state :ref)]
+                        (when (and ref value)
+                          (reset! ref value))))}])

+ 21 - 0
src/main/frontend/extensions/graph.css

@@ -0,0 +1,21 @@
+#global-graph,
+#page-graph {
+    min-height: 100% !important;
+    height: 100%;
+    width: 100%;
+    overflow: hidden;
+    position: relative;
+    z-index: 4;
+}
+
+#graphin-container {
+    height: 100%;
+    width: 100%;
+    position: relative;
+}
+.graphin-core {
+    height: 100%;
+    width: 100%;
+    min-height: 500px;
+    background: #fff;
+}

+ 126 - 0
src/main/frontend/extensions/graph/pixi.cljs

@@ -0,0 +1,126 @@
+(ns frontend.extensions.graph.pixi
+  (:require [rum.core :as rum]
+            [frontend.rum :as r]
+            [frontend.ui :as ui]
+            [shadow.lazy :as lazy]
+            [frontend.handler.route :as route-handler]
+            [frontend.util :as util :refer [profile]]
+            [clojure.string :as string]
+            [cljs-bean.core :as bean]
+            [goog.object :as gobj]
+            [frontend.state :as state]
+            [frontend.db :as db]
+            [promesa.core :as p]
+            [clojure.set :as set]
+            [cljs-bean.core :as bean]
+            ["pixi-graph-fork" :as Pixi-Graph]
+            ["graphology" :as graphology]
+            ["d3-force" :refer [forceSimulation forceManyBody forceCenter forceLink forceCollide forceRadial forceX forceY SimulationLinkDatum SimulationNodeDatum] :as force]))
+
+(def graph (gobj/get graphology "Graph"))
+
+(defonce colors
+  ["#1f77b4"
+   "#ff7f0e"
+   "#2ca02c"
+   "#d62728"
+   "#9467bd"
+   "#8c564b"
+   "#e377c2"
+   "#7f7f7f"
+   "#bcbd22"
+   "#17becf"])
+
+(defn default-style
+  [dark?]
+  {:node {:size (fn [node]
+                  (or (.-size node) 8))
+          :border {:width 0}
+          :color (fn [node]
+                   (if (gobj/get node "parent")
+                     (let [v (js/Math.abs (hash (.-id node)))]
+                       (nth colors (mod v (count colors))))
+                     (.-color node)))
+          :label {:content (fn [node] (.-id node))
+                  :type (.-TEXT (.-TextType Pixi-Graph))
+                  :fontSize 12
+                  :color "#333333"
+                  :backgroundColor "rgba(255, 255, 255, 0.5)"
+                  :padding 4}}
+   :edge {:width 1
+          :color (if dark? "#094b5a" "#cccccc")}})
+
+(defn default-hover-style
+  [dark?]
+  {:node {:color "#6366F1"
+          :border {:width 2
+                   :color "#6366F1"}
+          :label {:backgroundColor "rgba(238, 238, 238, 1)"}}
+   :edge {:color "#A5B4FC"}})
+
+;; TODO: animation
+;; (defn ticked [^js link ^js node]
+;;   (-> link
+;;       (.attr "x1" (fn [d] (.. d -source -x)))
+;;       (.attr "y1" (fn [d] (.. d -source -y)))
+;;       (.attr "x2" (fn [d] (.. d -target -x)))
+;;       (.attr "y2" (fn [d] (.. d -target -y))))
+
+;;   (-> node
+;;       (.attr "cx" (fn [d] (.-x d)))
+;;       (.attr "cy" (fn [d] (.-y d)))))
+
+(defn layout!
+  [nodes links]
+  (let [simulation (forceSimulation nodes)]
+    (-> simulation
+        (.force "link" (-> (forceLink)
+                           (.id (fn [d] (.-id d)))
+                           (.distance 180)
+                           (.links links)))
+        (.force "charge" (-> (forceManyBody)
+                             (.distanceMax 4000)
+                             (.theta 0.5)
+                             (.strength -600)))
+        (.force "collision" (-> (forceCollide)
+                                (.radius (+ 8 18))))
+        (.force "x" (-> (forceX 0) (.strength 0.02)))
+        (.force "y" (-> (forceX 0) (.strength 0.02)))
+        (.force "center" (forceCenter))
+        (.tick 30)
+        (.stop))))
+
+(defn render!
+  [state]
+  (when-let [graph (:graph state)]
+    (.destroy graph))
+  (let [{:keys [nodes links style hover-style height register-handlers-fn dark?]} (first (:rum/args state))
+        style (or style (default-style dark?))
+        hover-style (or hover-style (default-hover-style dark?))
+        graph (graph.)
+        nodes-set (set (map :id nodes))
+        links (->> (filter (fn [link]
+                             (and (nodes-set (:source link)) (nodes-set (:target link)))) links)
+                   (distinct))
+        nodes-js (bean/->js nodes)
+        links-js (bean/->js links)]
+    (layout! nodes-js links-js)
+    (doseq [node nodes-js]
+      (.addNode graph (.-id node) node))
+    (doseq [link links-js]
+      (.addEdge graph (.-id (.-source link)) (.-id (.-target link)) link))
+
+    (if-let [container-ref (:ref state)]
+      (let [graph (new (.-PixiGraph Pixi-Graph)
+                       (bean/->js
+                        {:container @container-ref
+                         :graph graph
+                         :style style
+                         :hoverStyle hover-style
+                         :height height}))]
+        (when register-handlers-fn
+          (register-handlers-fn graph))
+
+        ;; (.addEventListener container-ref)
+        (assoc state :graph graph))
+      state)))

+ 0 - 34
src/main/frontend/extensions/graph_2d.cljs

@@ -1,34 +0,0 @@
-(ns frontend.extensions.graph-2d
-  (:require [rum.core :as rum]
-            [frontend.loader :as loader]
-            [frontend.config :as config]
-            [goog.dom :as gdom]
-            [goog.object :as gobj]
-            [frontend.rum :as r]))
-
-;; TODO: extracted to a rum mixin
-(defn loaded? []
-  js/window.ForceGraph)
-
-(defonce graph-component
-  (atom nil))
-
-(defonce *loading? (atom true))
-
-(rum/defc graph < rum/reactive
-  {:init (fn [state]
-           (if @graph-component
-             (reset! *loading? false)
-             (do
-               (loader/load
-                (config/asset-uri "/static/js/react-force-graph.min.js")
-                (fn []
-                  (reset! graph-component
-                          (r/adapt-class (gobj/get js/window.ForceGraph "ForceGraph2D")))
-                  (reset! *loading? false)))))
-           state)}
-  [opts]
-  (let [loading? (rum/react *loading?)]
-    (when @graph-component
-      (@graph-component
-       opts))))

+ 7 - 1
src/main/frontend/format/block.cljs

@@ -272,13 +272,19 @@
   [original-page-name with-id?]
   (when original-page-name
     (let [[original-page-name page-name journal-day] (convert-page-if-journal original-page-name)
+          namespace? (and (string/includes? original-page-name "/")
+                          (text/namespace-page? original-page-name))
           m (merge
              {:block/name page-name
               :block/original-name original-page-name}
              (when with-id?
                (if-let [block (db/entity [:block/name page-name])]
                  {}
-                 {:block/uuid (db/new-block-id)})))]
+                 {:block/uuid (db/new-block-id)}))
+             (when namespace?
+               (let [namespace (first (util/split-last "/" original-page-name))]
+                 (when-not (string/blank? namespace)
+                   {:block/namespace {:block/name (string/lower-case namespace)}}))))]
       (if journal-day
         (merge m
                {:block/journal? true

+ 3 - 1
src/main/frontend/fs/nfs.cljs

@@ -133,13 +133,15 @@
                   format (-> (util/get-file-ext path)
                              (config/get-file-format))
                   pending-writes (state/get-write-chan-length)
-                  draw? (and path (string/ends-with? path ".excalidraw"))]
+                  draw? (and path (string/ends-with? path ".excalidraw"))
+                  config? (and path (string/ends-with? path "/config.edn"))]
             (p/let [_ (verify-permission repo file-handle true)
                     _ (utils/writeFile file-handle content)
                     file (.getFile file-handle)]
               (if (and local-content new?
                        (or
                         draw?
+                        config?
                         ;; Writing not finished
                         (> pending-writes 0)
                         ;; not changed by other editors

+ 0 - 161
src/main/frontend/graph.cljs

@@ -1,161 +0,0 @@
-(ns frontend.graph
-  (:require [frontend.handler.route :as route-handler]
-            [clojure.string :as string]
-            [cljs-bean.core :as bean]
-            [goog.object :as gobj]
-            [frontend.state :as state]
-            [frontend.db :as db]
-            [cljs-bean.core :as bean]))
-
-;; translated from https://github.com/vasturiano/react-force-graph/blob/master/example/highlight/index.html
-(defonce graph-mode (atom :dot-text))
-(defonce highlight-nodes (atom #{}))
-(defonce highlight-links (atom #{}))
-(defonce hover-node (atom nil))
-(defonce node-r 8)
-
-(defn- clear-highlights!
-  []
-  (reset! highlight-nodes #{})
-  (reset! highlight-links #{}))
-
-(defn- highlight-node!
-  [node]
-  (swap! highlight-nodes conj node))
-
-(defn- highlight-link!
-  [link]
-  (swap! highlight-links conj (bean/->clj link)))
-
-(defn- on-node-hover
-  [node]
-  (clear-highlights!)
-  (when node
-    (highlight-node! (gobj/get node "id"))
-    (doseq [neighbor (array-seq (gobj/get node "neighbors"))]
-      (highlight-node! neighbor))
-    (doseq [link (array-seq (gobj/get node "links"))]
-      (highlight-link! link)))
-  (reset! hover-node (gobj/get node "id")))
-
-(defn- on-link-hover
-  [link]
-  (clear-highlights!)
-  (when link
-    (highlight-link! link)
-    (highlight-node! (gobj/get link "source"))
-    (highlight-node! (gobj/get link "target"))))
-
-(defonce static-num (js/Math.pow 2 24))
-(defn get-color
-  [n]
-  (str "#" (-> (mod (* n 1234567)
-                    static-num)
-               (.toString 16)
-               (.padStart 6 "0"))))
-
-(defn- dot-text-mode
-  [node ctx global-scale dark?]
-  (let [hide-text? (< global-scale 0.45)
-        label (gobj/get node "id")
-        val (gobj/get node "val")
-        val (if (zero? val) 1 val)
-        font-size (min
-                   10
-                   (* (/ 15 global-scale) (js/Math.cbrt val)))
-        arc-radius (/ 3 global-scale)
-        _ (set! (.-font ctx)
-                (str font-size "px Inter"))
-        text-width (gobj/get (.measureText ctx label) "width")
-        x (gobj/get node "x")
-        y (gobj/get node "y")
-        color (gobj/get node "color")]
-    (set! (.-filltextAlign ctx) "center")
-    (set! (.-textBaseLine ctx) "middle")
-    (set! (.-fillStyle ctx) color)
-    (when-not hide-text?
-      (.fillText ctx label
-                 (- x (/ text-width 2))
-                 (- y (/ 9 global-scale))))
-
-    (.beginPath ctx)
-    (.arc ctx x y (if (zero? val)
-                    arc-radius
-                    (* arc-radius (js/Math.sqrt (js/Math.sqrt val)))) 0 (* 2 js/Math.PI) false)
-    (set! (.-fillStyle ctx)
-          (if (contains? @highlight-nodes label)
-            (if dark? "#A3BFFA" "#4C51BF")
-            (if dark? "#999" "#666")))
-    (.fill ctx)))
-
-(defn build-graph-data
-  [{:keys [links nodes]}]
-  (let [nodes (mapv
-               (fn [node]
-                 (let [links (filter (fn [{:keys [source target]}]
-                                       (let [node (:id node)]
-                                         (or (= source node) (= target node)))) links)]
-                   (assoc node
-                          :neighbors (vec
-                                      (distinct
-                                       (->>
-                                        (concat
-                                         (mapv :source links)
-                                         (mapv :target links))
-                                        (remove #(= (:id node) %)))))
-                          :links (vec links))))
-               nodes)]
-    {:links links
-     :nodes nodes}))
-
-(defn- build-graph-opts
-  [graph dark? option]
-  (let [nodes-count (count (:nodes graph))
-        graph-data (build-graph-data graph)]
-    (merge
-     {:graphData (bean/->js graph-data)
-      ;; :nodeRelSize node-r
-      :linkWidth (fn [link]
-                   (let [link {:source (gobj/get link "source")
-                               :target (gobj/get link "target")}]
-                     (if (contains? @highlight-links link) 5 1)))
-      :linkDirectionalParticles 2
-      :linkDirectionalParticleWidth (fn [link]
-                                      (let [link {:source (-> (gobj/get link "source")
-                                                              (gobj/get "id"))
-                                                  :target (-> (gobj/get link "target")
-                                                              (gobj/get "id"))}]
-                                        (if (contains? @highlight-links link) 2 0)))
-      :onNodeHover on-node-hover
-      :onLinkHover on-link-hover
-      :nodeLabel "id"
-      :linkColor (fn [] (if dark? "rgba(255,255,255,0.2)" "rgba(0,0,0,0.1)"))
-      :onZoom (fn [z]
-                (let [k (:k (bean/->clj z))]
-                  (reset! graph-mode
-                          (cond
-                            (< k 0.4)
-                            :dot
-
-                            :else
-                            :dot-text))))
-      :onNodeClick (fn [node event]
-                     (let [page-name (string/lower-case (gobj/get node "id"))]
-                       (if (gobj/get event "shiftKey")
-                         (let [repo (state/get-current-repo)
-                               page (db/entity repo [:block/name page-name])]
-                           (state/sidebar-add-block!
-                            repo
-                            (:db/id page)
-                            :page
-                            {:page page}))
-                         (route-handler/redirect! {:to :page
-                                                   :path-params {:name page-name}}))))
-      ;; :cooldownTicks 100
-      ;; :onEngineStop (fn []
-      ;;                 (when-let [ref (:ref-atom option)]
-      ;;                   (.zoomToFit @ref 400)))
-      :nodeCanvasObject
-      (fn [node ^CanvasRenderingContext2D ctx global-scale]
-        (dot-text-mode node ctx global-scale dark?))}
-     option)))

+ 0 - 9
src/main/frontend/graph.css

@@ -1,9 +0,0 @@
-#global-graph,
-#page-graph {
-  min-height: 100% !important;
-  height: 100%;
-  width: 100%;
-  overflow: hidden;
-  position: relative;
-  z-index: 4;
-}

+ 7 - 3
src/main/frontend/handler.cljs

@@ -80,10 +80,11 @@
                                  (assoc me :repos repos)
                                  old-db-schema
                                  (fn [repo]
-                                   (file-handler/restore-config! repo false)
-                                   (ui-handler/add-style-if-exists!))))
+                                   (file-handler/restore-config! repo false))))
                          (p/then
                           (fn []
+                            ;; try to load custom css only for current repo
+                            (ui-handler/add-style-if-exists!)
 
                             ;; install after config is restored
                             (shortcut/refresh!)
@@ -113,7 +114,10 @@
                                  (js/console.error "Failed to request GitHub app tokens."))))
 
                             (watch-for-date!)
-                            (file-handler/watch-for-local-dirs!)))
+                            (file-handler/watch-for-local-dirs!)
+                            ;; (when-not (state/logged?)
+                            ;;   (state/pub-event! [:after-db-restore repos]))
+                            ))
                          (p/catch (fn [error]
                                     (log/error :db/restore-failed error))))))]
     ;; clear this interval

+ 1 - 1
src/main/frontend/handler/config.cljs

@@ -6,7 +6,7 @@
 (defn set-config!
   [k v]
   (let [path (config/get-config-path)]
-    (file-handler/edn-file-set-key-value path k v state/set-config!)))
+    (file-handler/edn-file-set-key-value path k v)))
 
 (defn toggle-ui-show-brackets! []
   (let [show-brackets? (state/show-brackets?)]

+ 1 - 1
src/main/frontend/handler/editor.cljs

@@ -1658,7 +1658,7 @@
                       :data [block]}]
             (db/refresh! repo opts)))
         (when-let [block-node (util/get-first-block-by-id block-id)]
-          (.scrollIntoView block-node #js {:behavior "smooth" :block "center"}))))))
+          (.scrollIntoView block-node #js {:behavior "smooth" :block "nearest"}))))))
 
 ;; selections
 (defn on-tab

+ 29 - 1
src/main/frontend/handler/events.cljs

@@ -9,7 +9,12 @@
             [frontend.handler.notification :as notification]
             [frontend.components.encryption :as encryption]
             [frontend.fs.nfs :as nfs]
-            [frontend.handler.migrate :as migrate]))
+            [frontend.db.conn :as conn]
+            [frontend.handler.migrate :as migrate]
+            [frontend.db-schema :as db-schema]
+            [frontend.db :as db]
+            [datascript.core :as d]
+            ["semver" :as semver]))
 
 ;; TODO: should we move all events here?
 
@@ -47,6 +52,12 @@
     close-fn)))
 
 (defmethod handle :graph/added [[_ repo]]
+  ;; add ast/version to db
+  (let [conn (conn/get-conn repo false)
+        ast-version (d/datoms @conn :aevt :ast/version)]
+    (db/set-key-value repo :ast/version db-schema/ast-version))
+
+  ;; markdown convert notification
   (js/setTimeout
    (fn []
      (when (not (:markdown/version (state/get-config)))
@@ -85,6 +96,23 @@
   (when-let [repo (get-local-repo)]
     (state/set-modal! (ask-permission repo))))
 
+
+
+(defmethod handle :after-db-restore [[_ repos]]
+  (mapv (fn [{url :url} repo]
+          ;; compare :ast/version
+          (let [db (conn/get-conn url)
+                ast-version (:v (first (d/datoms db :aevt :ast/version)))]
+            (when (and (not= config/local-repo url)
+                       (or (nil? ast-version)
+                           (. semver lt ast-version db-schema/ast-version)))
+              (notification/show!
+               [:p.content
+                (util/format "DB-schema updated, Please re-index repo [%s]" url)]
+               :warning
+               false))))
+        repos))
+
 (defn run!
   []
   (let [chan (state/get-events-chan)]

+ 1 - 3
src/main/frontend/handler/file.cljs

@@ -301,7 +301,7 @@
         (reset-file! repo-url path default-content)))))
 
 (defn edn-file-set-key-value
-  [path k v ok-handler]
+  [path k v]
   (when-let [repo (state/get-current-repo)]
     (when-let [content (db/get-file-no-sub path)]
       (let [result (try
@@ -312,7 +312,5 @@
                        {}))
             ks (if (vector? k) k [k])
             new-result (rewrite/assoc-in result ks v)]
-        (when ok-handler (ok-handler repo new-result))
-        (state/set-config! repo new-result)
         (let [new-content (str new-result)]
           (set-file-content! repo path new-content))))))

+ 130 - 101
src/main/frontend/handler/graph.cljs

@@ -1,65 +1,75 @@
 (ns frontend.handler.graph
   (:require [frontend.db :as db]
             [clojure.string :as string]
-            [frontend.util :as util]
+            [frontend.util :as util :refer [profile]]
             [frontend.date :as date]
             [frontend.state :as state]
             [clojure.set :as set]
-            [medley.core :as medley]))
+            [medley.core :as medley]
+            [frontend.db.default :as default-db]
+            [frontend.text :as text]))
 
-(defn- build-edges
-  [edges]
+(defn- build-links
+  [links]
   (map (fn [[from to]]
          {:source from
           :target to})
-       edges))
+       links))
 
 (defn- get-connections
-  [page edges]
+  [page links]
   (count (filter (fn [{:keys [source target]}]
                    (or (= source page)
                        (= target page)))
-                 edges)))
+                 links)))
 
 (defn- build-nodes
-  [dark? current-page edges tags nodes]
-  (let [pages (->> (set (flatten nodes))
+  [dark? current-page page-links tags nodes namespaces]
+  (let [parents (set (map last namespaces))
+        current-page (or current-page "")
+        pages (->> (set (flatten nodes))
                    (remove nil?))]
     (->>
      (mapv (fn [p]
              (when p
                (let [p (str p)
                      current-page? (= p current-page)
-                     block? (and p (util/uuid-string? p))
-                     color (if block?
-                             "#1a6376"
-                             (case [dark? current-page?] ; FIXME: Put it into CSS
-                              [false false] "#222222"
-                              [false true]  "#045591"
-                              [true false]  "#8abbbb"
-                              [true true]   "#ffffff"))
-                     color (if (contains? tags (string/lower-case (str p)))
+                     color (case [dark? current-page?] ; FIXME: Put it into CSS
+                             [false false] "#999"
+                             [false true]  "#045591"
+                             [true false]  "#93a1a1"
+                             [true true]   "#ffffff")
+                     color (if (contains? tags p)
                              (if dark? "orange" "green")
                              color)]
-                 {:id p
-                  :name p
-                  :val (get-connections p edges)
-                  :autoColorBy "group"
-                  :group (js/Math.ceil (* (js/Math.random) 12))
-                  :color color})))
+                 (let [n (get page-links p 1)
+                       size-v (if (> n 2)
+                                (js/Math.cbrt n)
+                                n)
+                       size-v (if (< size-v 1)
+                                1
+                                (int size-v))
+                       size (* size-v 8)]
+                   (cond->
+                     {:id p
+                      :label p
+                      :size size
+                      :color color}
+                     (contains? parents p)
+                     (assoc :parent true))))))
            pages)
      (remove nil?))))
 
+;; slow
 (defn- uuid-or-asset?
   [id]
-  (let [id (str id)]
-    (or (util/uuid-string? id)
-       (string/starts-with? id "../assets/")
-       (= id "..")
-       (string/starts-with? id "assets/")
-       (string/ends-with? id ".gif")
-       (string/ends-with? id ".jpg")
-       (string/ends-with? id ".png"))))
+  (or (util/uuid-string? id)
+      (string/starts-with? id "../assets/")
+      (= id "..")
+      (string/starts-with? id "assets/")
+      (string/ends-with? id ".gif")
+      (string/ends-with? id ".jpg")
+      (string/ends-with? id ".png")))
 
 (defn- remove-uuids-and-files!
   [nodes]
@@ -68,73 +78,59 @@
    nodes))
 
 (defn- normalize-page-name
-  [{:keys [nodes links] :as g}]
-  (let [all-pages (->> (set (apply concat
-                                   [(map :id nodes)
-                                    (map :source links)
-                                    (map :target links)]))
-                       (map string/lower-case))
-        names (db/pull-many '[:block/name :block/original-name] (mapv (fn [page]
-                                                                        (if (util/uuid-string? page)
-                                                                          [:block/uuid (uuid page)]
-                                                                          [:block/name page])) all-pages))
-        names (zipmap (map (fn [x] (get x :block/name)) names)
-                      (map (fn [x]
-                             (get x :block/original-name (:block/name x))) names))
-        nodes (mapv (fn [node] (assoc node :id (get names (:id node) (:id node)))) nodes)
-        links (->>
-               links
-               (remove (fn [{:keys [source target]}]
-                         (or (nil? source) (nil? target))))
-               (mapv (fn [{:keys [source target]}]
-                       (when (and (not (uuid-or-asset? source))
-                                  (not (uuid-or-asset? target)))
-                         {:source (get names (string/lower-case source))
-                          :target (get names (string/lower-case target))})))
-               (remove nil?)
-               (remove (fn [{:keys [source target]}]
-                         (or (nil? source) (nil? target)))))
+  [{:keys [nodes links page-name->original-name]}]
+  (let [links (->>
+               (map
+                 (fn [{:keys [source target]}]
+                   (let [source (get page-name->original-name source)
+                         target (get page-name->original-name target)]
+                     (when (and source target)
+                       {:source source :target target})))
+                 links)
+               (remove nil?))
         nodes (->> (remove-uuids-and-files! nodes)
-                   (util/distinct-by #(string/lower-case (:id %))))]
+                   (util/distinct-by (fn [node] (:id node)))
+                   (map (fn [node]
+                          (if-let [original-name (get page-name->original-name (:id node))]
+                            (assoc node :id original-name :label original-name)
+                            nil)))
+                   (remove nil?))]
     {:nodes nodes
      :links links}))
 
 (defn build-global-graph
-  [theme show-journal?]
+  [theme {:keys [journal? orphan-pages? builtin-pages?] :as settings}]
   (let [dark? (= "dark" theme)
-        current-page (:block/name (db/get-current-page))]
+        current-page (or (:block/name (db/get-current-page)) "")]
     (when-let [repo (state/get-current-repo)]
-      (let [relation (db/get-pages-relation repo show-journal?)
+      (let [relation (db/get-pages-relation repo journal?)
             tagged-pages (db/get-all-tagged-pages repo)
+            namespaces (db/get-all-namespace-relation repo)
             tags (set (map second tagged-pages))
-            linked-pages (-> (concat
-                              relation
-                              tagged-pages)
-                             flatten
-                             set)
-            all-pages (db/get-pages repo)
-            other-pages (->> (remove linked-pages all-pages)
-                             (remove nil?))
-            other-pages (if show-journal? other-pages
-                            (remove date/valid-journal-title? other-pages))
-            other-pages (if (seq other-pages)
-                          (map string/lower-case other-pages)
-                          other-pages)
-            nodes (concat (seq relation)
+            full-pages (db/get-all-pages repo)
+            get-original-name (fn [p] (or (:block/original-name p) (:block/name p)))
+            all-pages (map get-original-name full-pages)
+            page-name->original-name (zipmap (map :block/name full-pages) all-pages)
+            pages-after-journal-filter (if-not journal?
+                                         (remove :block/journal? full-pages)
+                                         full-pages)
+            links (concat (seq relation)
                           (seq tagged-pages)
-                          (if (seq other-pages)
-                            (map (fn [page]
-                                   [page])
-                                 other-pages)
-                            []))
-            edges (build-edges (remove
-                                (fn [[_ to]]
-                                  (nil? to))
-                                nodes))
-            nodes (build-nodes dark? current-page edges tags nodes)]
+                          (seq namespaces))
+            linked (set (flatten links))
+            nodes (cond->> (map :block/name pages-after-journal-filter)
+                    (not builtin-pages?)
+                    (remove (fn [p] (default-db/built-in-pages-names (string/upper-case p))))
+                    (not orphan-pages?)
+                    (filter #(contains? linked (string/lower-case %))))
+            page-links (reduce (fn [m [k v]] (-> (update m k inc)
+                                                 (update v inc))) {} links)
+            links (build-links (remove (fn [[_ to]] (nil? to)) links))
+            nodes (build-nodes dark? (string/lower-case current-page) page-links tags nodes namespaces)]
         (normalize-page-name
          {:nodes nodes
-          :links edges})))))
+          :links links
+          :page-name->original-name page-name->original-name})))))
 
 (defn build-page-graph
   [page theme]
@@ -147,7 +143,9 @@
             tags (remove #(= page %) tags)
             ref-pages (db/get-page-referenced-pages repo page)
             mentioned-pages (db/get-pages-that-mentioned-page repo page)
-            edges (concat
+            namespaces (db/get-all-namespace-relation repo)
+            links (concat
+                   namespaces
                    (map (fn [[p aliases]]
                           [page p]) ref-pages)
                    (map (fn [[p aliases]]
@@ -159,7 +157,7 @@
                                      (map first mentioned-pages))
                              (remove nil?)
                              (set))
-            other-pages-edges (mapcat
+            other-pages-links (mapcat
                                (fn [page]
                                  (let [ref-pages (-> (map first (db/get-page-referenced-pages repo page))
                                                      (set)
@@ -171,21 +169,27 @@
                                     (map (fn [p] [page p]) ref-pages)
                                     (map (fn [p] [p page]) mentioned-pages))))
                                other-pages)
-            edges (->> (concat edges other-pages-edges)
+            links (->> (concat links other-pages-links)
                        (remove nil?)
                        (distinct)
-                       (build-edges))
+                       (build-links))
             nodes (->> (concat
                         [page]
                         (map first ref-pages)
                         (map first mentioned-pages)
                         tags)
                        (remove nil?)
-                       (distinct)
-                       (build-nodes dark? page edges (set tags)))]
+                       (distinct))
+            nodes (build-nodes dark? page links (set tags) nodes namespaces)
+            full-pages (db/get-all-pages repo)
+            get-original-name (fn [p] (or (:block/original-name p)
+                                         (:block/name p)))
+            all-pages (map get-original-name full-pages)
+            page-name->original-name (zipmap (map :block/name full-pages) all-pages)]
         (normalize-page-name
          {:nodes nodes
-          :links edges})))))
+          :links links
+          :page-name->original-name page-name->original-name})))))
 
 (defn build-block-graph
   "Builds a citation/reference graph for a given block uuid."
@@ -193,13 +197,15 @@
   (let [dark? (= "dark" theme)]
     (when-let [repo (state/get-current-repo)]
       (let [ref-blocks (db/get-block-referenced-blocks block)
-            edges (concat
+            namespaces (db/get-all-namespace-relation repo)
+            links (concat
                    (map (fn [[p aliases]]
-                          [block p]) ref-blocks))
+                          [block p]) ref-blocks)
+                   namespaces)
             other-blocks (->> (concat (map first ref-blocks))
                               (remove nil?)
                               (set))
-            other-blocks-edges (mapcat
+            other-blocks-links (mapcat
                                 (fn [block]
                                   (let [ref-blocks (-> (map first (db/get-block-referenced-blocks block))
                                                        (set)
@@ -207,17 +213,40 @@
                                     (concat
                                      (map (fn [p] [block p]) ref-blocks))))
                                 other-blocks)
-            edges (->> (concat edges other-blocks-edges)
+            links (->> (concat links other-blocks-links)
                        (remove nil?)
                        (distinct)
-                       (build-edges))
+                       (build-links))
             nodes (->> (concat
                         [block]
                         (map first ref-blocks))
                        (remove nil?)
                        (distinct)
                        ;; FIXME: get block tags
-                       (build-nodes dark? block edges #{}))]
+                       )
+            nodes (build-nodes dark? block links #{} nodes namespaces)]
         (normalize-page-name
          {:nodes nodes
-          :links edges})))))
+          :links links})))))
+
+(defn n-hops
+  "Get all nodes that are n hops from nodes (a collection of node ids)"
+  [{:keys [links] :as graph} nodes level]
+  (let [search-nodes (fn [forward?]
+                       (let [links (group-by (if forward? :source :target) links)]
+                         (loop [nodes nodes
+                               level level]
+                          (if (zero? level)
+                            nodes
+                            (recur (distinct (apply concat nodes
+                                               (map
+                                                 (fn [id]
+                                                   (->> (get links id) (map (if forward? :target :source))))
+                                                 nodes)))
+                                   (dec level))))))
+        nodes (concat (search-nodes true) (search-nodes false))
+        nodes (set nodes)]
+    (update graph :nodes
+            (fn [full-nodes]
+              (filter (fn [node] (contains? nodes (:id node)))
+                      full-nodes)))))

+ 2 - 4
src/main/frontend/handler/search.cljs

@@ -34,10 +34,8 @@
   ([]
    (clear-search! true))
   ([clear-search-mode?]
-   (let [m (cond-> {:search/result nil
-                    :search/q ""}
-             clear-search-mode?
-             (assoc :search/mode :global))]
+   (let [m {:search/result nil
+            :search/q ""}]
      (swap! state/state merge m))
    (when-let [input (gdom/getElement "search-field")]
      (gobj/set input "value" ""))))

+ 11 - 0
src/main/frontend/handler/ui.cljs

@@ -147,6 +147,17 @@
       (on-chosen (nth matched @current-idx) false)
       (and on-enter (on-enter state)))))
 
+(defn auto-complete-shift-complete
+  [state e]
+  (let [[matched {:keys [on-chosen on-shift-chosen on-enter]}] (:rum/args state)
+        current-idx (get state :frontend.ui/current-idx)]
+    (util/stop e)
+    (if (and (seq matched)
+             (> (count matched)
+                @current-idx))
+      ((or on-shift-chosen on-chosen) (nth matched @current-idx) false)
+      (and on-enter (on-enter state)))))
+
 ;; date-picker
 ;; TODO: find a better way
 (def *internal-model (rum/cursor state/state :date-picker/date))

+ 7 - 2
src/main/frontend/modules/shortcut/config.cljs

@@ -47,7 +47,11 @@
     :auto-complete/complete
     {:desc    "Auto-complete choose selected item"
      :binding "enter"
-     :fn      ui-handler/auto-complete-complete}}
+     :fn      ui-handler/auto-complete-complete}
+    :auto-complete/shift-complete
+    {:desc    "Auto-complete open selected item in sidebar"
+     :binding "shift+enter"
+     :fn      ui-handler/auto-complete-shift-complete}}
 
    :shortcut.handler/block-editing-only
    ^{:before m/enable-when-editing-mode!}
@@ -255,7 +259,7 @@
     :go/search
     {:desc    "Full text search"
      :binding "mod+u"
-     :fn      route-handler/go-to-search!}
+     :fn      #(route-handler/go-to-search! nil)}
     :go/journals
     {:desc    "Jump to journals"
      :binding (if mac? "mod+j" "alt+j")
@@ -412,6 +416,7 @@
     :auto-complete/prev
     :auto-complete/next
     :auto-complete/complete
+    :auto-complete/shift-complete
     :date-picker/prev-day
     :date-picker/next-day
     :date-picker/prev-week

+ 3 - 2
src/main/frontend/modules/shortcut/data_helper.cljs

@@ -7,7 +7,8 @@
             [frontend.modules.shortcut.config :as config]
             [frontend.state :as state]
             [frontend.util :as util]
-            [lambdaisland.glogi :as log])
+            [lambdaisland.glogi :as log]
+            [frontend.handler.common :as common-handler])
   (:import [goog.ui KeyboardShortcutHandler]))
 (defonce default-binding
   (->> (vals config/default-config)
@@ -130,8 +131,8 @@
                         result
                         :shortcuts
                         #(dissoc (rewrite/sexpr %) k))]
-        (state/set-config! repo new-result)
         (let [new-content (str new-result)]
+          (common-handler/reset-config! repo new-content)
           (file/set-file-content! repo path new-content))))))
 
 (defn get-group

+ 46 - 0
src/main/frontend/modules/zotero/api.cljs

@@ -0,0 +1,46 @@
+(ns frontend.modules.zotero.api
+  (:require [cljs-http.client :as http]
+            [cljs.core.async :refer [go <!]]
+            [camel-snake-kebab.core :as csk]
+            [camel-snake-kebab.extras :as cske]
+            [frontend.util :as util]))
+
+(def ^:dynamic *debug* true)
+
+(def config {:api-version 3
+             :base        "https://api.zotero.org"
+             :api-key     "api_key"
+             :type        :user
+             :type-id     8234867})
+
+;; "/users/475425/collections?v=3"
+(defn get*
+  ([config api]
+   (get* config api nil))
+  ([config api query-params]
+   (go (let [{:keys [api-version base type type-id api-key]} config
+             {:keys [status body] :as response}
+             (<! (http/get (str base
+                                (if (= type :user)
+                                  "/users/"
+                                  "/groups/")
+                                type-id
+                                api)
+                           {:with-credentials? false
+                            :headers {"Zotero-API-Key" api-key
+                                      "Zotero-API-Version" api-version}
+                            :query-params (cske/transform-keys csk/->camelCaseString
+                                                               query-params)}))]
+         (if (http/unexceptional-status? status)
+           (let [result (cske/transform-keys csk/->kebab-case-keyword body)]
+             (when *debug*
+               (def rr result)
+               (println result))
+             result)
+           (throw (ex-info "Http error"
+                           {:response response})))))))
+
+
+(comment
+  (get* config "/collections" {:limit 1})
+  (get* config "/items" {:limit 3}))

+ 1 - 0
src/main/frontend/modules/zotero/core.cljs

@@ -0,0 +1 @@
+(ns frontend.modules.zotero.core)

+ 5 - 3
src/main/frontend/page.cljs

@@ -17,7 +17,9 @@
                    (ui/inject-document-devices-envs!)
                    (ui/inject-dynamic-style-node!)
                    (plugin-handler/host-mounted!)
-                   (let [teardown-fn (comp (ui/setup-patch-ios-fixed-bottom-position!))]
+                   (let [td-fns [(ui/setup-active-keystroke!)
+                                 (ui/setup-active-keystroke!)]
+                         teardown-fn #(mapv (fn [f] (f)) td-fns)]
                      (assoc state ::teardown teardown-fn)))
    :will-unmount (fn [state]
                    (let [teardown (::teardown state)]
@@ -32,7 +34,7 @@
            (view route-match)
            (sidebar/sidebar
             route-match
-            (view route-match)))
+            (view route-match))))))))
 
         ;; FIXME: disable for now
         ;; (let [route-name (get-in route-match [:data :name])
@@ -51,4 +53,4 @@
         ;;           :timeout {:enter 300
         ;;                     :exit 200}}
         ;;          (route-view view route-match)))))))
-         )))))
+

+ 18 - 12
src/main/frontend/rum.cljs

@@ -1,7 +1,8 @@
 (ns frontend.rum
   (:require [clojure.string :as s]
             [clojure.set :as set]
-            [clojure.walk :as w]))
+            [clojure.walk :as w]
+            [cljs-bean.core :as bean]))
 
 ;; copy from https://github.com/priornix/antizer/blob/35ba264cf48b84e6597743e28b3570d8aa473e74/src/antizer/core.cljs
 
@@ -31,29 +32,34 @@
                 data)))
 
 ;; adapted from https://github.com/tonsky/rum/issues/20
-(defn adapt-class [react-class]
-  (fn [& args]
+(defn adapt-class
+  ([react-class]
+   (adapt-class react-class false))
+  ([react-class skip-opts-transform?]
+   (fn [& args]
     (let [[opts children] (if (map? (first args))
                             [(first args) (rest args)]
                             [{} args])
           type# (first children)
-             ;; we have to make sure to check if the children is sequential
-             ;; as a list can be returned, eg: from a (for)
+          ;; we have to make sure to check if the children is sequential
+          ;; as a list can be returned, eg: from a (for)
           new-children (if (sequential? type#)
                          (let [result (daiquiri.interpreter/interpret children)]
                            (if (sequential? result)
                              result
                              [result]))
                          children)
-             ;; convert any options key value to a react element, if
-             ;; a valid html element tag is used, using sablono
+          ;; convert any options key value to a react element, if
+          ;; a valid html element tag is used, using sablono
           vector->react-elems (fn [[key val]]
                                 (if (sequential? val)
                                   [key (daiquiri.interpreter/interpret val)]
                                   [key val]))
-          new-options (into {} (map vector->react-elems opts))]
-         ;; (.dir js/console new-children)
+          new-options (into {}
+                            (if skip-opts-transform?
+                              opts
+                              (map vector->react-elems opts)))]
       (apply js/React.createElement react-class
-           ;; sablono html-to-dom-attrs does not work for nested hashmaps
-             (clj->js (map-keys->camel-case new-options :html-props true))
-             new-children))))
+        ;; sablono html-to-dom-attrs does not work for nested hashmaps
+        (bean/->js (map-keys->camel-case new-options :html-props true))
+        new-children)))))

+ 34 - 6
src/main/frontend/state.cljs

@@ -47,6 +47,7 @@
       :search/q ""
       :search/mode :global
       :search/result nil
+      :search/graph-filters []
 
       ;; modals
       :modal/show? false
@@ -142,6 +143,17 @@
 
       :view/components {}})))
 
+
+(defn sub
+  [ks]
+  (if (coll? ks)
+    (util/react (rum/cursor-in state ks))
+    (util/react (rum/cursor state ks))))
+
+(defn sub-current-route
+  []
+  (get-in (sub :route-match) [:data :name]))
+
 (defn get-route-match
   []
   (:route-match @state))
@@ -164,12 +176,6 @@
   []
   (get-in (get-route-match) [:query-params :p]))
 
-(defn sub
-  [ks]
-  (if (coll? ks)
-    (util/react (rum/cursor-in state ks))
-    (util/react (rum/cursor state ks))))
-
 (defn set-state!
   [path value]
   (if (vector? path)
@@ -260,6 +266,10 @@
   ;; Disable block timestamps for now, because it doesn't work with undo/redo
   false)
 
+(defn sub-graph-config
+  []
+  (:graph/settings (get (sub-config) (get-current-repo))))
+
 ;; Enable by default
 (defn show-brackets?
   []
@@ -1232,6 +1242,24 @@
   []
   (set-search-result! nil))
 
+(defn add-graph-search-filter!
+  [q]
+  (when-not (string/blank? q)
+    (update-state! :search/graph-filters
+                  (fn [value]
+                    (vec (distinct (conj value q)))))))
+
+(defn remove-search-filter!
+  [q]
+  (when-not (string/blank? q)
+    (update-state! :search/graph-filters
+                   (fn [value]
+                     (remove #{q} value)))))
+
+(defn clear-search-filters!
+  []
+  (set-state! :search/graph-filters []))
+
 (defn get-search-mode
   []
   (:search/mode @state))

+ 60 - 19
src/main/frontend/ui.cljs

@@ -18,7 +18,8 @@
             [frontend.ui.date-picker]
             [frontend.context.i18n :as i18n]
             [frontend.modules.shortcut.core :as shortcut]
-            [lambdaisland.glogi :as log]))
+            [lambdaisland.glogi :as log]
+            [frontend.config :as config]))
 
 (defonce transition-group (r/adapt-class TransitionGroup))
 (defonce css-transition (r/adapt-class CSSTransition))
@@ -292,6 +293,25 @@
     (state/sync-system-theme!)
     #(.removeEventListener schemaMedia "change" state/sync-system-theme!)))
 
+(defn setup-active-keystroke! []
+  (let [active-keystroke (atom #{})
+        handle-global-keystroke
+        (fn [down? e]
+          (let [handler (if down? conj disj)
+                keystroke e.key]
+            (swap! active-keystroke handler keystroke))
+          (.setAttribute
+            js/document.body
+            "data-active-keystroke"
+            (apply str (interpose "+" (vec @active-keystroke)))))
+        keydown-handler (partial handle-global-keystroke true)
+        keyup-handler (partial handle-global-keystroke false)]
+    (.addEventListener js/window "keydown" keydown-handler)
+    (.addEventListener js/window "keyup" keyup-handler)
+    (fn []
+      (.removeEventListener js/window "keydown" keydown-handler)
+      (.removeEventListener js/window "keyup" keyup-handler))))
+
 (defn on-scroll
   [node on-load on-top-reached]
   (let [full-height (gobj/get node "scrollHeight")
@@ -349,16 +369,17 @@
            {:key idx}
            (let [item-cp
                  [:div {:key idx}
-                  (menu-link
-                   {:id       (str "ac-" idx)
-                    :class    (when (= @current-idx idx)
-                                "chosen")
-                    :on-mouse-down (fn [e]
-                                     (util/stop e)
-                                     (if (and (gobj/get e "shiftKey") on-shift-chosen)
-                                       (on-shift-chosen item)
-                                       (on-chosen item)))}
-                   (if item-render (item-render item) item))]]
+                  (let [chosen? (= @current-idx idx)]
+                    (menu-link
+                      {:id            (str "ac-" idx)
+                       :class         (when chosen? "chosen")
+                       :on-mouse-enter #(reset! current-idx idx)
+                       :on-mouse-down (fn [e]
+                                        (util/stop e)
+                                        (if (and (gobj/get e "shiftKey") on-shift-chosen)
+                                          (on-shift-chosen item)
+                                          (on-chosen item)))}
+                      (if item-render (item-render item chosen?) item)))]]
 
              (if get-group-name
                (if-let [group-name (get-group-name item)]
@@ -588,14 +609,15 @@
   (when error
     (js/console.error error)
     (log/error :ui/catch-error error))
-  (if (some? error)
+  (if (and (not config/dev?) (some? error))
     error-view
     view))
 
 (rum/defc select
-  [options on-change]
-  [:select.mt-1.form-select.block.w-full.px-3.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5.ml-4
-   {:style     {:padding "0 0 0 12px"}
+  [options on-change class]
+  [:select.mt-1.block.px-3.text-base.leading-6.border-gray-300.focus:outline-none.focus:shadow-outline-blue.focus:border-blue-300.sm:text-sm.sm:leading-5.ml-4
+   {:class     (or class "form-select")
+    :style     {:padding "0 0 0 12px"}
     :on-change (fn [e]
                  (let [value (util/evalue e)]
                    (on-change value)))}
@@ -609,23 +631,42 @@
 
 (rum/defcs tippy < rum/static
   (rum/local false ::mounted?)
-  [state opts child]
+  [state {:keys [fixed-position? open?] :as opts} child]
   (let [*mounted? (::mounted? state)
-        mounted? @*mounted?]
+        mounted? @*mounted?
+        manual (not= open? nil)]
     (Tippy (->
             (merge {:arrow true
                     :sticky true
                     :theme "customized"
                     :disabled (not (state/enable-tooltip?))
                     :unmountHTMLWhenHide true
-                    :open @*mounted?
+                    :open (if manual open? @*mounted?)
+                    :trigger (if manual "manual" "mouseenter focus")
+                    ;; See https://github.com/tvkhoa/react-tippy/issues/13
+                    :popperOptions (if fixed-position?
+                                      {:modifiers {:flip {:enabled false}
+                                                   :hide {:enabled false}
+                                                   :preventOverflow {:enabled false}}}
+                                      {})
                     :onShow #(reset! *mounted? true)
                     :onHide #(reset! *mounted? false)}
                    opts)
-            (assoc :html (if mounted?
+            (assoc :html (if (or open? mounted?)
                            (when-let [html (:html opts)]
                              (if (fn? html)
                                (html)
                                html))
                            [:div {:key "tippy"} ""])))
            child)))
+
+(defn slider
+  [default-value {:keys [min max on-change]}]
+  [:input.cursor-pointer
+   {:type  "range"
+    :value (int default-value)
+    :min   min
+    :max   max
+    :style {:width "100%"}
+    :on-change #(let [value (util/evalue %)]
+                  (on-change value))}])

+ 1 - 1
src/main/frontend/version.cljs

@@ -1,3 +1,3 @@
 (ns frontend.version)
 
-(defonce version "0.2.2-1")
+(defonce version "0.2.2-2")

+ 59 - 1
src/workspaces/workspaces/cards.cljs

@@ -5,7 +5,10 @@
             [nubank.workspaces.card-types.test :as ct.test]
             [cljs.test :refer [is async]]
             [rum.core :as rum]
-            [frontend.ui :as ui]))
+            [frontend.ui :as ui]
+            [frontend.extensions.graph :as graph]
+            [frontend.extensions.graph.pixi :as pixi]
+            [cljs-bean.core :as bean]))
 
 ;; simple function to create react elemnents
 (defn element [name props & children]
@@ -24,3 +27,58 @@
 (ws/defcard button-card
   (ct.react/react-card
    (ui-button)))
+
+(rum/defc graph
+  []
+  (graph/graph-2d
+   {:data {:nodes [{:id "a" :label "a"} {:id "b" :label "b"}]
+           :edges [{:source "a" :target "b"}]}
+    :width 150
+    :height 150
+    :fitView true}))
+
+(ws/defcard graph-card
+  (ct.react/react-card
+   (graph)))
+
+(defn- random-graph
+  [n]
+  (let [nodes (for [i (range 0 n)]
+                {:id (str i)
+                 :label (str i)})
+        edges (->
+               (for [i (range 0 (/ n 2))]
+                 (let [source i
+                       target (inc i)]
+                   {:id (str source target)
+                    :source (str source)
+                    :target (str target)}))
+               (distinct))]
+    {:nodes nodes
+     :links edges}))
+
+;; (rum/defc pixi-graph
+;;   []
+;;   (let [{:keys [nodes links]} (random-graph 4000)]
+;;     (pixi/graph (fn []
+;;                   {:nodes nodes
+;;                   :links links
+;;                   :style {:node {:size 15
+;;                                  :color "#666666"
+;;                                  :border {:width 2
+;;                                           :color "#ffffff"}
+;;                                  :label {:content (fn [node] (.-id node))
+;;                                          :type js/window.PixiGraph.TextType.TEXT
+;;                                          :fontSize 12
+;;                                          :color "#333333"
+;;                                          :backgroundColor "rgba(255, 255, 255, 0.5)"
+;;                                          :padding 4}}
+;;                           :edge {:width 1
+;;                                  :color "#cccccc"}}
+;;                   :hover-style {:node {:border {:color "#000000"}
+;;                                        :label {:backgroundColor "rgba(238, 238, 238, 1)"}}
+;;                                 :edge {:color "#999999"}}}))))
+
+;; (ws/defcard pixi-graph-card
+;;   (ct.react/react-card
+;;    (pixi-graph)))

+ 1 - 0
tailwind.config.js

@@ -1,6 +1,7 @@
 const colors = require('tailwindcss/colors')
 
 module.exports = {
+  mode: 'jit',
   purge: [
     './src/**/*.js',
     './src/**/*.cljs',

+ 3 - 10
templates/tutorial-en.md

@@ -19,17 +19,10 @@ some changes on the right sidebar, those referenced blocks will be changed too!
 - 4. Do you support tasks like todo/doing/done and priorities?
     - Yes, type `/` and pick your favorite todo keyword or priority (A/B/C).
     - NOW [#A] A dummy tutorial on "How to take dummy notes?"
-    - LATER [#A] Check out this awesome video by [:a {:href "https://twitter.com/EdTravelling" :target "_blank"} "@EdTravelling"], which shows how to use logseq to open your local directory.
+    - LATER [#A] Check out this awesome video by [:a {:href "https://twitter.com/TechWithEd" :target "_blank"} "@TechWithEd"], which shows how to use logseq to open your local directory.
+
+    {{tutorial-video}}
 
-[:div.video-wrapper.mb-4
-        [:iframe
-         {:allowFullScreen "allowfullscreen"
-          :allow
-          "accelerometer; autoplay; encrypted-media; gyroscope"
-        :frameBorder "0"
-        :src "https://www.youtube.com/embed/Afmqowr0qEQ"
-        :height "367"
-        :width "653"}]]
     - DONE Create a page
     - CANCELED [#C] Write a page with more than 1000 blocks
 - That's it! You can create more bullets or open a local directory to import some notes now!

تفاوت فایلی نمایش داده نمی شود زیرا این فایل بسیار بزرگ است
+ 484 - 90
yarn.lock


برخی فایل ها در این مقایسه diff نمایش داده نمی شوند زیرا تعداد فایل ها بسیار زیاد است