handler.cljs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533
  1. (ns frontend.handler
  2. (:refer-clojure :exclude [clone load-file])
  3. (:require [frontend.git :as git]
  4. [frontend.fs :as fs]
  5. [frontend.state :as state]
  6. [frontend.db :as db]
  7. [frontend.storage :as storage]
  8. [frontend.util :as util]
  9. [frontend.config :as config]
  10. [clojure.walk :as walk]
  11. [clojure.string :as string]
  12. [promesa.core :as p]
  13. [cljs-bean.core :as bean]
  14. [reitit.frontend.easy :as rfe]
  15. [goog.crypt.base64 :as b64]
  16. [goog.object :as gobj]
  17. [goog.dom :as gdom]
  18. [rum.core :as rum]
  19. [datascript.core :as d]
  20. [frontend.utf8 :as utf8]
  21. [frontend.image :as image])
  22. (:import [goog.events EventHandler]))
  23. ;; We only support Github token now
  24. (defn load-file
  25. [repo-url path state-handler]
  26. (util/p-handle (fs/read-file (git/get-repo-dir repo-url) path)
  27. (fn [content]
  28. (state-handler content))))
  29. (defn- hidden?
  30. [path patterns]
  31. (some (fn [pattern]
  32. (or
  33. (= path pattern)
  34. (and (string/starts-with? pattern "/")
  35. (= (str "/" (first (string/split path #"/")))
  36. pattern)))) patterns))
  37. (defn load-files
  38. [repo-url]
  39. (util/p-handle (git/list-files repo-url)
  40. (fn [files]
  41. (when (> (count files) 0)
  42. (let [files (js->clj files)]
  43. ;; FIXME: don't load blobs
  44. (if (contains? (set files) config/hidden-file)
  45. (load-file repo-url config/hidden-file
  46. (fn [patterns-content]
  47. (when patterns-content
  48. (let [patterns (string/split patterns-content #"\n")
  49. files (remove (fn [path] (hidden? path patterns)) files)]
  50. (db/transact-files! repo-url files)))))
  51. (p/promise (db/transact-files! repo-url files))))))))
  52. ;; TODO: remove this
  53. (declare load-repo-to-db!)
  54. (defn get-latest-commit
  55. [handler]
  56. (-> (git/log (db/get-current-repo)
  57. (db/get-github-token)
  58. 1)
  59. (.then (fn [commits]
  60. (handler (first commits))))
  61. (.catch (fn [error]
  62. (prn "get latest commit failed: " error)))))
  63. (defonce latest-commit (atom nil))
  64. ;; TODO: Maybe replace with fetch?
  65. ;; TODO: callback hell
  66. (defn pull
  67. [repo-url token]
  68. (when (and (nil? (:git-error @state/state))
  69. (nil? (:git-status @state/state)))
  70. (util/p-handle
  71. (git/pull repo-url token)
  72. (fn [result]
  73. (prn "pull successfully!")
  74. (get-latest-commit
  75. (fn [commit]
  76. (when (or (nil? @latest-commit)
  77. (and @latest-commit
  78. commit
  79. (not= (gobj/get commit "oid")
  80. (gobj/get @latest-commit "oid"))))
  81. (prn "New commit oid: " (gobj/get commit "oid"))
  82. (-> (load-files repo-url)
  83. (p/then
  84. (fn []
  85. (load-repo-to-db! repo-url)))))
  86. (reset! latest-commit commit)))))))
  87. (defn periodically-pull
  88. [repo-url]
  89. (when-let [token (db/get-github-token)]
  90. (pull repo-url token)
  91. (js/setInterval #(pull repo-url token)
  92. (* 60 1000))))
  93. (defn git-add-commit
  94. [repo-url file message content]
  95. (swap! state/state assoc :git-status :commit)
  96. (db/reset-file! repo-url file content)
  97. (git/add-commit repo-url file message
  98. (fn []
  99. (swap! state/state assoc
  100. :git-status :should-push))
  101. (fn [error]
  102. (prn "Commit failed, "
  103. {:repo repo-url
  104. :file file
  105. :message message})
  106. (swap! state/state assoc
  107. :git-status :commit-failed
  108. :git-error error))))
  109. ;; TODO: update latest commit
  110. (defn push
  111. [repo-url file]
  112. (when (and (= :should-push (:git-status @state/state))
  113. (nil? (:git-error @state/state)))
  114. (swap! state/state assoc :git-status :push)
  115. (let [token (db/get-github-token)]
  116. (util/p-handle
  117. (git/push repo-url token)
  118. (fn []
  119. (prn "Push successfully!")
  120. (swap! state/state assoc
  121. :git-status nil
  122. :git-error nil)
  123. ;; TODO: update latest-commit
  124. (get-latest-commit
  125. (fn [commit]
  126. (reset! latest-commit commit))))
  127. (fn [error]
  128. (prn "Failed to push, error: " error)
  129. (swap! state/state assoc
  130. :git-status :push-failed
  131. :git-error error))))))
  132. (defn clone
  133. [repo]
  134. (let [token (db/get-github-token)]
  135. (util/p-handle
  136. (do
  137. (db/set-repo-cloning repo true)
  138. (git/clone repo token))
  139. (fn []
  140. (db/set-repo-cloning repo false)
  141. (db/mark-repo-as-cloned repo)
  142. (db/set-current-repo! repo)
  143. ;; load contents
  144. (load-files repo))
  145. (fn [e]
  146. (db/set-repo-cloning repo false)
  147. (prn "Clone failed, reason: " e)))))
  148. (defn new-notification
  149. [text]
  150. (js/Notification. "Logseq" #js {:body text
  151. ;; :icon logo
  152. }))
  153. (defn request-notifications
  154. []
  155. (util/p-handle (.requestPermission js/Notification)
  156. (fn [result]
  157. (storage/set :notification-permission-asked? true)
  158. (when (= "granted" result)
  159. (storage/set :notification-permission? true)))))
  160. (defn request-notifications-if-not-asked
  161. []
  162. (when-not (storage/get :notification-permission-asked?)
  163. (request-notifications)))
  164. ;; notify deadline or scheduled tasks
  165. (defn run-notify-worker!
  166. []
  167. (when (storage/get :notification-permission?)
  168. (let [notify-fn (fn []
  169. (let [tasks (:tasks @state/state)
  170. tasks (flatten (vals tasks))]
  171. (doseq [{:keys [marker title] :as task} tasks]
  172. (when-not (contains? #{"DONE" "CANCElED" "CANCELLED"} marker)
  173. (doseq [[type {:keys [date time] :as timestamp}] (:timestamps task)]
  174. (let [{:keys [year month day]} date
  175. {:keys [hour min]
  176. :or {hour 9
  177. min 0}} time
  178. now (util/get-local-date)]
  179. (when (and (contains? #{"Scheduled" "Deadline"} type)
  180. (= (assoc date :hour hour :minute min) now))
  181. (let [notification-text (str type ": " (second (first title)))]
  182. (new-notification notification-text)))))))))]
  183. (notify-fn)
  184. (js/setInterval notify-fn (* 1000 60)))))
  185. (defn show-notification!
  186. [text]
  187. (swap! state/state assoc
  188. :notification/show? true
  189. :notification/text text)
  190. (js/setTimeout #(swap! state/state assoc
  191. :notification/show? false
  192. :notification/text nil)
  193. 3000))
  194. (defn alter-file
  195. ([path commit-message content]
  196. (alter-file path commit-message content true))
  197. ([path commit-message content redirect?]
  198. (let [token (db/get-github-token)
  199. repo-url (db/get-current-repo)]
  200. (util/p-handle
  201. (fs/write-file (git/get-repo-dir repo-url) path content)
  202. (fn [_]
  203. (when redirect?
  204. (rfe/push-state :file {:path (b64/encodeString path)}))
  205. (git-add-commit repo-url path commit-message content))))))
  206. (defn clear-storage
  207. [repo-url]
  208. (js/window.pfs._idb.wipe)
  209. (clone repo-url))
  210. ;; TODO: utf8 encode performance
  211. (defn check
  212. [heading]
  213. (let [{:heading/keys [repo file marker meta uuid]} heading
  214. pos (:pos meta)
  215. repo (db/entity (:db/id repo))
  216. file (db/entity (:db/id file))
  217. repo-url (:repo/url repo)
  218. file (:file/path file)
  219. token (db/get-github-token)]
  220. (when-let [content (db/get-file-content repo-url file)]
  221. (let [encoded-content (utf8/encode content)
  222. content' (str (utf8/substring encoded-content 0 pos)
  223. (-> (utf8/substring encoded-content pos)
  224. (string/replace-first marker "DONE")))]
  225. (util/p-handle
  226. (fs/write-file (git/get-repo-dir repo-url) file content')
  227. (fn [_]
  228. (prn "check successfully, " file)
  229. (git-add-commit repo-url file
  230. (util/format "`%s` marked as DONE." marker)
  231. content')))))))
  232. (defn uncheck
  233. [heading]
  234. (let [{:heading/keys [repo file marker meta]} heading
  235. pos (:pos meta)
  236. repo (db/entity (:db/id repo))
  237. file (db/entity (:db/id file))
  238. repo-url (:repo/url repo)
  239. file (:file/path file)
  240. token (db/get-github-token)]
  241. (when-let [content (db/get-file-content repo-url file)]
  242. (let [encoded-content (utf8/encode content)
  243. content' (str (utf8/substring encoded-content 0 pos)
  244. (-> (utf8/substring encoded-content pos)
  245. (string/replace-first "DONE" "TODO")))]
  246. (util/p-handle
  247. (fs/write-file (git/get-repo-dir repo-url) file content')
  248. (fn [_]
  249. (prn "uncheck successfully, " file)
  250. (git-add-commit repo-url file
  251. "DONE rollbacks to TODO."
  252. content')))))))
  253. (defn remove-non-text-files
  254. [files]
  255. (remove
  256. (fn [file]
  257. (not (contains?
  258. #{"org"
  259. "md"
  260. "markdown"
  261. "txt"}
  262. (string/lower-case (last (string/split file #"\."))))))
  263. files))
  264. (defn load-all-contents!
  265. [repo-url ok-handler]
  266. (let [files (db/get-repo-files repo-url)
  267. files (remove-non-text-files files)]
  268. (-> (p/all (for [file files]
  269. (load-file repo-url file
  270. (fn [content]
  271. (db/set-file-content! repo-url file content)))))
  272. (p/then
  273. (fn [_]
  274. (ok-handler))))))
  275. (defonce headings-atom (atom nil))
  276. (defn load-repo-to-db!
  277. [repo-url]
  278. (load-all-contents!
  279. repo-url
  280. (fn []
  281. (let [headings (db/extract-all-headings repo-url)]
  282. (reset! headings-atom headings)
  283. (db/reset-headings! repo-url headings)))))
  284. ;; (defn sync
  285. ;; []
  286. ;; (let [[_user token repos] (get-user-token-repos)]
  287. ;; (doseq [repo repos]
  288. ;; (pull repo token))))
  289. (defn get-github-access-token
  290. ([]
  291. (util/fetch (str config/api "token/github")
  292. (fn [resp]
  293. (if (:success resp)
  294. (db/transact-github-token! (get-in resp [:body :access_token]))
  295. (prn "Get token failed, error: " resp)))
  296. (fn [error]
  297. (prn "Get token failed, error: " error))))
  298. ([code]
  299. (util/fetch (str config/api "oauth/github?code=" code)
  300. (fn [resp]
  301. (if (:success resp)
  302. (do
  303. (db/transact-github-token! (get-in resp [:body :access_token]))
  304. ;; redirect to home
  305. (rfe/push-state :home))
  306. (prn "Get token failed, error: " resp)))
  307. (fn [error]
  308. (prn "Get token failed, error: " error)))))
  309. ;; org-journal format, something like `* Tuesday, 06/04/13`
  310. (defn default-month-journal-content
  311. []
  312. (let [{:keys [year month day]} (util/get-date)
  313. last-day (util/get-month-last-day)
  314. month-pad (if (< month 10) (str "0" month) month)]
  315. (->> (map
  316. (fn [day]
  317. (let [day-pad (if (< day 10) (str "0" day) day)
  318. weekday (util/get-weekday (js/Date. year (dec month) (dec day)))]
  319. (util/format "* %s, %s/%s/%d\n\n" weekday month-pad day-pad year)))
  320. (range 1 (inc last-day)))
  321. (apply str))))
  322. ;; journals
  323. (defn create-month-journal-if-not-exists
  324. [repo-url]
  325. (let [repo-dir (git/get-repo-dir repo-url)
  326. path (util/current-journal-path)
  327. file-path (str "/" path)
  328. default-content (default-month-journal-content)]
  329. (->
  330. (util/p-handle
  331. (fs/mkdir (str repo-dir "/journals"))
  332. (fn [result]
  333. (fs/create-if-not-exists repo-dir file-path default-content))
  334. (fn [error]
  335. (fs/create-if-not-exists repo-dir file-path default-content)))
  336. (util/p-handle
  337. (fn [file-exists?]
  338. (if file-exists?
  339. (prn "Month journal already exists!")
  340. (do
  341. (prn "create a month journal")
  342. (git-add-commit repo-url path "create a month journal" default-content))))
  343. (fn [error]
  344. (prn error))))))
  345. (defn clone-and-pull
  346. [repo]
  347. (p/then (clone repo)
  348. (fn []
  349. (create-month-journal-if-not-exists repo)
  350. (periodically-pull repo))))
  351. (defn set-route-match!
  352. [route]
  353. (swap! state/state assoc :route-match route))
  354. (defn set-ref-component!
  355. [k ref]
  356. (swap! state/state assoc :ref-components k ref))
  357. (defn set-root-component!
  358. [comp]
  359. (swap! state/state assoc :root-component comp))
  360. (defn re-render!
  361. []
  362. (when-let [comp (get @state/state :root-component)]
  363. (when-not (:edit? @state/state)
  364. (rum/request-render comp))))
  365. (defn db-listen-to-tx!
  366. []
  367. (d/listen! db/conn :persistence
  368. (fn [tx-report] ;; FIXME do not notify with nil as db-report
  369. ;; FIXME do not notify if tx-data is empty
  370. (when-let [db (:db-after tx-report)]
  371. (prn "DB changed, re-rendered!")
  372. (re-render!)
  373. (js/setTimeout (fn []
  374. (db/persist db)) 0)))))
  375. (defn periodically-push-tasks
  376. [repo-url]
  377. (let [token (db/get-github-token)
  378. push (fn []
  379. (push repo-url token))]
  380. (js/setInterval push
  381. (* 10 1000))))
  382. (defn periodically-pull-and-push
  383. [repo-url]
  384. (periodically-pull repo-url)
  385. ;; (periodically-push-tasks repo-url)
  386. )
  387. (defn set-state-kv!
  388. [key value]
  389. (swap! state/state assoc key value))
  390. (defn edit-journal!
  391. [content journal]
  392. (swap! state/state assoc
  393. :edit? true
  394. :edit-journal journal))
  395. (defn set-latest-journals!
  396. []
  397. (set-state-kv! :latest-journals (db/get-latest-journals {})))
  398. (defn set-journal-content!
  399. [uuid content]
  400. (swap! state/state update :latest-journals
  401. (fn [journals]
  402. (mapv
  403. (fn [journal]
  404. (if (= (:uuid journal) uuid)
  405. (assoc journal :content content)
  406. journal))
  407. journals))))
  408. (defn save-current-edit-journal!
  409. [edit-content]
  410. (let [{:keys [edit-journal]} @state/state
  411. {:keys [start-pos end-pos]} edit-journal]
  412. (swap! state/state assoc
  413. :edit? false
  414. :edit-journal nil)
  415. (when-not (= edit-content (:content edit-journal)) ; if new changes
  416. (let [path (:file-path edit-journal)
  417. current-journals (db/get-file path)
  418. new-content (utf8/insert! current-journals start-pos end-pos edit-content)]
  419. (set-state-kv! :latest-journals (db/get-latest-journals {:content new-content}))
  420. (alter-file path "Auto save" new-content false)))))
  421. (defn render-local-images!
  422. []
  423. (let [images (array-seq (gdom/getElementsByTagName "img"))
  424. get-src (fn [image] (.getAttribute image "src"))
  425. local-images (filter
  426. (fn [image]
  427. (let [src (get-src image)]
  428. (and src
  429. (not (or (string/starts-with? src "http://")
  430. (string/starts-with? src "https://"))))))
  431. images)]
  432. (doseq [img local-images]
  433. (gobj/set img
  434. "onerror"
  435. (fn []
  436. (gobj/set (gobj/get img "style")
  437. "display" "none")))
  438. (let [path (get-src img)
  439. path (if (= (first path) \.)
  440. (subs path 1)
  441. path)]
  442. (util/p-handle
  443. (fs/read-file-2 (git/get-repo-dir (db/get-current-repo))
  444. path)
  445. (fn [blob]
  446. (let [blob (js/Blob. (array blob) (clj->js {:type "image"}))
  447. img-url (image/create-object-url blob)]
  448. (gobj/set img "src" img-url)
  449. (gobj/set (gobj/get img "style")
  450. "display" "initial"))))))))
  451. ;; FIXME:
  452. (defn set-username-email
  453. []
  454. (git/set-username-email
  455. (git/get-repo-dir (db/get-current-repo))
  456. "Tienson Qin"
  457. "[email protected]"))
  458. (defn load-more-journals!
  459. []
  460. (let [journals (:latest-journals @state/state)]
  461. (when-let [title (:title (last journals))]
  462. (let [before-date (last (string/split title #", "))
  463. more-journals (->> (db/get-latest-journals {:before-date before-date
  464. :days 4})
  465. (drop 1))
  466. journals (concat journals more-journals)]
  467. (set-state-kv! :latest-journals journals)))))
  468. (defn start!
  469. []
  470. (db/restore!)
  471. (db-listen-to-tx!)
  472. (when-let [first-repo (first (db/get-repos))]
  473. (db/set-current-repo! first-repo))
  474. (let [repos (db/get-repos)]
  475. (doseq [repo repos]
  476. (create-month-journal-if-not-exists repo)
  477. (periodically-pull-and-push repo))))
  478. (comment
  479. (util/p-handle (fs/read-file (git/get-repo-dir (db/get-current-repo)) "test.org")
  480. (fn [content]
  481. (prn content)))
  482. (pull (db/get-current-repo) (db/get-github-token))
  483. )