refactor: remove web

This commit is contained in:
Tienson Qin
2020-10-27 23:19:40 +08:00
parent 3d1c809020
commit 2d1fa33204
118 changed files with 0 additions and 2 deletions

View File

@@ -0,0 +1,511 @@
(ns frontend.handler.dnd
(:require [frontend.handler.notification :as notification]
[frontend.handler.repo :as repo-handler]
[frontend.config :as config]
[frontend.util :as util :refer-macros [profile]]
[frontend.db :as db]
[clojure.walk :as walk]
[clojure.string :as string]
[frontend.utf8 :as utf8]
[cljs-time.coerce :as tc]
[cljs-time.core :as t]))
(defn- remove-block-child!
[target-block parent-block]
(let [child-ids (set (db/get-block-ids target-block))]
(db/get-block-content-rec
parent-block
(fn [{:block/keys [uuid level content]}]
(if (contains? child-ids uuid)
""
content)))))
(defn- recompute-block-level
[to-block nested?]
(+ (:block/level to-block)
(if nested? 1 0)))
(defn- recompute-block-content-and-changes
[target-block to-block nested? same-repo? same-file?]
(let [new-level (recompute-block-level to-block nested?)
target-level (:block/level target-block)
format (:block/format target-block)
pattern (config/get-block-pattern format)
block-changes (atom [])
all-content (db/get-block-content-rec
target-block
(fn [{:block/keys [uuid level content]
:as block}]
(let [new-level (+ new-level (- level target-level))
new-content (string/replace-first content
(apply str (repeat level pattern))
(apply str (repeat new-level pattern)))
block (cond->
{:block/uuid uuid
:block/level new-level
:block/content new-content
:block/page (:block/page to-block)}
(not same-repo?)
(merge (dissoc block [:block/level :block/content]))
(not same-file?)
(merge {:block/page (:block/page to-block)
:block/file (:block/file to-block)}))]
(swap! block-changes conj block)
new-content)))]
[all-content @block-changes]))
(defn- move-parent-to-child?
[target-block to-block]
(let [to-block-id (:block/uuid to-block)
result (atom false)
_ (walk/postwalk
(fn [form]
(when (map? form)
(when-let [id (:block/uuid form)]
(when (= id to-block-id)
(reset! result true))))
form)
target-block)]
@result))
(defn- compute-target-child?
[target-block to-block]
(let [target-block-id (:block/uuid target-block)
result (atom false)
_ (walk/postwalk
(fn [form]
(when (map? form)
(when-let [id (:block/uuid form)]
(when (= id target-block-id)
(reset! result true))))
form)
to-block)]
@result))
(defn rebuild-dnd-blocks
[repo file target-child? start-pos target-blocks offset-block-uuid {:keys [delete? same-file?]
:or {delete? false
same-file? true}}]
(when (seq target-blocks)
(let [file-id (:db/id file)
target-block-ids (set (map :block/uuid target-blocks))
after-blocks (->> (db/get-file-after-blocks repo file-id start-pos)
(remove (fn [h] (contains? target-block-ids (:block/uuid h)))))
after-blocks (cond
delete?
after-blocks
(and offset-block-uuid
(not (contains? (set (map :block/uuid after-blocks)) offset-block-uuid)))
(concat target-blocks after-blocks)
offset-block-uuid
(let [[before after] (split-with (fn [h] (not= (:block/uuid h)
offset-block-uuid)) after-blocks)]
(concat (conj (vec before) (first after))
target-blocks
(rest after)))
:else
(concat target-blocks after-blocks))
after-blocks (remove nil? after-blocks)
;; _ (prn {:start-pos start-pos
;; :target-blocks target-blocks
;; :after-blocks (map (fn [block]
;; (:block/content block))
;; after-blocks)})
last-start-pos (atom start-pos)
result (mapv
(fn [{:block/keys [uuid meta content level page] :as block}]
(let [content (str (util/trim-safe content) "\n")
target-block? (contains? target-block-ids uuid)
content-length (if target-block?
(utf8/length (utf8/encode content))
(- (:end-pos meta) (:start-pos meta)))
new-end-pos (+ @last-start-pos content-length)
new-meta {:start-pos @last-start-pos
:end-pos new-end-pos}]
(reset! last-start-pos new-end-pos)
(let [data {:block/uuid uuid
:block/meta new-meta}]
(cond
(and target-block? (not same-file?))
(merge
(dissoc block :block/idx :block/dummy?)
data)
target-block?
(merge
data
{:block/level level
:block/content content
:block/page page})
:else
data))))
after-blocks)]
result)))
(defn- get-start-pos
[block]
(get-in block [:block/meta :start-pos]))
(defn- get-end-pos
[block]
(get-in block [:block/meta :end-pos]))
(defn- compute-direction
[target-block top-block nested? top? target-child?]
(cond
(= top-block target-block)
:down
(and target-child? nested?)
:up
(and target-child? (not top?))
:down
:else
:up))
(defn- compute-after-blocks-in-same-file
[repo target-block to-block direction top? nested? target-child? target-file original-top-block-start-pos block-changes]
(cond
top?
(rebuild-dnd-blocks repo target-file target-child?
original-top-block-start-pos
block-changes
nil
{})
(= direction :up)
(let [offset-block-id (if nested?
(:block/uuid to-block)
(last (db/get-block-ids to-block)))
offset-end-pos (get-end-pos
(db/entity repo [:block/uuid offset-block-id]))]
(rebuild-dnd-blocks repo target-file target-child?
offset-end-pos
block-changes
nil
{}))
(= direction :down)
(let [offset-block-id (if nested?
(:block/uuid to-block)
(last (db/get-block-ids to-block)))
target-start-pos (get-start-pos target-block)]
(rebuild-dnd-blocks repo target-file target-child?
target-start-pos
block-changes
offset-block-id
{}))))
;; TODO: still could be different pages, e.g. move a block from one journal to another journal
(defn- move-block-in-same-file
[repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes]
(if (move-parent-to-child? target-block to-block)
nil
(let [old-file-content (db/get-file (:file/path (db/entity (:db/id (:block/file target-block)))))
old-file-content (utf8/encode old-file-content)
subs (fn [start-pos end-pos] (utf8/substring old-file-content start-pos end-pos))
bottom-content (db/get-block-content-rec bottom-block)
top-content (remove-block-child! bottom-block top-block)
top-area (subs 0 (get-start-pos top-block))
bottom-area (subs
(cond
(and nested? (= direction :down))
(get-end-pos bottom-block)
target-child?
(db/get-block-end-pos-rec repo top-block)
:else
(db/get-block-end-pos-rec repo bottom-block))
nil)
between-area (if (= direction :down)
(subs (db/get-block-end-pos-rec repo target-block) (get-start-pos to-block))
(subs (db/get-block-end-pos-rec repo to-block) (get-start-pos target-block)))
up-content (when (= direction :up)
(cond
nested?
(util/join-newline (:block/content top-block)
target-content
(if target-child?
(remove-block-child! target-block (:block/children to-block))
(db/get-block-content-rec (:block/children top-block))))
(and top? target-child?)
(util/join-newline target-content (remove-block-child! target-block to-block))
top?
(util/join-newline target-content top-content)
:else
(let [top-content (if target-child?
(remove-block-child! target-block to-block)
top-content)]
(util/join-newline top-content target-content))))
down-content (when (= direction :down)
(cond
nested?
(util/join-newline (:block/content bottom-block)
target-content)
target-child?
(util/join-newline top-content target-content)
:else
(util/join-newline bottom-content target-content)))
;; _ (prn {:direction direction
;; :nested? nested?
;; :top? top?
;; :target-child? target-child?
;; :top-area top-area
;; :up-content up-content
;; :between-area between-area
;; :down-content down-content
;; :bottom-area bottom-area
;; })
new-file-content (string/trim
(util/join-newline
top-area
up-content
between-area
down-content
bottom-area))
after-blocks (->> (compute-after-blocks-in-same-file repo target-block to-block direction top? nested? target-child? target-file original-top-block-start-pos block-changes)
(remove nil?))
path (:file/path (db/entity repo (:db/id (:block/file to-block))))
modified-time (let [modified-at (tc/to-long (t/now))]
(->
[[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
[:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
[:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]
[:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]
distinct
vec))]
(profile
"Move block in the same file: "
(repo-handler/transact-react-and-alter-file!
repo
(concat
after-blocks
modified-time)
{:key :block/change
:data block-changes}
[[path new-file-content]]))
;; (alter-file repo
;; path
;; new-file-content
;; {:re-render-root? true})
)))
(defn- move-block-in-different-files
[repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes]
(let [target-file (db/entity repo (:db/id (:block/file target-block)))
target-file-path (:file/path target-file)
target-file-content (db/get-file repo target-file-path)
to-file (db/entity repo (:db/id (:block/file to-block)))
to-file-path (:file/path to-file)
target-block-end-pos (db/get-block-end-pos-rec repo target-block)
to-block-start-pos (get-start-pos to-block)
to-block-end-pos (db/get-block-end-pos-rec repo to-block)
new-target-file-content (utf8/delete! target-file-content
(get-start-pos target-block)
target-block-end-pos)
to-file-content (utf8/encode (db/get-file repo to-file-path))
new-to-file-content (let [separate-pos (cond nested?
(get-end-pos to-block)
top?
to-block-start-pos
:else
to-block-end-pos)]
(string/trim
(util/join-newline
(utf8/substring to-file-content 0 separate-pos)
target-content
(utf8/substring to-file-content separate-pos))))
modified-time (let [modified-at (tc/to-long (t/now))]
(->
[[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
[:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
[:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]
[:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]
distinct
vec))
target-after-blocks (rebuild-dnd-blocks repo target-file target-child?
(get-start-pos target-block)
block-changes nil {:delete? true})
to-after-blocks (cond
top?
(rebuild-dnd-blocks repo to-file target-child?
(get-start-pos to-block)
block-changes
nil
{:same-file? false})
:else
(let [offset-block-id (if nested?
(:block/uuid to-block)
(last (db/get-block-ids to-block)))
offset-end-pos (get-end-pos
(db/entity repo [:block/uuid offset-block-id]))]
(rebuild-dnd-blocks repo to-file target-child?
offset-end-pos
block-changes
nil
{:same-file? false})))]
(profile
"Move block between different files: "
(repo-handler/transact-react-and-alter-file!
repo
(concat
target-after-blocks
to-after-blocks
modified-time)
{:key :block/change
:data (conj block-changes target-block)}
[[target-file-path new-target-file-content]
[to-file-path new-to-file-content]]))))
(defn- move-block-in-different-repos
[target-block-repo to-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes]
(let [target-file (db/entity target-block-repo (:db/id (:block/file target-block)))
target-file-path (:file/path target-file)
target-file-content (db/get-file target-block-repo target-file-path)
to-file (db/entity to-block-repo (:db/id (:block/file to-block)))
to-file-path (:file/path to-file)
target-block-end-pos (db/get-block-end-pos-rec target-block-repo target-block)
to-block-start-pos (get-start-pos to-block)
to-block-end-pos (db/get-block-end-pos-rec to-block-repo to-block)
new-target-file-content (utf8/delete! target-file-content
(get-start-pos target-block)
target-block-end-pos)
to-file-content (utf8/encode (db/get-file to-block-repo to-file-path))
new-to-file-content (let [separate-pos (cond nested?
(get-end-pos to-block)
top?
to-block-start-pos
:else
to-block-end-pos)]
(string/trim
(util/join-newline
(utf8/substring to-file-content 0 separate-pos)
target-content
(utf8/substring to-file-content separate-pos))))
target-delete-tx (map (fn [id]
[:db.fn/retractEntity [:block/uuid id]])
(db/get-block-ids target-block))
[target-modified-time to-modified-time]
(let [modified-at (tc/to-long (t/now))]
[[[:db/add (:db/id (:block/page target-block)) :page/last-modified-at modified-at]
[:db/add (:db/id (:block/file target-block)) :file/last-modified-at modified-at]]
[[:db/add (:db/id (:block/page to-block)) :page/last-modified-at modified-at]
[:db/add (:db/id (:block/file to-block)) :file/last-modified-at modified-at]]])
target-after-blocks (rebuild-dnd-blocks target-block-repo target-file target-child?
(get-start-pos target-block)
block-changes nil {:delete? true})
to-after-blocks (cond
top?
(rebuild-dnd-blocks to-block-repo to-file target-child?
(get-start-pos to-block)
block-changes
nil
{:same-file? false})
:else
(let [offset-block-id (if nested?
(:block/uuid to-block)
(last (db/get-block-ids to-block)))
offset-end-pos (get-end-pos
(db/entity to-block-repo [:block/uuid offset-block-id]))]
(rebuild-dnd-blocks to-block-repo to-file target-child?
offset-end-pos
block-changes
nil
{:same-file? false})))]
(profile
"[Target file] Move block between different files: "
(repo-handler/transact-react-and-alter-file!
target-block-repo
(concat
target-delete-tx
target-after-blocks
target-modified-time)
{:key :block/change
:data [(dissoc target-block :block/children)]}
[[target-file-path new-target-file-content]]))
(profile
"[Destination file] Move block between different files: "
(repo-handler/transact-react-and-alter-file!
to-block-repo
(concat
to-after-blocks
to-modified-time)
{:key :block/change
:data [block-changes]}
[[to-file-path new-to-file-content]]))))
(defn move-block
"There can be at least 3 possible situations:
1. Move a block in the same file (either top-to-bottom or bottom-to-top).
2. Move a block between two different files.
3. Move a block between two files in different repos.
Notes:
1. Those two blocks might have different formats, e.g. one is `org` and another is `markdown`,
we don't handle this now. TODO: transform between different formats in mldoc.
2. Sometimes we might need to move a parent block to it's own child.
"
[target-block to-block target-dom-id top? nested?]
(when (and target-block to-block (:block/format target-block) (:block/format to-block))
(cond
(not= (:block/format target-block)
(:block/format to-block))
(notification/show!
(util/format "Sorry, you can't move a block of format %s to another file of format %s."
(:block/format target-block)
(:block/format to-block))
:error)
(= (:block/uuid target-block) (:block/uuid to-block))
nil
:else
(let [pattern (config/get-block-pattern (:block/format to-block))
target-block-repo (:block/repo target-block)
to-block-repo (:block/repo to-block)
target-block (assoc target-block
:block/meta
(:block/meta (db/entity target-block-repo [:block/uuid (:block/uuid target-block)])))
to-block (assoc to-block
:block/meta
(:block/meta (db/entity [:block/uuid (:block/uuid to-block)])))
same-repo? (= target-block-repo to-block-repo)
target-file (:block/file target-block)
same-file? (and
same-repo?
(= (:db/id target-file)
(:db/id (:block/file to-block))))
[top-block bottom-block] (if same-file?
(if (< (get-start-pos target-block)
(get-start-pos to-block))
[target-block to-block]
[to-block target-block])
[nil nil])
target-child? (compute-target-child? target-block to-block)
direction (compute-direction target-block top-block nested? top? target-child?)
original-top-block-start-pos (get-start-pos top-block)
[target-content block-changes] (recompute-block-content-and-changes target-block to-block nested? same-repo? same-file?)]
(cond
same-file?
(move-block-in-same-file target-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes)
;; same repo but different files
same-repo?
(move-block-in-different-files target-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes)
;; different repos
:else
(move-block-in-different-repos target-block-repo to-block-repo target-block to-block top-block bottom-block nested? top? target-child? direction target-content target-file original-top-block-start-pos block-changes))))))

View File

@@ -0,0 +1,142 @@
(ns frontend.handler.draw
(:refer-clojure :exclude [load-file])
(:require [frontend.util :as util :refer-macros [profile]]
[frontend.fs :as fs]
[promesa.core :as p]
[frontend.state :as state]
[frontend.db :as db]
[frontend.git :as git]
[frontend.github :as github]
[frontend.handler.file :as file-handler]
[frontend.handler.git :as git-handler]
[cljs-bean.core :as bean]
[frontend.date :as date]
[frontend.config :as config]
[frontend.format :as format]
[frontend.format.protocol :as protocol]
[frontend.storage :as storage]
[clojure.string :as string]
[cljs-time.local :as tl]
[cljs-time.core :as t]
[cljs-time.coerce :as tc]))
;; state
(defonce *files (atom nil))
(defonce *current-file (atom nil))
(defonce *current-title (atom ""))
(defonce *file-loading? (atom nil))
(defonce *elements (atom nil))
(defonce *unsaved? (atom false))
(defonce *search-files (atom []))
(defonce *saving-title (atom nil))
(defonce *excalidraw (atom nil))
;; TODO: refactor
(defonce draw-state :draw-state)
(defn get-draw-state []
(storage/get draw-state))
(defn set-draw-state! [value]
(storage/set draw-state value))
(defn set-k
[k v]
(when-let [repo (state/get-current-repo)]
(let [state (get-draw-state)]
(let [new-state (assoc-in state [repo k] v)]
(set-draw-state! new-state)))))
(defn set-last-file!
[value]
(set-k :last-file value))
;; excalidraw
(defn create-draws-directory!
[repo]
(let [repo-dir (util/get-repo-dir repo)]
(util/p-handle
(fs/mkdir (str repo-dir (str "/" config/default-draw-directory)))
(fn [_result] nil)
(fn [_error] nil))))
(defn save-excalidraw!
[file data ok-handler]
(let [path (str config/default-draw-directory "/" file)
repo (state/get-current-repo)]
(when repo
(let [repo-dir (util/get-repo-dir repo)]
(p/let [_ (create-draws-directory! repo)]
(util/p-handle
(fs/write-file repo-dir path data)
(fn [_]
(util/p-handle
(git-handler/git-add repo path)
(fn [_]
(ok-handler file)
(let [modified-at (tc/to-long (t/now))]
(db/transact! repo
[{:file/path path
:file/last-modified-at modified-at}
{:page/name file
:page/file path
:page/last-modified-at (tc/to-long (t/now))
:page/journal? false}])))))
(fn [error]
(prn "Write file failed, path: " path ", data: " data)
(js/console.dir error))))))))
(defn get-all-excalidraw-files
[ok-handler]
(when-let [repo (state/get-current-repo)]
(p/let [_ (create-draws-directory! repo)]
(let [dir (str (util/get-repo-dir repo)
"/"
config/default-draw-directory)]
(util/p-handle
(fs/readdir dir)
(fn [files]
(let [files (-> (filter #(string/ends-with? % ".excalidraw") files)
(distinct)
(sort)
(reverse))]
(ok-handler files)))
(fn [error]
(js/console.dir error)))))))
(defn load-excalidraw-file
[file ok-handler]
(when-let [repo (state/get-current-repo)]
(util/p-handle
(file-handler/load-file repo (str config/default-draw-directory "/" file))
(fn [content]
(ok-handler content))
(fn [error]
(prn "Error loading " file ": "
error)))))
(defonce default-content
(util/format
"{\n \"type\": \"excalidraw\",\n \"version\": 2,\n \"source\": \"%s\",\n \"elements\": [],\n \"appState\": {\n \"viewBackgroundColor\": \"#FFF\",\n \"gridSize\": null\n }\n}"
config/website))
(defn title->file-name
[title]
(when (not (string/blank? title))
(let [title (string/lower-case (string/replace title " " "-"))]
(str (date/get-date-time-string-2) "-" title ".excalidraw"))))
(defn create-draw-with-default-content
[current-file ok-handler]
(when-let [repo (state/get-current-repo)]
(p/let [exists? (fs/file-exists? (util/get-repo-dir repo)
(str config/default-draw-directory current-file))]
(when-not exists?
(save-excalidraw! current-file default-content
(fn [file]
(reset! *files
(distinct (conj @*files file)))
(reset! *current-file file)
(reset! *unsaved? false)
(set-last-file! file)
(reset! *saving-title nil)
(ok-handler)))))))

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,102 @@
(ns frontend.handler.expand
(:require [dommy.core :as d]
[goog.dom :as gdom]
[goog.object :as gobj]
[frontend.util :as util]
[clojure.string :as string]
[medley.core :as medley]
[frontend.state :as state]
[frontend.db :as db]))
(defn- hide!
[element]
(d/set-style! element :display "none"))
(defn- show!
[element]
(d/set-style! element :display ""))
(defn collapse!
[block]
(let [uuid (:block/uuid block)
nodes (array-seq (js/document.getElementsByClassName (str uuid)))]
(doseq [node nodes]
(d/add-class! node "collapsed")
(when-let [e (.querySelector node ".block-body")]
(hide! e))
(when-let [e (.querySelector node ".block-children")]
(hide! e)
(let [elements (d/by-class node "ls-block")]
(doseq [element elements]
(hide! element))))
(db/collapse-block! block))))
(defn expand!
[block]
(let [uuid (:block/uuid block)
nodes (array-seq (js/document.getElementsByClassName (str uuid)))]
(doseq [node nodes]
(when-let [e (.querySelector node ".block-body")]
(show! e))
(when-let [e (.querySelector node ".block-children")]
(let [elements (d/by-class node "ls-block")]
(doseq [element elements]
(show! element)))
(show! e))
(db/expand-block! block))))
(defn set-bullet-closed!
[element]
(when element
(when-let [node (.querySelector element ".bullet-container")]
(d/add-class! node "bullet-closed"))))
;; Collapse acts like TOC
;; There are three modes to cycle:
;; 1. Collapse all blocks which levels are greater than 2
;; 2. Hide all block's body (user can still see the block title)
;; 3. Show everything
(defn cycle!
[]
(let [mode (state/next-collapse-mode)
get-blocks (fn []
(let [elements (d/by-class "ls-block")
result (group-by (fn [e]
(let [level (d/attr e "level")]
(and level
(> (util/parse-int level) 2)))) elements)]
[(get result true) (get result false)]))]
(case mode
:show-all
(do
(doseq [element (d/by-class "ls-block")]
(show! element))
(let [elements (d/by-class "block-body")]
(doseq [element elements]
(show! element)))
(doseq [element (d/by-class "bullet-closed")]
(d/remove-class! element "bullet-closed"))
(doseq [element (d/by-class "block-children")]
(show! element)))
:hide-block-body
(let [elements (d/by-class "block-body")]
(doseq [element elements]
(d/set-style! element :display "none")
(when-let [parent (util/rec-get-block-node element)]
(set-bullet-closed! parent))))
:hide-block-children
(let [[elements top-level-elements] (get-blocks)
level-2-elements (filter (fn [e]
(let [level (d/attr e "level")]
(and level
(= (util/parse-int level) 2)
(not (d/has-class? e "pre-block")))))
top-level-elements)]
(doseq [element elements]
(hide! element))
(doseq [element level-2-elements]
(when (= "true" (d/attr element "haschild"))
(set-bullet-closed! element)))))
(state/cycle-collapse!)))

View File

@@ -0,0 +1,75 @@
(ns frontend.handler.export
(:require [frontend.state :as state]
[frontend.db :as db]
[frontend.util :as util]
[cljs-bean.core :as bean]
[clojure.string :as string]
[goog.dom :as gdom]
[frontend.publishing.html :as html]))
(defn copy-block!
[block-id]
(when-let [block (db/pull [:block/uuid block-id])]
(let [content (:block/content block)]
(util/copy-to-clipboard! content))))
(defn copy-block-as-json!
[block-id]
(when-let [repo (state/get-current-repo)]
(let [block-children (db/get-block-and-children repo block-id)]
(util/copy-to-clipboard! (js/JSON.stringify (bean/->js block-children))))))
(defn copy-page-as-json!
[page-name]
(when-let [repo (state/get-current-repo)]
(let [properties (db/get-page-properties page-name)
blocks (db/get-page-blocks repo page-name)]
(util/copy-to-clipboard!
(js/JSON.stringify
(bean/->js
{:properties properties
:blocks blocks}))))))
(defn export-repo-as-json!
[repo]
(when-let [db (db/get-conn repo)]
(let [db-json (db/db->json db)
data-str (str "data:text/json;charset=utf-8," (js/encodeURIComponent db-json))]
(when-let [anchor (gdom/getElement "download-as-json")]
(.setAttribute anchor "href" data-str)
(.setAttribute anchor "download" (str (last (string/split repo #"/")) ".json"))
(.click anchor)))))
(defn download-file!
[file-path]
(when-let [repo (state/get-current-repo)]
(when-let [content (db/get-file repo file-path)]
(let [data (js/Blob. (array content)
(clj->js {:type "text/plain"}))]
(let [anchor (gdom/getElement "download")
url (js/window.URL.createObjectURL data)]
(.setAttribute anchor "href" url)
(.setAttribute anchor "download" file-path)
(.click anchor))))))
(defn export-repo-as-html!
[repo]
(when-let [db (db/get-conn repo)]
(let [db (if (state/all-pages-public?)
(db/clean-export! db)
(db/filter-only-public-pages-and-blocks db))
db-str (db/db->string db)
state (select-keys @state/state
[:ui/theme :ui/cycle-collapse
:ui/collapsed-blocks
:ui/sidebar-collapsed-blocks
:ui/show-recent?
:config])
state (update state :config (fn [config]
{"local" (get config repo)}))
html-str (str "data:text/html;charset=UTF-8,"
(js/encodeURIComponent (html/publishing-html db-str (pr-str state))))]
(when-let [anchor (gdom/getElement "download-as-html")]
(.setAttribute anchor "href" html-str)
(.setAttribute anchor "download" "index.html")
(.click anchor)))))

View File

@@ -0,0 +1,45 @@
(ns frontend.handler.external
(:require [frontend.external :as external]
[frontend.handler.file :as file-handler]
[frontend.handler.notification :as notification]
[frontend.state :as state]
[frontend.date :as date]
[frontend.config :as config]
[clojure.string :as string]
[frontend.db :as db]))
(defn index-files!
[repo files error-files]
(doseq [file files]
(let [title (:title file)
journal? (date/valid-journal-title? title)]
(try
(when-let [text (:text file)]
(let [path (str (if journal?
config/default-journals-directory
config/default-pages-directory)
"/"
(if journal?
(date/journal-title->default title)
(string/replace title "/" "-"))
".md")]
(file-handler/alter-file repo path text {})
(when journal?
(let [page-name (string/lower-case title)]
(db/transact! repo
[{:page/name page-name
:page/journal? true
:page/journal-day (date/journal-title->int title)}])))))
(catch js/Error e
(swap! error-files conj file))))))
;; TODO: compute the dependencies
;; TODO: Should it merge the roam daily notes with the month journals
(defn import-from-roam-json!
[data]
(when-let [repo (state/get-current-repo)]
(let [files (external/to-markdown-files :roam data {})
error-files (atom #{})]
(index-files! repo files error-files)
(when (seq @error-files)
(index-files! repo @error-files (atom nil))))))

View File

@@ -0,0 +1,182 @@
(ns frontend.handler.file
(:refer-clojure :exclude [load-file])
(:require [frontend.util :as util :refer-macros [profile]]
[frontend.fs :as fs]
[promesa.core :as p]
[frontend.state :as state]
[frontend.db :as db]
[frontend.git :as git]
[frontend.handler.git :as git-handler]
[frontend.handler.ui :as ui-handler]
[datascript.core :as d]
[frontend.github :as github]
[cljs-bean.core :as bean]
[frontend.date :as date]
[frontend.config :as config]
[frontend.format :as format]
[frontend.format.protocol :as protocol]
[clojure.string :as string]
[frontend.history :as history]
[frontend.handler.project :as project-handler]))
(defn load-file
[repo-url path]
(->
(p/let [content (fs/read-file (util/get-repo-dir repo-url) path)]
content)
(p/catch
(fn [e]
(println "Load file failed: " path)
(js/console.error e)))))
(defn load-multiple-files
[repo-url paths]
(doall
(mapv #(load-file repo-url %) paths)))
(defn- keep-formats
[files formats]
(filter
(fn [file]
(let [format (format/get-format file)]
(contains? formats format)))
files))
(defn- only-supported-formats
[files]
(keep-formats files (config/supported-formats)))
(defn- only-text-formats
[files]
(keep-formats files (config/text-formats)))
(defn- only-image-formats
[files]
(keep-formats files (config/img-formats)))
(defn- hidden?
[path patterns]
(some (fn [pattern]
(or
(= path pattern)
(and (util/starts-with? pattern "/")
(= (str "/" (first (string/split path #"/")))
pattern)))) patterns))
(defn restore-config!
([repo-url project-changed-check?]
(restore-config! repo-url nil project-changed-check?))
([repo-url config-content project-changed-check?]
(let [old-project (:project (state/get-config))
new-config (db/reset-config! repo-url config-content)]
(when project-changed-check?
(let [new-project (:project new-config)
project-name (:name old-project)]
(when-not (= new-project old-project)
(project-handler/sync-project-settings! project-name new-project)))))))
(defn load-files
[repo-url]
(state/set-cloning? false)
(state/set-state! :repo/loading-files? true)
(p/let [files (git/list-files repo-url)
files (bean/->clj files)
config-content (load-file repo-url (str config/app-name "/" config/config-file))
files (if config-content
(let [config (restore-config! repo-url config-content true)]
(if-let [patterns (seq (:hidden config))]
(remove (fn [path] (hidden? path patterns)) files)
files))
files)]
(only-supported-formats files)))
(defn load-files-contents!
[repo-url files ok-handler]
(let [images (only-image-formats files)
files (only-text-formats files)]
(-> (p/all (load-multiple-files repo-url files))
(p/then (fn [contents]
(ok-handler
(cond->
(zipmap files contents)
(seq images)
(merge (zipmap images (repeat (count images) "")))))))
(p/catch (fn [error]
(println "load files failed: ")
(js/console.dir error))))))
(defn alter-file
[repo path content {:keys [reset? re-render-root? add-history?]
:or {reset? true
re-render-root? false
add-history? true}}]
(let [original-content (db/get-file-no-sub repo path)]
(if reset?
(db/reset-file! repo path content)
(db/set-file-content! repo path content))
(util/p-handle
(fs/write-file (util/get-repo-dir repo) path content)
(fn [_]
(git-handler/git-add repo path)
(when (= path (str config/app-name "/" config/config-file))
(restore-config! repo true))
(when (= path (str config/app-name "/" config/custom-css-file))
(ui-handler/add-style-if-exists!))
(when re-render-root? (ui-handler/re-render-root!))
(when add-history?
(history/add-history! repo [[path original-content content]])))
(fn [error]
(println "Write file failed, path: " path ", content: " content)
(js/console.error error)))))
(defn alter-files
[repo files]
(let [files-tx (mapv (fn [[path content]]
(let [original-content (db/get-file-no-sub repo path)]
[path original-content content])) files)]
(-> (p/all
(doall
(map
(fn [[path content]]
(db/set-file-content! repo path content)
(util/p-handle
(fs/write-file (util/get-repo-dir repo) path content)
(fn [_]
(git-handler/git-add repo path))
(fn [error]
(println "Write file failed, path: " path ", content: " content)
(js/console.error error))))
files)))
(p/then (fn [_result]
(ui-handler/re-render-file!)
(history/add-history! repo files-tx))))))
(defn remove-file!
[repo file]
(when-not (string/blank? file)
(->
(p/let [_ (git/remove-file repo file)
result (fs/unlink (str (util/get-repo-dir repo)
"/"
file)
nil)]
(state/git-add! repo (str "- " file))
(when-let [file (db/entity repo [:file/path file])]
(let [file-id (:db/id file)
page-id (db/get-file-page-id (:file/path file))
tx-data (map
(fn [db-id]
[:db.fn/retractEntity db-id])
(remove nil? [file-id page-id]))]
(when (seq tx-data)
(db/transact! repo tx-data)))))
(p/catch (fn [err]
(prn "error: " err))))))
(defn re-index!
[file]
(when-let [repo (state/get-current-repo)]
(let [path (:file/path file)
content (db/get-file path)]
(alter-file repo path content {:re-render-root? true}))))

View File

@@ -0,0 +1,99 @@
(ns frontend.handler.git
(:refer-clojure :exclude [clone load-file])
(:require [frontend.util :as util :refer-macros [profile]]
[frontend.fs :as fs]
[promesa.core :as p]
[frontend.state :as state]
[frontend.db :as db]
[frontend.git :as git]
[frontend.github :as github]
[cljs-bean.core :as bean]
[frontend.date :as date]
[frontend.config :as config]
[frontend.format :as format]
[frontend.format.protocol :as protocol]
[goog.object :as gobj]
[frontend.handler.notification :as notification]
[frontend.handler.route :as route-handler]
[clojure.string :as string]
[cljs-time.local :as tl]
[cljs-time.core :as t]
[cljs-time.coerce :as tc]))
(defn- set-latest-commit!
[repo-url hash]
(db/set-key-value repo-url :git/latest-commit hash))
(defn- set-remote-latest-commit!
[repo-url hash]
(db/set-key-value repo-url :git/remote-latest-commit hash))
(defn- set-git-status!
[repo-url value]
(db/set-key-value repo-url :git/status value)
(state/set-git-status! repo-url value))
(defn- set-git-last-pulled-at!
[repo-url]
(db/set-key-value repo-url :git/last-pulled-at
(date/get-date-time-string (tl/local-now))))
(defn- set-git-error!
[repo-url value]
(db/set-key-value repo-url :git/error (if value (str value))))
(defn git-add
[repo-url file]
(p/let [result (git/add repo-url file)]
(state/git-add! repo-url file)))
(defn get-latest-commit
([repo-url handler]
(get-latest-commit repo-url handler 1))
([repo-url handler length]
(-> (p/let [commits (git/log repo-url length)]
(handler (if (= length 1)
(first commits)
commits)))
(p/catch (fn [error]
(println "get latest commit failed: " error)
(js/console.log (.-stack error))
;; TODO: safe check
(println "It might be an empty repo"))))))
(defn set-latest-commit-if-exists! [repo-url]
(get-latest-commit
repo-url
(fn [commit]
(when-let [hash (gobj/get commit "oid")]
(set-latest-commit! repo-url hash)))))
(defn set-remote-latest-commit-if-exists! [repo-url]
(get-latest-commit
repo-url
(fn [commit]
(when-let [hash (gobj/get commit "oid")]
(set-remote-latest-commit! repo-url hash)))))
(defn commit-and-force-push!
[commit-message pushing?]
(when-let [repo (frontend.state/get-current-repo)]
(let [remote-oid (db/get-key-value repo
:git/remote-latest-commit)]
(p/let [commit-oid (git/commit repo commit-message (array remote-oid))
result (git/write-ref! repo commit-oid)
push-result (git/push repo
(state/get-github-token repo)
true)]
(reset! pushing? false)
(state/clear-changed-files! repo)
(notification/clear! nil)
(route-handler/redirect! {:to :home})))))
(defn git-set-username-email!
[repo-url {:keys [name email]}]
(when (and name email)
(git/set-username-email
(util/get-repo-dir repo-url)
name
email)))

View File

@@ -0,0 +1,33 @@
(ns frontend.handler.history
(:require [frontend.state :as state]
[frontend.history :as history]
[frontend.handler.ui :as ui-handler]
[frontend.handler.file :as file]))
(defn- default-undo
[]
(js/document.execCommand "undo" false nil))
(defn- default-redo
[]
(js/document.execCommand "redo" false nil))
(defn undo!
[]
(let [route (get-in (:route-match @state/state) [:data :name])]
(if (and (contains? #{:home :page :file} route)
(not (state/get-edit-input-id))
(state/get-current-repo))
(let [repo (state/get-current-repo)]
(history/undo! repo file/alter-file))
(default-undo))))
(defn redo!
[]
(let [route (get-in (:route-match @state/state) [:data :name])]
(if (and (contains? #{:home :page :file} route)
(not (state/get-edit-input-id))
(state/get-current-repo))
(let [repo (state/get-current-repo)]
(history/redo! repo file/alter-file))
(default-redo))))

View File

@@ -0,0 +1,92 @@
(ns frontend.handler.image
(:require [goog.object :as gobj]
[frontend.handler.notification :as notification]
[frontend.util :as util :refer-macros [profile]]
[frontend.config :as config]
[frontend.image :as image]
[frontend.state :as state]
[frontend.fs :as fs]
[clojure.string :as string]
[goog.dom :as gdom]))
(defn render-local-images!
[]
(try
(let [images (array-seq (gdom/getElementsByTagName "img"))
get-src (fn [image] (.getAttribute image "src"))
local-images (filter
(fn [image]
(let [src (get-src image)]
(and src
(not (or (util/starts-with? src "http://")
(util/starts-with? src "https://"))))))
images)]
(doseq [img local-images]
(gobj/set img
"onerror"
(fn []
(gobj/set (gobj/get img "style")
"display" "none")))
(let [path (get-src img)
path (string/replace-first path "file:" "")
path (if (= (first path) \.)
(subs path 1)
path)]
(util/p-handle
(fs/read-file-2 (util/get-repo-dir (state/get-current-repo))
path)
(fn [blob]
(let [blob (js/Blob. (array blob) (clj->js {:type "image"}))
img-url (image/create-object-url blob)]
(gobj/set img "src" img-url)
(gobj/set (gobj/get img "style")
"display" "initial")))
(fn [error]
(println "Can't read local image file: ")
(js/console.dir error))))))
(catch js/Error e
nil)))
(defn request-presigned-url
[file filename mime-type uploading? url-handler on-processing]
(cond
(> (gobj/get file "size") (* 12 1024 1024))
(notification/show! [:p "Sorry, we don't support any file that's larger than 12MB."] :error)
:else
(do
(reset! uploading? true)
;; start uploading?
(util/post (str config/api "presigned_url")
{:filename filename
:mime-type mime-type}
(fn [{:keys [presigned-url s3-object-key] :as resp}]
(if presigned-url
(util/upload presigned-url
file
(fn [_result]
;; request cdn signed url
(util/post (str config/api "signed_url")
{:s3-object-key s3-object-key}
(fn [{:keys [signed-url]}]
(reset! uploading? false)
(if signed-url
(do
(url-handler signed-url))
(prn "Something error, can't get a valid signed url.")))
(fn [error]
(reset! uploading? false)
(prn "Something error, can't get a valid signed url."))))
(fn [error]
(reset! uploading? false)
(prn "upload failed.")
(js/console.dir error))
(fn [e]
(on-processing e)))
;; TODO: notification, or re-try
(do
(reset! uploading? false)
(prn "failed to get any presigned url, resp: " resp))))
(fn [_error]
;; (prn "Get token failed, error: " error)
(reset! uploading? false))))))

View File

@@ -0,0 +1,75 @@
(ns frontend.handler.migration
(:require [frontend.handler.notification :as notification]
[frontend.db :as db]
[frontend.ui :as ui]
[promesa.core :as p]
[frontend.util :as util]
[frontend.git :as git]
[clojure.string :as str]
[frontend.date :as date]
[frontend.config :as config]
[frontend.state :as state]
[frontend.handler.ui :as ui-handler]
[frontend.handler.file :as file-handler]
[frontend.handler.git :as git-handler]
[frontend.fs :as fs]))
(defn get-files-from-blocks
[blocks]
(if (<= (count (:page blocks)) 1)
nil
{:path (str config/default-journals-directory "/" (date/journal-title->default (:title blocks)) "." (config/get-file-extension (state/get-preferred-format)))
:page (reduce #(if (not (str/blank? (:block/content %2))) (str %1 (:block/content %2)) %1) "" (:page blocks))}))
(defn handle-journal-migration-from-monthly-to-daily!
[repo]
(state/set-daily-migrating! true)
(let [all-journals (->>
(db/q repo [:journals] {:use-cache? false}
'[:find ?page-name
:where
[?page :page/journal? true]
[?page :page/original-name ?page-name]])
(db/react)
(map first)
(distinct)
(map (fn [el] {:title el :page (db/get-page-blocks repo el)}))
(util/remove-nils)
(map get-files-from-blocks)
(remove nil?))
all-files (map first (db/get-files repo))]
(let [to-delete (filter #(re-find #"journals/[0-9]{4}_[0-9]{2}\.+" %) all-files)]
(-> (p/all (doall (map (fn [{:keys [path page]}]
(println "migrating" path)
(p/let [file-exists? (fs/create-if-not-exists (util/get-repo-dir repo) path page)]
(db/reset-file! repo path page)
(git-handler/git-add repo path))) all-journals)))
(p/then
(fn [_result]
(let [remove-files (doall (map (fn [path]
(db/delete-file! repo path)
(file-handler/remove-file! repo path)) to-delete))]
(-> (p/all remove-files)
(p/then (fn [result]
(println "Migration successfully!")
(state/set-daily-migrating! false)
(ui-handler/re-render-root!)
(notification/show!
"Migration successfully! Please re-index your repository after the sync indicator turned green for a smooth experience."
:success)))
(p/catch (fn [error]
(state/set-daily-migrating! false)
(println "Migration failed: ")
(js/console.dir error)))))))))))
(defn show!
[]
(when-let [current-repo (state/get-current-repo)]
(when (db/monthly-journals-exists? current-repo)
(notification/show!
[:div
[:p "Logseq is migrating to creating journal pages on a daily basis for better performance and data safety. In the future, the current method of storing journal files once a month would be removed. Please click the following button to migrate, and feel free to let us know if anything unexpected happened!"]
(ui/button "Begin migration"
:on-click #(handle-journal-migration-from-monthly-to-daily! current-repo))]
:warning
false))))

View File

@@ -0,0 +1,25 @@
(ns frontend.handler.notification
(:require [frontend.state :as state]
[frontend.util :as util]))
(defn clear!
[uid]
(let [contents (state/get-notification-contents)]
(state/set-state! :notification/contents (dissoc contents uid))))
(defn show!
([content status]
(show! content status true nil))
([content status clear?]
(show! content status clear? nil))
([content status clear? uid]
(let [contents (state/get-notification-contents)
uid (or uid (keyword (util/unique-id)))]
(state/set-state! :notification/contents (assoc contents
uid {:content content
:status status}))
(when clear?
(js/setTimeout #(clear! uid) 3000))
uid)))

View File

@@ -0,0 +1,341 @@
(ns frontend.handler.page
(:require [clojure.string :as string]
[frontend.db :as db]
[datascript.core :as d]
[frontend.state :as state]
[frontend.util :as util :refer-macros [profile]]
[frontend.tools.html-export :as html-export]
[frontend.config :as config]
[frontend.handler.route :as route-handler]
[frontend.handler.file :as file-handler]
[frontend.handler.git :as git-handler]
[frontend.handler.editor :as editor-handler]
[frontend.handler.project :as project-handler]
[frontend.handler.notification :as notification]
[frontend.handler.ui :as ui-handler]
[frontend.date :as date]
[clojure.walk :as walk]
[frontend.git :as git]
[frontend.fs :as fs]
[promesa.core :as p]
[goog.object :as gobj]
[frontend.format.mldoc :as mldoc]))
(defn create!
[title]
(let [repo (state/get-current-repo)
dir (util/get-repo-dir repo)
journal-page? (date/valid-journal-title? title)
directory (if journal-page?
config/default-journals-directory
config/default-pages-directory)]
(when dir
(p/let [_ (-> (fs/mkdir (str dir "/" directory))
(p/catch (fn [_e])))]
(let [format (name (state/get-preferred-format))
page (string/lower-case title)
path (str (if journal-page?
(date/journal-title->default title)
(util/page-name-sanity page))
"."
(if (= format "markdown") "md" format))
path (str directory "/" path)
file-path (str "/" path)]
(p/let [exists? (fs/file-exists? dir file-path)]
(if exists?
(notification/show!
[:p.content
(util/format "File %s already exists!" file-path)]
:error)
;; create the file
(let [content (util/default-content-with-title format title)]
(p/let [_ (fs/create-if-not-exists dir file-path content)]
(db/reset-file! repo path content)
(git-handler/git-add repo path)
(route-handler/redirect! {:to :page
:path-params {:name page}})
(let [blocks (db/get-page-blocks page)
last-block (last blocks)]
(when last-block
(js/setTimeout
#(editor-handler/edit-last-block-for-new-page! last-block 0)
100))))))))))))
(defn page-add-properties!
[page-name properties]
(let [page (db/entity [:page/name page-name])
page-format (db/get-page-format page-name)
properties-content (db/get-page-properties-content page-name)
properties-content (if properties-content
(string/trim properties-content)
(config/properties-wrapper page-format))]
(let [file (db/entity (:db/id (:page/file page)))
file-path (:file/path file)
file-content (db/get-file file-path)
after-content (subs file-content (inc (count properties-content)))
new-properties-content (db/add-properties! page-format properties-content properties)
full-content (str new-properties-content "\n\n" (string/trim after-content))]
(file-handler/alter-file (state/get-current-repo)
file-path
full-content
{:reset? true
:re-render-root? true}))))
(defn page-remove-property!
[page-name k]
(when-let [properties-content (string/trim (db/get-page-properties-content page-name))]
(let [page (db/entity [:page/name page-name])
file (db/entity (:db/id (:page/file page)))
file-path (:file/path file)
file-content (db/get-file file-path)
after-content (subs file-content (count properties-content))
page-format (db/get-page-format page-name)
new-properties-content (let [lines (string/split-lines properties-content)
prefix (case page-format
:org (str "#+" (string/upper-case k) ": ")
:markdown (str (string/lower-case k) ": ")
"")
exists? (atom false)
lines (remove #(util/starts-with? % prefix) lines)]
(string/join "\n" lines))
full-content (str new-properties-content "\n\n" (string/trim after-content))]
(file-handler/alter-file (state/get-current-repo)
file-path
full-content
{:reset? true
:re-render-root? true}))))
(defn published-success-handler
[page-name]
(fn [result]
(let [permalink (:permalink result)]
(page-add-properties! page-name {"permalink" permalink})
(let [win (js/window.open (str
config/website
"/"
(state/get-current-project)
"/"
permalink))]
(.focus win)))))
(defn published-failed-handler
[error]
(notification/show!
"Publish failed, please give it another try."
:error))
(defn get-plugins
[blocks]
(let [plugins (atom {})
add-plugin #(swap! plugins assoc % true)]
(walk/postwalk
(fn [x]
(if (and (vector? x)
(>= (count x) 2))
(let [[type option] x]
(case type
"Src" (when (:language option)
(add-plugin "highlight"))
"Export" (when (= option "latex")
(add-plugin "latex"))
"Latex_Fragment" (add-plugin "latex")
"Math" (add-plugin "latex")
"Latex_Environment" (add-plugin "latex")
nil)
x)
x))
(map :block/body blocks))
@plugins))
(defn publish-page-as-slide!
([page-name project-add-modal]
(publish-page-as-slide! page-name (db/get-page-blocks page-name) project-add-modal))
([page-name blocks project-add-modal]
(project-handler/exists-or-create!
(fn [project]
(page-add-properties! page-name {"published" true
"slide" true})
(let [properties (db/get-page-properties page-name)
plugins (get-plugins blocks)
data {:project project
:title page-name
:permalink (:permalink properties)
:html (html-export/export-page page-name blocks notification/show!)
:tags (:tags properties)
:settings (merge
(assoc properties
:slide true
:published true)
plugins)
:repo (state/get-current-repo)}]
(util/post (str config/api "pages")
data
(published-success-handler page-name)
published-failed-handler)))
project-add-modal)))
(defn publish-page!
[page-name project-add-modal]
(project-handler/exists-or-create!
(fn [project]
(let [properties (db/get-page-properties page-name)
slide? (let [slide (:slide properties)]
(or (true? slide)
(= "true" slide)))
blocks (db/get-page-blocks page-name)
plugins (get-plugins blocks)]
(if slide?
(publish-page-as-slide! page-name blocks project-add-modal)
(do
(page-add-properties! page-name {"published" true})
(let [data {:project project
:title page-name
:permalink (:permalink properties)
:html (html-export/export-page page-name blocks notification/show!)
:tags (:tags properties)
:settings (merge properties plugins)
:repo (state/get-current-repo)}]
(util/post (str config/api "pages")
data
(published-success-handler page-name)
published-failed-handler))))))
project-add-modal))
(defn unpublished-success-handler
[page-name]
(fn [result]
(notification/show!
"Un-publish successfully!"
:success)))
(defn unpublished-failed-handler
[error]
(notification/show!
"Un-publish failed, please give it another try."
:error))
(defn unpublish-page!
[page-name]
(page-add-properties! page-name {"published" false})
(let [properties (db/get-page-properties page-name)
permalink (:permalink properties)
project (state/get-current-project)]
(if (and project permalink)
(util/delete (str config/api project "/" permalink)
(unpublished-success-handler page-name)
unpublished-failed-handler)
(notification/show!
"Can't find the permalink of this page!"
:error))))
(defn delete!
[page-name ok-handler]
(when page-name
(when-let [repo (state/get-current-repo)]
(let [page-name (string/lower-case page-name)]
(let [file (db/get-page-file page-name)
file-path (:file/path file)]
;; delete file
(when file-path
(db/transact! [[:db.fn/retractEntity [:file/path file-path]]])
(when-let [files-conn (db/get-files-conn repo)]
(d/transact! files-conn [[:db.fn/retractEntity [:file/path file-path]]]))
(let [blocks (db/get-page-blocks page-name)
tx-data (mapv
(fn [block]
[:db.fn/retractEntity [:block/uuid (:block/uuid block)]])
blocks)]
(db/transact! tx-data)
;; remove file
(->
(p/let [_ (git/remove-file repo file-path)
_result (fs/unlink (str (util/get-repo-dir repo)
"/"
file-path)
nil)]
(state/git-add! repo (str "- " file-path)))
(p/catch (fn [err]
(prn "error: " err))))))
(db/transact! [[:db.fn/retractEntity [:page/name page-name]]])
(ok-handler))))))
(defn rename!
[old-name new-name]
(when (and old-name new-name
(not= (string/lower-case old-name) (string/lower-case new-name)))
(when-let [repo (state/get-current-repo)]
(when-let [page (db/entity [:page/name (string/lower-case old-name)])]
(let [old-original-name (:page/original-name page)
file (:page/file page)]
(d/transact! (db/get-conn repo false)
[{:db/id (:db/id page)
:page/name (string/lower-case new-name)
:page/original-name new-name}])
(when file
(page-add-properties! (string/lower-case new-name) {:title new-name}))
;; update all files which have references to this page
(let [files (db/get-files-that-referenced-page (:db/id page))]
(doseq [file-path files]
(let [file-content (db/get-file file-path)
;; FIXME: not safe
new-content (string/replace file-content
(util/format "[[%s]]" old-original-name)
(util/format "[[%s]]" new-name))]
(file-handler/alter-file repo
file-path
new-content
{:reset? true
:re-render-root? false})))))
;; TODO: update browser history, remove the current one
;; Redirect to the new page
(route-handler/redirect! {:to :page
:path-params {:name (util/encode-str (string/lower-case new-name))}})
(notification/show! "Page renamed successfully!" :success)
(ui-handler/re-render-root!)))))
(defn rename-when-alter-title-propertiy!
[page path format original-content content]
(when (and page (contains? config/mldoc-support-formats format))
(let [old-name page
new-name (let [ast (mldoc/->edn content (mldoc/default-config format))]
(db/get-page-name path ast))]
(when (not= old-name new-name)
(rename! old-name new-name)))))
(defn handle-add-page-to-contents!
[page-name]
(let [last-block (last (db/get-page-blocks (state/get-current-repo) "contents"))
last-empty? (>= 3 (count (:block/content last-block)))
heading-pattern (config/get-block-pattern (state/get-preferred-format))
pre-str (str heading-pattern heading-pattern)
new-content (if last-empty? (str pre-str " [[" page-name "]]") (str (:block/content last-block) pre-str " [[" page-name "]]"))]
(editor-handler/insert-new-block-aux!
last-block
new-content
{:create-new-block? false
:ok-handler
(fn [[_first-block last-block _new-block-content]]
(notification/show! "Added to contents!" :success)
(editor-handler/clear-when-saved!))
:with-level? true
:new-level 2
:current-page "Contents"})))
(defn load-more-journals!
[]
(let [current-length (:journals-length @state/state)]
(when (< current-length (db/get-journals-length))
(state/update-state! :journals-length inc))))
(defn update-public-attribute!
[page-name value]
(page-add-properties! page-name {:public value}))

View File

@@ -0,0 +1,71 @@
(ns frontend.handler.project
(:require [frontend.state :as state]
[frontend.util :as util :refer-macros [profile]]
[clojure.string :as string]
[frontend.config :as config]
[frontend.handler.notification :as notification]))
;; project exists and current user owns it
;; if project not exists, the server will create it
(defn project-exists?
[project]
(let [projects (set (map :name (:projects (state/get-me))))]
(and (seq projects) (contains? projects project))))
(defn create-project!
([ok-handler]
(create-project! (state/get-current-project) ok-handler))
([project ok-handler]
(let [config (state/get-config)
data {:name project
:repo (state/get-current-repo)
:settings (or (get config :project)
{:name project})}]
(util/post (str config/api "projects")
data
(fn [result]
(swap! state/state
update-in [:me :projects]
(fn [projects]
(util/distinct-by :name (conj projects result))))
(ok-handler project))
(fn [error]
(js/console.dir error)
(notification/show! (util/format "Project \"%s\" already taken, please change to another name." project) :error))))))
(defn exists-or-create!
[ok-handler modal-content]
(if-let [project (state/get-current-project)]
(if (project-exists? project)
(ok-handler project)
(create-project! ok-handler))
(state/set-modal! modal-content)))
(defn add-project!
[project]
(create-project! project
(fn []
(notification/show! (util/format "Project \"%s\" was created successfully." project) :success)
(state/close-modal!))))
(defn sync-project-settings!
([]
(when-let [project-name (state/get-current-project)]
(let [settings (:project (state/get-config))]
(sync-project-settings! project-name settings))))
([project-name settings]
(when-let [repo (state/get-current-repo)]
(if (project-exists? project-name)
(util/post (str config/api "projects/" project-name)
{:name project-name
:settings settings
:repo repo}
(fn [response]
(notification/show! "Project settings changed successfully!" :success))
(fn [error]
(println "Project settings updated failed, reason: ")
(js/console.dir error)))
(when (and settings
(not (string/blank? (:name settings)))
(>= (count (string/trim (:name settings))) 2))
(add-project! (:name settings)))))))

View File

@@ -0,0 +1,137 @@
(ns frontend.handler.repeated
(:require [cljs-time.core :as t]
[cljs-time.local :as tl]
[cljs-time.format :as tf]
[frontend.date :as date]
[clojure.string :as string]
[frontend.util :as util]))
(def custom-formatter (tf/formatter "yyyy-MM-dd EEE"))
(defn repeated?
[timestamp]
(some? (:repetition timestamp)))
(defn- get-duration-f-and-text
[duration]
(case duration
"Hour"
[t/hours "h"]
"Day"
[t/days "d"]
"Week"
[t/weeks "w"]
"Month"
[t/months "m"]
"Year"
[t/years "y"]
nil))
(defn get-repeater-symbol
[kind]
(case kind
"Plus"
"+"
"Dotted"
".+"
"++"))
(defn timestamp->text
([timestamp]
(timestamp->text timestamp nil))
([{:keys [date wday repetition time active]} start-time]
(let [{:keys [year month day]} date
{:keys [hour min]
:or {hour 0 min 0}} time
[hour min] (if start-time
[(t/hour start-time)
(t/minute start-time)]
[hour min])
[[kind] [duration] num] repetition
start-time (or start-time (t/local-date-time year month day hour min))
[duration-f d] (get-duration-f-and-text duration)
kind (get-repeater-symbol kind)
repeater (when (and kind num d)
(str kind num d))
time-repeater (if time
(str (util/zero-pad hour) ":" (util/zero-pad min)
(if (string/blank? repeater)
""
(str " " repeater)))
repeater)]
(util/format "<%s%s>"
(tf/unparse custom-formatter start-time)
(if (string/blank? time-repeater)
""
(str " " time-repeater))))))
(defn- repeat-until-future-timestamp
[datetime now delta keep-week?]
(let [result (loop [result datetime]
(if (t/after? result now)
result
(recur (t/plus result delta))))
w1 (t/day-of-week datetime)
w2 (t/day-of-week result)]
(if (and keep-week? (not= w1 w2))
;; next week
(if (> w2 w1)
(t/plus result (t/days (- 7 (- w2 w1))))
(t/plus result (t/days (- w1 w2))))
result)))
;; Fro https://www.reddit.com/r/orgmode/comments/hr2ytg/difference_between_the_repeaters_orgzly/fy2izqx?utm_source=share&utm_medium=web2x&context=3
;; I use these repeaters for habit tracking and it can get a little tricky to keep track. This is my short form understanding:
;; ".+X" = repeat in X d/w/m from the last time I marked it done
;; "++X" = repeat in at least X d/w/m from the last time I marked it done and keep it on the same day of the week move the due date into the future by increments of d/w/m. If the due date, after being moved forward X d/w/m is still in the past, adjust it by however many d/w/m needed to get it into the future. For the w, the day of the week is kept constant.
;; "+X" = repeat in X d/w/m from when I originally scheduled it, regardless of when I marked it done. Rarely used (as described by u/serendependy). A relevant case would be "paying rent" from the link.
(defn next-timestamp-text
[{:keys [date wday repetition time active] :as timestamp}]
(let [{:keys [year month day]} date
{:keys [hour min]
:or {hour 0 min 0}} time
[[kind] [duration] num] repetition
[duration-f _] (get-duration-f-and-text duration)
delta (duration-f num)
today (date/get-local-date)
start-time (t/local-date-time year month day hour min)
start-time' (if (or (= kind "Dotted")
(= kind "DoublePlus"))
(if (t/before? (tl/local-now) start-time)
start-time
;; Repeatedly add delta to make it a future timestamp
(repeat-until-future-timestamp start-time (tl/local-now) delta
(= kind "DoublePlus")))
(t/plus start-time delta))]
(timestamp->text timestamp start-time')))
(defn timestamp-map->text
[{:keys [date time repeater]}]
(let [{:keys [kind duration num]} repeater
repeater (when (and kind num duration)
(str kind num duration))
time-repeater (if-not (string/blank? time)
(str time
(if (string/blank? repeater)
""
(str " " repeater)))
repeater)]
(util/format "<%s%s>"
(tf/unparse custom-formatter date)
(if (string/blank? time-repeater)
""
(str " " time-repeater)))))
(defn timestamp->map
[{:keys [date wday repetition time active]}]
(let [{:keys [year month day]} date
{:keys [hour min]} time
[[kind] [duration] num] repetition]
{:date (t/local-date year month day)
:time (when (and hour min)
(str (util/zero-pad hour) ":" (util/zero-pad min)))
:repeater (when (and kind duration num)
{:kind (get-repeater-symbol kind)
:duration (last (get-duration-f-and-text duration))
:num num})}))

View File

@@ -0,0 +1,641 @@
(ns frontend.handler.repo
(:refer-clojure :exclude [clone])
(:require [frontend.util :as util :refer-macros [profile]]
[frontend.fs :as fs]
[promesa.core :as p]
[datascript.core :as d]
[frontend.state :as state]
[frontend.db :as db]
[frontend.git :as git]
[frontend.github :as github]
[cljs-bean.core :as bean]
[frontend.date :as date]
[frontend.config :as config]
[frontend.format :as format]
[frontend.format.protocol :as protocol]
[goog.object :as gobj]
[frontend.handler.ui :as ui-handler]
[frontend.handler.git :as git-handler]
[frontend.handler.file :as file-handler]
[frontend.handler.migration :as migration-handler]
[frontend.handler.project :as project-handler]
[frontend.handler.notification :as notification]
[frontend.handler.route :as route-handler]
[frontend.handler.user :as user-handler]
[frontend.ui :as ui]
[cljs-time.local :as tl]
[cljs-time.core :as t]
[cljs.reader :as reader]
[clojure.string :as string]
[frontend.dicts :as dicts]
;; [clojure.set :as set]
))
;; Project settings should be checked in two situations:
;; 1. User changes the config.edn directly in logseq.com (fn: alter-file)
;; 2. Git pulls the new change (fn: load-files)
(defn load-repo-to-db!
[repo-url diffs first-clone?]
(let [load-contents (fn [files delete-files delete-blocks re-render?]
(file-handler/load-files-contents!
repo-url
files
(fn [contents]
(state/set-state! :repo/loading-files? false)
(state/set-state! :repo/importing-to-db? true)
(let [parsed-files (filter
(fn [[file _]]
(let [format (format/get-format file)]
(contains? config/mldoc-support-formats format)))
contents)
blocks-pages (if (seq parsed-files)
(db/extract-all-blocks-pages repo-url parsed-files)
[])]
(db/reset-contents-and-blocks! repo-url contents blocks-pages delete-files delete-blocks)
(let [config-file (str config/app-name "/" config/config-file)]
(when (contains? (set files) config-file)
(when-let [content (get contents config-file)]
(file-handler/restore-config! repo-url content true))))
;; (let [metadata-file (str config/app-name "/" config/metadata-file)]
;; (when (contains? (set files) metadata-file)
;; (when-let [content (get contents metadata-file)]
;; (let [{:keys [tx-data]} (reader/read-string content)]
;; (db/transact! repo-url tx-data)))))
(state/set-state! :repo/importing-to-db? false)
(when re-render?
(ui-handler/re-render-root!))))))]
(if first-clone?
(->
(p/let [files (file-handler/load-files repo-url)]
(load-contents files nil nil false))
(p/catch (fn [error]
(println "loading files failed: ")
(js/console.dir error)
(state/set-state! :repo/loading-files? false))))
(when (seq diffs)
(let [filter-diffs (fn [type] (->> (filter (fn [f] (= type (:type f))) diffs)
(map :path)))
remove-files (filter-diffs "remove")
modify-files (filter-diffs "modify")
add-files (filter-diffs "add")
delete-files (if (seq remove-files)
(db/delete-files remove-files))
delete-blocks (db/delete-blocks repo-url (concat remove-files modify-files))
delete-pages (if (seq remove-files)
(db/delete-pages-by-files remove-files)
[])
add-or-modify-files (util/remove-nils (concat add-files modify-files))]
(load-contents add-or-modify-files (concat delete-files delete-pages) delete-blocks true))))))
(defn show-install-error!
[repo-url title]
(notification/show!
[:p.content
title
[:span.text-gray-700.mr-2
(util/format
"Please make sure that you've installed the logseq app for the repo %s on GitHub. "
repo-url)
(ui/button
"Install Logseq on GitHub"
:href (str "https://github.com/apps/" config/github-app-name "/installations/new"))]]
:error
false))
(defn show-diff-error!
[_repo-url]
(notification/show!
[:p
[:span.text-gray-700.font-bold.mr-2
"Please resolve the diffs if any."]
(ui/button
"Go to diff"
:href "/diff")]
:error
false))
(defn get-new-token
[repo]
(when-let [installation-id (-> (filter
(fn [r]
(= (:url r) repo))
(:repos (state/get-me)))
(first)
:installation_id)]
(util/post (str config/api "refresh_github_token")
{:installation-ids [installation-id]}
(fn [result]
(let [token (:token (first result))]
(state/set-github-token! repo token)))
(fn [error]
(println "Something wrong!")
(js/console.dir error)))))
(defn request-app-tokens!
[ok-handler error-handler]
(let [repos (:repos (state/get-me))
installation-ids (->> (map :installation_id repos)
(remove nil?)
(distinct))]
(when (or (seq repos)
(seq installation-ids))
(util/post (str config/api "refresh_github_token")
{:installation-ids installation-ids
:repos repos}
(fn [result]
(state/set-github-installation-tokens! result)
(when ok-handler (ok-handler)))
(fn [error]
(println "Something wrong!")
(js/console.dir error)
(when error-handler (error-handler)))))))
(defn journal-file-changed?
[repo-url diffs]
(contains? (set (map :path diffs))
(db/get-current-journal-path)))
(defn create-config-file-if-not-exists
[repo-url]
(let [repo-dir (util/get-repo-dir repo-url)
app-dir config/app-name
dir (str repo-dir "/" app-dir)]
(p/let [_ (-> (fs/mkdir dir)
(p/catch (fn [_e])))]
(let [default-content config/config-default-content]
(p/let [file-exists? (fs/create-if-not-exists repo-dir (str app-dir "/" config/config-file) default-content)]
(let [path (str app-dir "/" config/config-file)
old-content (when file-exists?
(db/get-file repo-url path))
content (or
(and old-content
(string/replace old-content "heading" "block"))
default-content)]
(db/reset-file! repo-url path content)
(db/reset-config! repo-url content)
(when-not (= content old-content)
(git-handler/git-add repo-url path))))
;; (p/let [file-exists? (fs/create-if-not-exists repo-dir (str app-dir "/" config/metadata-file) default-content)]
;; (let [path (str app-dir "/" config/metadata-file)]
;; (when-not file-exists?
;; (db/reset-file! repo-url path "{:tx-data []}")
;; (git-handler/git-add repo-url path))))
))))
(defn create-contents-file
[repo-url]
(let [repo-dir (util/get-repo-dir repo-url)
format (state/get-preferred-format)
path (str "pages/contents." (if (= (name format) "markdown")
"md"
(name format)))
file-path (str "/" path)
default-content (util/default-content-with-title format "contents")]
(p/let [_ (-> (fs/mkdir (str repo-dir "/pages"))
(p/catch (fn [_e])))
file-exists? (fs/create-if-not-exists repo-dir file-path default-content)]
(when-not file-exists?
(db/reset-file! repo-url path default-content)
(git-handler/git-add repo-url path)))))
(defn create-dummy-notes-page
[repo-url content]
(let [repo-dir (util/get-repo-dir repo-url)
path (str config/default-pages-directory "/how_to_make_dummy_notes.md")
file-path (str "/" path)]
(p/let [_ (-> (fs/mkdir (str repo-dir "/" config/default-pages-directory))
(p/catch (fn [_e])))
_file-exists? (fs/create-if-not-exists repo-dir file-path content)]
(db/reset-file! repo-url path content))))
(defn create-today-journal-if-not-exists
([repo-url]
(create-today-journal-if-not-exists repo-url nil))
([repo-url content]
(let [repo-dir (util/get-repo-dir repo-url)
format (state/get-preferred-format)
title (date/today)
file-name (date/journal-title->default title)
default-content (util/default-content-with-title format title false)
template (state/get-journal-template)
template (if (and template
(not (string/blank? template)))
template)
content (cond
content
content
template
(str default-content template)
:else
(util/default-content-with-title format title true))
path (str config/default-journals-directory "/" file-name "."
(config/get-file-extension format))
file-path (str "/" path)
page-exists? (db/entity repo-url [:page/name (string/lower-case title)])
empty-blocks? (empty? (db/get-page-blocks-no-cache repo-url (string/lower-case title)))]
(when (or empty-blocks?
(not page-exists?))
(p/let [_ (-> (fs/mkdir (str repo-dir "/" config/default-journals-directory))
(p/catch (fn [_e])))
file-exists? (fs/create-if-not-exists repo-dir file-path content)]
;; TODO: why file exists but page not created
(p/let [resolved-content (if file-exists?
(file-handler/load-file repo-url path)
(p/resolved content))]
(let [content (if (string/blank? (string/trim resolved-content))
content
resolved-content)]
(db/reset-file! repo-url path content)
(ui-handler/re-render-root!)
(git-handler/git-add repo-url path))))))))
(defn create-default-files!
[repo-url]
(when-let [name (get-in @state/state [:me :name])]
(create-config-file-if-not-exists repo-url)
(create-today-journal-if-not-exists repo-url)
(create-contents-file repo-url)))
(defn persist-repo!
[repo]
(when-let [files-conn (db/get-files-conn repo)]
(db/persist repo @files-conn true))
(when-let [db (db/get-conn repo)]
(db/persist repo db false)))
(defn load-db-and-journals!
[repo-url diffs first-clone?]
(when (or diffs first-clone?)
(p/let [_ (load-repo-to-db! repo-url diffs first-clone?)]
(when first-clone?
(create-default-files! repo-url))
(when first-clone?
(migration-handler/show!)))))
(defn transact-react-and-alter-file!
[repo tx transact-option files]
(let [files (remove nil? files)
pages (->> (map db/get-file-page (map first files))
(remove nil?))]
(db/transact-react!
repo
tx
transact-option)
(when (seq pages)
(let [children-tx (mapcat #(db/rebuild-page-blocks-children repo %) pages)]
(when (seq children-tx)
(db/transact! repo children-tx)))))
(when (seq files)
(file-handler/alter-files repo files)))
(defn persist-repo-metadata!
[repo]
(let [files (db/get-files repo)]
(when (seq files)
(let [data (db/get-sync-metadata repo)
data-str (pr-str data)]
(file-handler/alter-file repo
(str config/app-name "/" config/metadata-file)
data-str
{:reset? false})))))
(defn periodically-persist-app-metadata
[repo-url]
(js/setInterval #(persist-repo-metadata! repo-url)
(* 5 60 1000)))
(declare push)
(defn pull
[repo-url token {:keys [fallback? force-pull?]
:or {fallback? false
force-pull? false}}]
(when (and
(db/get-conn repo-url true)
(db/cloned? repo-url)
token)
(let [status (db/get-key-value repo-url :git/status)]
(when (or
force-pull?
(and
;; (not= status :push-failed)
(not= status :pushing)
(empty? (state/get-changed-files repo-url))
(not (state/get-edit-input-id))
(not (state/in-draw-mode?))))
(git-handler/set-git-status! repo-url :pulling)
(let [latest-commit (db/get-key-value repo-url :git/latest-commit)]
(->
(p/let [result (git/fetch repo-url token)]
(let [{:keys [fetchHead]} (bean/->clj result)]
(when fetchHead
(git-handler/set-remote-latest-commit! repo-url fetchHead))
(-> (git/merge repo-url)
(p/then (fn [result]
(-> (git/checkout repo-url)
(p/then (fn [result]
(git-handler/set-git-status! repo-url nil)
(git-handler/set-git-last-pulled-at! repo-url)
(when (and latest-commit fetchHead
(not= latest-commit fetchHead))
(p/let [diffs (git/get-diffs repo-url latest-commit fetchHead)]
(when (seq diffs)
(load-db-and-journals! repo-url diffs false)
(git-handler/set-latest-commit! repo-url fetchHead)
(when (seq (state/get-changed-files repo-url))
;; FIXME: no need to create a new commit
(push repo-url {:diff-push? true})))))))
(p/catch (fn [error]
(git-handler/set-git-status! repo-url :checkout-failed)
(git-handler/set-git-error! repo-url error))))))
(p/catch (fn [error]
(git-handler/set-git-status! repo-url :merge-failed)
(git-handler/set-git-error! repo-url error)
(notification/show!
[:p.content
"Failed to merge, please "
[:span.text-gray-700.font-bold
"resolve any diffs first."]]
:error)
(route-handler/redirect! {:to :diff}))))))
(p/catch (fn [error]
(println "Pull error:" (str error))
(js/console.error error)
;; token might be expired, request new token
(cond
(and (or (string/includes? (str error) "401")
(string/includes? (str error) "404"))
(not fallback?))
(request-app-tokens!
(fn []
(pull repo-url (state/get-github-token repo-url) {:fallback? true}))
nil)
(or (string/includes? (str error) "401")
(string/includes? (str error) "404"))
(show-install-error! repo-url (util/format "Failed to fetch %s." repo-url))
:else
nil)))))))))
(defn check-changed-files-status
[f]
(when (gobj/get js/window.workerThread "getChangedFiles")
(->
(p/let [files (js/window.workerThread.getChangedFiles (util/get-repo-dir (state/get-current-repo)))]
(let [files (bean/->clj files)]
(when (empty? files)
;; FIXME: getChangedFiles not return right result
(state/reset-changed-files! files))))
(p/catch (fn [error]
(js/console.dir error))))))
(defn push
[repo-url {:keys [commit-message fallback? diff-push? force?]
:or {commit-message "Logseq auto save"
fallback? false
diff-push? false
force? false}}]
(let [status (db/get-key-value repo-url :git/status)]
(when (and
(db/cloned? repo-url)
(not (state/get-edit-input-id)))
(-> (p/let [files (js/window.workerThread.getChangedFiles (util/get-repo-dir (state/get-current-repo)))]
(prn {:changed-files files})
(when (or
;; FIXME:
force?
(and
(seq (state/get-changed-files repo-url))
(seq files))
fallback?
diff-push?)
;; auto commit if there are any un-committed changes
(let [commit-message (if (string/blank? commit-message)
"Logseq auto save"
commit-message)]
(p/let [_ (git/commit repo-url commit-message)]
(git-handler/set-latest-commit-if-exists! repo-url)
(git-handler/set-git-status! repo-url :pushing)
(when-let [token (state/get-github-token repo-url)]
(util/p-handle
(git/push repo-url token)
(fn [result]
(git-handler/set-git-status! repo-url nil)
(git-handler/set-git-error! repo-url nil)
(state/clear-changed-files! repo-url))
(fn [error]
(js/console.error error)
(let [permission? (or (string/includes? (str error) "401")
(string/includes? (str error) "404"))]
(cond
(and permission? (not fallback?))
(request-app-tokens!
(fn []
(push repo-url
{:commit-message commit-message
:fallback? true}))
nil)
:else
(do
(git-handler/set-git-status! repo-url :push-failed)
(git-handler/set-git-error! repo-url error)
(if permission?
(show-install-error! repo-url (util/format "Failed to push to %s. " repo-url))
(pull repo-url token {:force-pull? true}))))))))))))
(p/catch (fn [error]
(println "Git push error: ")
(js/console.dir error)))))))
(defn pull-current-repo
[]
(when-let [repo (state/get-current-repo)]
(when-let [token (state/get-github-token repo)]
(pull repo token {:force-pull? true}))))
(defn clone
([repo-url]
(clone repo-url false))
([repo-url fallback?]
(when-let [token (state/get-github-token repo-url)]
(util/p-handle
(do
(state/set-cloning? true)
(git/clone repo-url token))
(fn [result]
(state/set-git-clone-repo! "")
(state/set-current-repo! repo-url)
(db/start-db-conn! (:me @state/state) repo-url)
(db/mark-repo-as-cloned repo-url)
(git-handler/set-latest-commit-if-exists! repo-url)
(git-handler/set-remote-latest-commit-if-exists! repo-url))
(fn [e]
(if (and (not fallback?)
(or (string/includes? (str e) "401")
(string/includes? (str e) "404")))
(request-app-tokens!
(fn []
(clone repo-url true))
nil)
(do
(println "Clone failed, error: ")
(js/console.error e)
(state/set-cloning? false)
(git-handler/set-git-status! repo-url :clone-failed)
(git-handler/set-git-error! repo-url e)
(show-install-error! repo-url (util/format "Failed to clone %s." repo-url)))))))))
(defn set-config-content!
[repo path new-config]
(let [new-content (util/pp-str new-config)]
(file-handler/alter-file repo path new-content {:reset? false
:re-render-root? false})))
(defn set-config!
[k v]
(when-let [repo (state/get-current-repo)]
(let [path (str config/app-name "/" config/config-file)]
(when-let [config (db/get-file-no-sub path)]
(let [config (try
(reader/read-string config)
(catch js/Error e
(println "Parsing config file failed: ")
(js/console.dir e)
{}))
ks (if (vector? k) k [k])
new-config (assoc-in config ks v)]
(state/set-config! repo new-config)
(set-config-content! repo path new-config))))))
(defn remove-repo!
[{:keys [id url] :as repo}]
(util/delete (str config/api "repos/" id)
(fn []
(db/remove-conn! url)
(db/remove-db! url)
(db/remove-files-db! url)
(fs/rmdir (util/get-repo-dir url))
(state/delete-repo! repo)
(state/clear-changed-files! repo))
(fn [error]
(prn "Delete repo failed, error: " error))))
(defn setup-local-repo-if-not-exists!
[]
(if js/window.pfs
(let [repo config/local-repo]
(p/let [result (-> (fs/mkdir (str "/" repo))
(p/catch (fn [_e] nil)))
_ (state/set-current-repo! repo)
_ (db/start-db-conn! nil repo)
_ (when-not config/publishing?
(let [dummy-notes (get-in dicts/dicts [:en :tutorial/dummy-notes])]
(create-dummy-notes-page repo dummy-notes)))
_ (when-not config/publishing?
(let [tutorial (get-in dicts/dicts [:en :tutorial/text])
tutorial (string/replace-first tutorial "$today" (date/today))]
(create-today-journal-if-not-exists repo tutorial)))
_ (create-config-file-if-not-exists repo)
_ (create-contents-file repo)]
(state/set-db-restoring! false)))
(js/setTimeout setup-local-repo-if-not-exists! 100)))
(defn periodically-pull
[repo-url pull-now?]
(when-let [token (state/get-github-token repo-url)]
(when pull-now? (pull repo-url token nil))
(js/setInterval #(pull repo-url token nil)
(* (config/git-pull-secs) 1000))))
(defn periodically-push-tasks
[repo-url]
(let [token (state/get-github-token repo-url)
push (fn []
(when (and (not (false? (:git-auto-push (state/get-config repo-url))))
;; (not config/dev?)
)
(push repo-url nil)))]
(js/setInterval push
(* (config/git-push-secs) 1000))))
(defn periodically-pull-and-push
[repo-url {:keys [pull-now?]
:or {pull-now? true}}]
(periodically-pull repo-url pull-now?)
(periodically-push-tasks repo-url))
(defn create-repo!
[repo-url branch]
(util/post (str config/api "repos")
{:url repo-url
:branch branch}
(fn [result]
(if (:installation_id result)
(set! (.-href js/window.location) config/website)
(set! (.-href js/window.location) (str "https://github.com/apps/" config/github-app-name "/installations/new"))))
(fn [error]
(println "Something wrong!")
(js/console.dir error))))
(defn clone-and-pull
[repo-url]
(->
(p/let [_ (clone repo-url)
_ (git-handler/git-set-username-email! repo-url (:me @state/state))]
(load-db-and-journals! repo-url nil true)
(periodically-pull-and-push repo-url {:pull-now? false})
;; (periodically-persist-app-metadata repo-url)
)
(p/catch (fn [error]
(js/console.error error)))))
(defn clone-and-pull-repos
[me]
(if (and js/window.git js/window.pfs)
(doseq [{:keys [id url]} (:repos me)]
(let [repo url]
(p/let [config-exists? (fs/file-exists?
(util/get-repo-dir url)
".git/config")]
(if (and config-exists?
(db/cloned? repo))
(do
(git-handler/git-set-username-email! repo me)
(periodically-pull-and-push repo {:pull-now? true})
;; (periodically-persist-app-metadata repo)
)
(clone-and-pull repo)))))
(js/setTimeout (fn []
(clone-and-pull-repos me))
500)))
(defn rebuild-index!
[{:keys [id url] :as repo}]
(db/remove-conn! url)
(db/clear-query-state!)
(state/clear-changed-files! url)
(-> (p/let [_ (db/remove-db! url)
_ (db/remove-files-db! url)]
(fs/rmdir (util/get-repo-dir url)))
(p/catch (fn [error]
(prn "Delete repo failed, error: " error)))
(p/finally (fn []
(clone-and-pull url)))))
(defn git-commit-and-push!
[commit-message]
(when-let [repo (state/get-current-repo)]
(push repo {:commit-message commit-message
:fallback? false
:force? true})))
(defn read-repair-journals!
[repo-url]
;; TODO: check file corrupts
)

View File

@@ -0,0 +1,127 @@
(ns frontend.handler.route
(:require [frontend.util :as util]
[reitit.frontend.easy :as rfe]
[reitit.frontend.history :as rfh]
[frontend.state :as state]
[goog.dom :as gdom]
[frontend.handler.ui :as ui-handler]
[frontend.db :as db]
[frontend.date :as date]
[clojure.string :as string]
[medley.core :as medley]
[frontend.text :as text]))
(defn redirect!
"If `push` is truthy, previous page will be left in history."
[{:keys [to path-params query-params push]
:or {push true}}]
(if push
(rfe/push-state to path-params query-params)
(rfe/replace-state to path-params query-params)))
(defn redirect-to-home!
[]
(redirect! {:to :home}))
(defn redirect-with-fragment!
[path]
(.pushState js/window.history nil "" path)
(rfh/-on-navigate @rfe/history path))
(defn get-title
[name path-params]
(case name
:home
"Logseq"
:repos
"Repos"
:repo-add
"Add another repo"
:graph
"Graph"
:all-files
"All files"
:all-pages
"All pages"
:all-journals
"All journals"
:file
(str "File " (util/url-decode (:path path-params)))
:new-page
"Create a new page"
:page
(let [name (:name path-params)
block? (util/uuid-string? name)]
(if block?
(if-let [block (db/entity [:block/uuid (medley/uuid name)])]
(let [content (text/remove-level-spaces (:block/content block)
(:block/format block))]
(if (> (count content) 48)
(str (subs content 0 48) "...")
content))
"Page no longer exists!!")
(util/capitalize-all (util/url-decode name))))
:tag
(str "#" (util/url-decode (:name path-params)))
:diff
"Git diff"
:draw
"Draw"
:settings
"Settings"
:import
"Import data into Logseq"
"Logseq"))
(defn set-route-match!
[route]
(swap! state/state assoc :route-match route)
(let [{:keys [data path-params]} route
title (get-title (:name data) path-params)]
(util/set-title! title)
(ui-handler/scroll-and-highlight! nil)))
(defn go-to-search!
[]
(when-let [element (gdom/getElement "search_field")]
(.focus element)))
(defn go-to-journals!
[]
(state/set-journals-length! 1)
(let [route (if (state/custom-home-page?)
:all-journals
:home)]
(redirect! {:to route}))
(util/scroll-to-top))
(defn- redirect-to-file!
[page]
(when-let [path (-> (db/get-page-file (string/lower-case page))
:db/id
(db/entity)
:file/path)]
(redirect! {:to :file
:path-params {:path path}})))
(defn toggle-between-page-and-file!
[]
(let [current-route (state/get-current-route)]
(case current-route
:home
(redirect-to-file! (date/today))
:all-journals
(redirect-to-file! (date/today))
:page
(when-let [page-name (get-in (state/get-route-match) [:path-params :name])]
(redirect-to-file! page-name))
:file
(when-let [path (get-in (state/get-route-match) [:path-params :path])]
(when-let [page (db/get-file-page path)]
(redirect! {:to :page
:path-params {:name page}})))
nil)))

View File

@@ -0,0 +1,20 @@
(ns frontend.handler.search
(:require [goog.object :as gobj]
[frontend.state :as state]
[goog.dom :as gdom]
[frontend.search :as search]))
(defn search
[q]
(swap! state/state assoc :search/result
{:pages (search/page-search q)
:files (search/file-search q)
:blocks (search/search q)}))
(defn clear-search!
[]
(swap! state/state assoc
:search/result nil
:search/q "")
(when-let [input (gdom/getElement "search_field")]
(gobj/set input "value" "")))

View File

@@ -0,0 +1,95 @@
(ns frontend.handler.ui
(:require [dommy.core :as dom]
[frontend.state :as state]
[frontend.db :as db]
[rum.core :as rum]
[goog.dom :as gdom]
[goog.object :as gobj]
[frontend.util :as util :refer-macros [profile]]))
;; sidebars
(defn hide-left-sidebar
[]
(dom/add-class! (dom/by-id "menu")
"md:block")
(dom/remove-class! (dom/by-id "left-sidebar")
"enter")
(dom/remove-class! (dom/by-id "search")
"sidebar-open")
(dom/remove-class! (dom/by-id "main")
"sidebar-open"))
(defn show-left-sidebar
[]
(dom/remove-class! (dom/by-id "menu")
"md:block")
(dom/add-class! (dom/by-id "left-sidebar")
"enter")
(dom/add-class! (dom/by-id "search")
"sidebar-open")
(dom/add-class! (dom/by-id "main")
"sidebar-open"))
(defn hide-right-sidebar
[]
(state/hide-right-sidebar!))
(defn show-right-sidebar
[]
(state/open-right-sidebar!))
(defn toggle-right-sidebar!
[]
(state/toggle-sidebar-open?!))
;; FIXME: re-render all embedded blocks since they will not be re-rendered automatically
(defn re-render-root!
[]
(when-let [component (state/get-root-component)]
(db/clear-query-state-without-refs-and-embeds!)
(rum/request-render component)
(doseq [component (state/get-custom-query-components)]
(rum/request-render component))))
(defn re-render-file!
[]
(when-let [component (state/get-file-component)]
(when (= :file (state/get-current-route))
(rum/request-render component))))
(defn highlight-element!
[fragment]
(let [id (and
(> (count fragment) 36)
(subs fragment (- (count fragment) 36)))]
(if (and id (util/uuid-string? id))
(let [elements (array-seq (js/document.getElementsByClassName id))]
(when (first elements)
(util/scroll-to-element (gobj/get (first elements) "id")))
(doseq [element elements]
(dom/add-class! element "block-highlight")
(js/setTimeout #(dom/remove-class! element "block-highlight")
4000)))
(when-let [element (gdom/getElement fragment)]
(util/scroll-to-element fragment)
(dom/add-class! element "block-highlight")
(js/setTimeout #(dom/remove-class! element "block-highlight")
4000)))))
(defn scroll-and-highlight!
[state]
(when-let [fragment (util/get-fragment)]
(highlight-element! fragment))
state)
(defn add-style-if-exists!
[]
(when-let [style (or
(state/get-custom-css-link)
(db/get-custom-css)
;; (state/get-custom-css-link)
)]
(util/add-style! style)))

View File

@@ -0,0 +1,78 @@
(ns frontend.handler.user
(:require [frontend.util :as util :refer-macros [profile]]
[frontend.state :as state]
[frontend.db :as db]
[frontend.config :as config]
[frontend.storage :as storage]
[promesa.core :as p]
[goog.object :as gobj]
[frontend.handler.notification :as notification])
(:import [goog.format EmailAddress]))
(defn email? [v]
(and v
(.isValid (EmailAddress. v))))
(defn set-email!
[email]
(when (email? email)
(util/post (str config/api "email")
{:email email}
(fn [result]
(db/transact! [{:me/email email}])
(swap! state/state assoc-in [:me :email] email))
(fn [error]
(notification/show! "Email already exists!"
:error)))))
(defn set-cors!
[cors-proxy]
(util/post (str config/api "cors_proxy")
{:cors-proxy cors-proxy}
(fn [result]
(db/transact! [{:me/cors_proxy cors-proxy}])
(swap! state/state assoc-in [:me :cors_proxy] cors-proxy))
(fn [error]
(notification/show! "Set cors proxy failed." :error)
(js/console.dir error))))
(defn set-preferred-format!
[format]
(when format
(state/set-preferred-format! format)
(when (:name (:me @state/state))
(util/post (str config/api "set_preferred_format")
{:preferred_format (name format)}
(fn [_result]
(notification/show! "Format set successfully!" :success))
(fn [_e])))))
(defn set-preferred-workflow!
[workflow]
(when workflow
(state/set-preferred-workflow! workflow)
(when (:name (:me @state/state))
(util/post (str config/api "set_preferred_workflow")
{:preferred_workflow (name workflow)}
(fn [_result]
(notification/show! "Workflow set successfully!" :success))
(fn [_e])))))
(defn- clear-store!
[]
(p/let [_ (.clear db/localforage-instance)
dbs (js/window.indexedDB.databases)]
(doseq [db dbs]
(js/window.indexedDB.deleteDatabase (gobj/get db "name")))))
(defn sign-out!
[e]
(->
(do
(storage/clear)
(clear-store!))
(p/catch (fn [e]
(println "sign out error: ")
(js/console.dir e)))
(p/finally (fn []
(set! (.-href js/window.location) "/logout")))))