file_sync.cljs 39 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869
  1. (ns frontend.components.file-sync
  2. (:require [cljs.core.async :as async]
  3. [cljs.core.async.interop :refer [p->c]]
  4. [frontend.util.persist-var :as persist-var]
  5. [clojure.string :as string]
  6. [electron.ipc :as ipc]
  7. [frontend.components.lazy-editor :as lazy-editor]
  8. [frontend.components.onboarding.quick-tour :as quick-tour]
  9. [frontend.components.page :as page]
  10. [frontend.config :as config]
  11. [frontend.db :as db]
  12. [frontend.db.model :as db-model]
  13. [frontend.fs :as fs]
  14. [frontend.fs.sync :as fs-sync]
  15. [frontend.handler.file-sync :refer [*beta-unavailable?] :as file-sync-handler]
  16. [frontend.handler.notification :as notification]
  17. [frontend.handler.repo :as repo-handler]
  18. [frontend.handler.user :as user-handler]
  19. [frontend.handler.page :as page-handler]
  20. [frontend.handler.file-based.nfs :as nfs-handler]
  21. [frontend.mobile.util :as mobile-util]
  22. [frontend.state :as state]
  23. [frontend.ui :as ui]
  24. [frontend.util :as util]
  25. [frontend.util.fs :as fs-util]
  26. [frontend.storage :as storage]
  27. [logseq.shui.ui :as shui]
  28. [promesa.core :as p]
  29. [reitit.frontend.easy :as rfe]
  30. [rum.core :as rum]
  31. [cljs-time.core :as t]
  32. [cljs-time.coerce :as tc]
  33. [goog.functions :refer [debounce]]
  34. [logseq.common.util :as common-util]))
  35. (declare maybe-onboarding-show)
  36. (declare open-icloud-graph-clone-picker)
  37. (rum/defc clone-local-icloud-graph-panel
  38. [repo graph-name close-fn]
  39. (rum/use-effect!
  40. #(some->> (state/sub :file-sync/jstour-inst)
  41. (.complete))
  42. [])
  43. (let [graph-dir (config/get-repo-dir repo)
  44. [selected-path set-selected-path] (rum/use-state "")
  45. selected-path? (and (not (string/blank? selected-path))
  46. (not (mobile-util/in-iCloud-container-path? selected-path)))
  47. on-confirm (fn []
  48. (when-let [dest-dir (and selected-path?
  49. ;; avoid using `util/node-path.join` to join mobile path since it replaces `file:///abc` to `file:/abc`
  50. (str (string/replace selected-path #"/+$" "") "/" graph-name))]
  51. (-> (cond
  52. (util/electron?)
  53. (ipc/ipc :copyDirectory graph-dir dest-dir)
  54. (mobile-util/native-ios?)
  55. (fs/copy! repo graph-dir dest-dir)
  56. :else
  57. nil)
  58. (.then #(do
  59. (notification/show! (str "Cloned to => " dest-dir) :success)
  60. (nfs-handler/ls-dir-files-with-path! dest-dir)
  61. (repo-handler/remove-repo! {:url repo})
  62. (close-fn)))
  63. (.catch #(js/console.error %)))))]
  64. [:div.cp__file-sync-related-normal-modal
  65. [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "folders")]]
  66. [:h1.text-xl.font-semibold.opacity-90.text-center.py-2
  67. "Clone your local graph away from " [:strong "☁️"] " iCloud!"]
  68. [:h2.text-center.opacity-70.text-xs.leading-5
  69. "Unfortunately, Logseq Sync and iCloud don't work perfectly together at the moment. To make sure"
  70. [:br]
  71. "You can always delete the remote graph at a later point."]
  72. [:div.folder-tip.flex.flex-col.items-center
  73. [:h3
  74. [:span (ui/icon "folder") [:label.pl-0.5 (common-util/safe-decode-uri-component graph-name)]]]
  75. [:h4.px-6 (config/get-string-repo-dir repo)]
  76. (when (not (string/blank? selected-path))
  77. [:h5.text-xs.pt-1.-mb-1.flex.items-center.leading-none
  78. (if (mobile-util/in-iCloud-container-path? selected-path)
  79. [:span.inline-block.pr-1.text-error.scale-75 (ui/icon "alert-circle")]
  80. [:span.inline-block.pr-1.text-success.scale-75 (ui/icon "circle-check")])
  81. selected-path])
  82. [:div.out-icloud
  83. (ui/button
  84. [:span.inline-flex.items-center.leading-none.opacity-90
  85. "Select new parent folder outside of iCloud" (ui/icon "arrow-right")]
  86. :on-click
  87. (fn []
  88. ;; TODO: support mobile
  89. (cond
  90. (util/electron?)
  91. (p/let [path (ipc/ipc "openDialog")]
  92. (set-selected-path path))
  93. (mobile-util/native-ios?)
  94. (p/let [{:keys [path _localDocumentsPath]}
  95. (p/chain
  96. (.pickFolder mobile-util/folder-picker)
  97. #(js->clj % :keywordize-keys true))]
  98. (set-selected-path path))
  99. :else
  100. nil)))]]
  101. [:p.flex.items-center.space-x-2.pt-6.flex.justify-center.sm:justify-end.-mb-2
  102. (ui/button "Cancel" :background "gray" :class "opacity-50" :on-click close-fn)
  103. (ui/button "Clone graph" :disabled (not selected-path?) :on-click on-confirm)]]))
  104. (rum/defc create-remote-graph-panel
  105. [repo graph-name close-fn]
  106. (rum/use-effect!
  107. #(some->> (state/sub :file-sync/jstour-inst)
  108. (.complete))
  109. [])
  110. (let [on-confirm
  111. (fn []
  112. (async/go
  113. (close-fn)
  114. (if (mobile-util/in-iCloud-container-path? repo)
  115. (open-icloud-graph-clone-picker repo)
  116. (do
  117. (state/set-state! [:ui/loading? :graph/create-remote?] true)
  118. (when-let [GraphUUID (get (async/<! (file-sync-handler/create-graph graph-name)) 2)]
  119. (async/<! (fs-sync/<sync-start))
  120. (state/set-state! [:ui/loading? :graph/create-remote?] false)
  121. ;; update both local && remote graphs
  122. (state/add-remote-graph! {:GraphUUID GraphUUID
  123. :GraphName graph-name})
  124. (state/set-repos! (map (fn [r]
  125. (if (= (:url r) repo)
  126. (assoc r
  127. :GraphUUID GraphUUID
  128. :GraphName graph-name
  129. :remote? true)
  130. r))
  131. (state/get-repos))))))))]
  132. [:div.cp__file-sync-related-normal-modal
  133. [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "cloud-upload" {:size 20})]]
  134. [:h1.text-xl.font-semibold.opacity-90.text-center.py-2
  135. "Are you sure you want to create a new remote graph?"]
  136. [:h2.text-center.opacity-70.text-xs
  137. "By continuing this action you will create an encrypted cloud version of your current local graph." [:br]
  138. "You can always delete the remote graph at a later point."]
  139. [:div.folder-tip.flex.flex-col.items-center
  140. [:h3
  141. [:span (ui/icon "folder") [:label.pl-0.5 graph-name]]
  142. [:span.opacity-50.scale-75 (ui/icon "arrow-right")]
  143. [:span (ui/icon "cloud-lock")]]
  144. [:h4.px-4 (config/get-string-repo-dir repo)]]
  145. [:p.flex.items-center.space-x-2.pt-6.flex.justify-center.sm:justify-end.-mb-2
  146. (ui/button "Cancel" :background "gray" :class "opacity-50" :on-click close-fn)
  147. (ui/button "Create remote graph" :on-click on-confirm)]]))
  148. (rum/defc indicator-progress-pie
  149. [percentage]
  150. (let [*el (rum/use-ref nil)]
  151. (rum/use-effect!
  152. #(when-let [^js el (rum/deref *el)]
  153. (set! (.. el -style -backgroundImage)
  154. (util/format "conic-gradient(var(--ls-pie-fg-color) %s%, var(--ls-pie-bg-color) %s%)" percentage percentage)))
  155. [percentage])
  156. [:span.cp__file-sync-indicator-progress-pie {:ref *el}]))
  157. (rum/defc last-synced-cp < rum/reactive
  158. []
  159. (let [last-synced-at (state/sub [:file-sync/graph-state
  160. (state/get-current-file-sync-graph-uuid)
  161. :file-sync/last-synced-at])
  162. last-synced-at (if last-synced-at
  163. (util/time-ago (tc/from-long (* last-synced-at 1000)))
  164. "just now")]
  165. [:div.cl
  166. [:span.opacity-60 "Last change was"]
  167. [:span.pl-1 last-synced-at]]))
  168. (rum/defc sync-now
  169. []
  170. (ui/button "Sync now"
  171. :class "block cursor-pointer"
  172. :small? true
  173. :on-click #(async/offer! fs-sync/immediately-local->remote-chan true)
  174. :style {:color "#ffffff"}))
  175. (def *last-calculated-time (atom nil))
  176. (rum/defc ^:large-vars/cleanup-todo indicator-progress-pane
  177. [sync-state sync-progress
  178. {:keys [idle? syncing? no-active-files? online? history-files? queuing?]}]
  179. (rum/use-effect!
  180. (fn []
  181. #(reset! *last-calculated-time nil))
  182. [])
  183. (let [uploading-files (:current-local->remote-files sync-state)
  184. downloading-files (:current-remote->local-files sync-state)
  185. uploading? (seq uploading-files)
  186. downloading? (seq downloading-files)
  187. progressing? (or uploading? downloading?)
  188. full-upload-files (:full-local->remote-files sync-state)
  189. full-download-files (:full-remote->local-files sync-state)
  190. calc-progress-total #(cond
  191. uploading? (count full-upload-files)
  192. downloading? (count full-download-files)
  193. :else 0)
  194. calc-progress-finished (fn []
  195. (let [current-sync-files (set
  196. (->> (or (seq full-upload-files) (seq full-download-files))
  197. (map :path)))]
  198. (count (filter #(and (= (:percent (second %)) 100)
  199. (contains? current-sync-files (first %))) sync-progress))))
  200. calc-time-left (fn [] (let [last-calculated-at (:calculated-at @*last-calculated-time)
  201. now (tc/to-epoch (t/now))]
  202. (if (and last-calculated-at (< (- now last-calculated-at) 10))
  203. (:result @*last-calculated-time)
  204. (let [result (file-sync-handler/calculate-time-left sync-state sync-progress)]
  205. (reset! *last-calculated-time {:calculated-at now
  206. :result result})
  207. result))))
  208. p-total (if syncing? (calc-progress-total) 0)
  209. p-finished (if syncing? (calc-progress-finished) 0)
  210. tip-b&p (if (and syncing? progressing?)
  211. [[:span (util/format "%s of %s files" p-finished p-total)]
  212. [:div.progress-bar [:i {:style
  213. {:width (str (if (> p-total 0)
  214. (* (/ p-finished p-total) 100) 0) "%")}}]]]
  215. [[:span.opacity-60 "all file edits"]
  216. (last-synced-cp)])
  217. *el-ref (rum/use-ref nil)
  218. [list-active?, set-list-active?] (rum/use-state
  219. (-> (storage/get :ui/file-sync-active-file-list?)
  220. (#(if (nil? %) true %))))]
  221. (rum/use-effect!
  222. (fn []
  223. (when-let [^js outer-class-list
  224. (some-> (rum/deref *el-ref)
  225. (.closest ".menu-links-outer")
  226. (.-classList))]
  227. (->> "is-list-active"
  228. (#(if list-active?
  229. (.add outer-class-list %)
  230. (.remove outer-class-list %))))
  231. (storage/set :ui/file-sync-active-file-list? list-active?)))
  232. [list-active?])
  233. (let [idle-&-no-active? (and idle? no-active-files?)
  234. waiting? (not (or (not online?)
  235. idle-&-no-active?
  236. syncing?))]
  237. [:div.cp__file-sync-indicator-progress-pane
  238. {:ref *el-ref
  239. :class (when (and syncing? progressing?) "is-progress-active")}
  240. [:div.a
  241. [:div.al
  242. [:strong
  243. {:class (when idle-&-no-active? "is-no-active")}
  244. (cond
  245. (not online?) (ui/icon "wifi-off")
  246. uploading? (ui/icon "arrow-up")
  247. downloading? (ui/icon "arrow-down")
  248. :else (ui/icon "thumb-up"))]
  249. [:span
  250. (cond
  251. (not online?) "Currently having connection issues..."
  252. idle-&-no-active? "Everything is synced!"
  253. syncing? "Currently syncing your graph..."
  254. :else "Waiting...")]]
  255. [:div.ar
  256. (when queuing? (sync-now))]]
  257. (when-not waiting?
  258. [:div.b.dark:text-gray-200
  259. [:div.bl
  260. [:span.flex.items-center
  261. (if no-active-files?
  262. [:span.opacity-100.pr-1 "Successfully processed"]
  263. [:span.opacity-60.pr-1 "Processed"])]
  264. (first tip-b&p)]
  265. [:div.br
  266. [:small.opacity-50
  267. (when syncing?
  268. (calc-time-left))]]])
  269. [:div.c
  270. {:class (when waiting? "pt-2")}
  271. (second tip-b&p)
  272. (when (or history-files? (not no-active-files?))
  273. [:span.inline-flex.pl-2.active:opacity-50
  274. {:on-click #(set-list-active? (not list-active?))}
  275. (if list-active?
  276. (ui/icon "chevron-up" {:style {:font-size 24}})
  277. (ui/icon "chevron-left" {:style {:font-size 24}}))])]])))
  278. (defn- sort-files
  279. [files]
  280. (sort-by (fn [f] (or (:size f) 0)) > files))
  281. (rum/defcs ^:large-vars/cleanup-todo indicator <
  282. rum/reactive
  283. {:key-fn #(identity "file-sync-indicator")}
  284. {:will-mount (fn [state]
  285. (let [unsub-fn (file-sync-handler/setup-file-sync-event-listeners)]
  286. (assoc state ::unsub-events unsub-fn)))
  287. :will-unmount (fn [state]
  288. (apply (::unsub-events state) nil)
  289. state)}
  290. [_state]
  291. (let [_ (state/sub :auth/id-token)
  292. online? (state/sub :network/online?)
  293. enabled-progress-panel? true
  294. current-repo (state/get-current-repo)
  295. creating-remote-graph? (state/sub [:ui/loading? :graph/create-remote?])
  296. current-graph-id (state/sub-current-file-sync-graph-uuid)
  297. sync-state (state/sub-file-sync-state current-graph-id)
  298. sync-progress (state/sub [:file-sync/graph-state
  299. current-graph-id
  300. :file-sync/progress])
  301. _ (rum/react file-sync-handler/refresh-file-sync-component)
  302. synced-file-graph? (file-sync-handler/synced-file-graph? current-repo)
  303. uploading-files (sort-files (:current-local->remote-files sync-state))
  304. downloading-files (sort-files (:current-remote->local-files sync-state))
  305. queuing-files (:queued-local->remote-files sync-state)
  306. history-files (:history sync-state)
  307. status (:state sync-state)
  308. status (or (nil? status) (keyword (name status)))
  309. off? (fs-sync/sync-off? sync-state)
  310. full-syncing? (contains? #{:local->remote-full-sync :remote->local-full-sync} status)
  311. syncing? (or full-syncing? (contains? #{:local->remote :remote->local} status))
  312. idle? (contains? #{:idle} status)
  313. need-password? (and (contains? #{:need-password} status)
  314. (not (fs-sync/graph-encrypted?)))
  315. queuing? (and idle? (boolean (seq queuing-files)))
  316. no-active-files? (empty? (concat downloading-files queuing-files uploading-files))
  317. create-remote-graph-fn #(when (and current-repo (not (config/demo-graph? current-repo)))
  318. (let [graph-name
  319. (js/decodeURI (util/node-path.basename current-repo))
  320. confirm-fn
  321. (fn [{:keys [close]}]
  322. (create-remote-graph-panel current-repo graph-name close))]
  323. (shui/dialog-open! confirm-fn {:center? true :close-btn? false})))
  324. turn-on (->
  325. (fn []
  326. (when-not (file-sync-handler/current-graph-sync-on?)
  327. (async/go
  328. (let [graphs-txid fs-sync/graphs-txid]
  329. (async/<! (p->c (persist-var/-load graphs-txid)))
  330. (cond
  331. @*beta-unavailable?
  332. (state/pub-event! [:file-sync/onboarding-tip :unavailable])
  333. ;; current graph belong to other user, do nothing
  334. (let [user-uuid (async/<! (user-handler/<user-uuid))
  335. user-uuid (when-not (instance? ExceptionInfo user-uuid) user-uuid)]
  336. (and (first @graphs-txid)
  337. user-uuid
  338. (not (fs-sync/check-graph-belong-to-current-user
  339. user-uuid
  340. (first @graphs-txid)))))
  341. nil
  342. (and (second @graphs-txid)
  343. (fs-sync/graph-sync-off? (second @graphs-txid))
  344. (async/<! (fs-sync/<check-remote-graph-exists (second @graphs-txid))))
  345. (fs-sync/<sync-start)
  346. ;; remote graph already has been deleted, clear repos first, then create-remote-graph
  347. (second @graphs-txid) ; <check-remote-graph-exists -> false
  348. (do (state/set-repos!
  349. (map (fn [r]
  350. (if (= (:url r) current-repo)
  351. (dissoc r :GraphUUID :GraphName :remote?)
  352. r))
  353. (state/get-repos)))
  354. (create-remote-graph-fn))
  355. (second @graphs-txid) ; sync not started yet
  356. nil
  357. :else
  358. (create-remote-graph-fn))))))
  359. (debounce 1500))]
  360. (if creating-remote-graph?
  361. (ui/loading "")
  362. [:div.cp__file-sync-indicator
  363. {:class (util/classnames
  364. [{:is-enabled-progress-pane enabled-progress-panel?
  365. :has-active-files (not no-active-files?)}
  366. (str "status-of-" (and (keyword? status) (name status)))])}
  367. (when (and (not config/publishing?)
  368. (user-handler/logged-in?))
  369. (ui/dropdown-with-links
  370. ;; trigger
  371. (fn [{:keys [toggle-fn]}]
  372. (if (not off?)
  373. [:a.button.cloud.on
  374. {:on-click toggle-fn
  375. :class (util/classnames [{:syncing syncing?
  376. :is-full full-syncing?
  377. :queuing queuing?
  378. :idle (and (not queuing?) idle?)}])}
  379. [:span.flex.items-center
  380. (ui/icon "cloud" {:size ui/icon-size})]]
  381. [:a.button.cloud.off
  382. {:on-click turn-on}
  383. (ui/icon "cloud-off" {:size ui/icon-size})]))
  384. ;; links
  385. (cond-> (vec
  386. (when-not (and no-active-files? idle?)
  387. (cond
  388. need-password?
  389. [{:title [:div.file-item.flex.items-center.leading-none.pt-3
  390. {:style {:margin-left -8}}
  391. (ui/icon "lock" {:size 20}) [:span.pl-1.font-semibold "Password is required"]]
  392. :options {:on-click fs-sync/sync-need-password!}}]
  393. ;; head of upcoming sync
  394. (not no-active-files?)
  395. [{:title [:div.file-item.is-first ""]
  396. :options {:class "is-first-placeholder"}}])))
  397. synced-file-graph?
  398. (concat
  399. (map (fn [f] {:title [:div.file-item
  400. {:key (str "downloading-" f)}
  401. f]
  402. :key (str "downloading-" f)
  403. :icon (if enabled-progress-panel?
  404. (let [progress (get sync-progress f)
  405. percent (or (:percent progress) 0)]
  406. (if (and (number? percent)
  407. (< percent 100))
  408. (indicator-progress-pie percent)
  409. (ui/icon "circle-check")))
  410. (ui/icon "arrow-narrow-down"))}) downloading-files)
  411. (map (fn [e] (let [icon (case (.-type e)
  412. "add" "plus"
  413. "unlink" "minus"
  414. "edit")
  415. path (fs-sync/relative-path e)]
  416. {:title [:div.file-item
  417. {:key (str "queue-" path)}
  418. path]
  419. :key (str "queue-" path)
  420. :icon (ui/icon icon)})) (take 10 queuing-files))
  421. (map (fn [f] {:title [:div.file-item
  422. {:key (str "uploading-" f)}
  423. f]
  424. :key (str "uploading-" f)
  425. :icon (if enabled-progress-panel?
  426. (let [progress (get sync-progress f)
  427. percent (or (:percent progress) 0)]
  428. (if (and (number? percent)
  429. (< percent 100))
  430. (indicator-progress-pie percent)
  431. (ui/icon "circle-check")))
  432. (ui/icon "arrow-up"))}) uploading-files)
  433. (when (seq history-files)
  434. (map-indexed (fn [i f] (:time f)
  435. (when-let [path (:path f)]
  436. (let [full-path (util/node-path.join (config/get-repo-dir current-repo) path)
  437. page-name (db/get-file-page full-path)]
  438. {:title [:div.files-history.cursor-pointer
  439. {:key i :class (when (= i 0) "is-first")
  440. :on-click (fn []
  441. (if page-name
  442. (rfe/push-state :page {:name page-name})
  443. (rfe/push-state :file {:path full-path})))}
  444. [:span.file-sync-item (:path f)]
  445. [:div.opacity-50 (ui/humanity-time-ago (:time f) nil)]]})))
  446. (take 10 history-files)))))
  447. ;; options
  448. {:outer-header
  449. [:<>
  450. (indicator-progress-pane
  451. sync-state sync-progress
  452. {:idle? idle?
  453. :syncing? syncing?
  454. :need-password? need-password?
  455. :full-sync? full-syncing?
  456. :online? online?
  457. :queuing? queuing?
  458. :no-active-files? no-active-files?
  459. :history-files? (seq history-files)})
  460. (when (and
  461. (not enabled-progress-panel?)
  462. synced-file-graph? queuing?)
  463. [:div.head-ctls (sync-now)])]}))])))
  464. (rum/defc pick-local-graph-for-sync [graph]
  465. [:div.cp__file-sync-related-normal-modal
  466. [:div.flex.justify-center.pb-4
  467. [:span.icon-wrap (ui/icon "cloud-download" {:size 22})]]
  468. [:h1.mb-5.text-2xl.text-center.font-bold
  469. (util/format "Sync graph \"%s\" to local" (:GraphName graph))]
  470. (ui/button
  471. "Open a local directory"
  472. :class "block w-full mt-4"
  473. :size :lg
  474. :on-click #(do
  475. (state/close-modal!)
  476. (fs-sync/<sync-stop)
  477. (->
  478. (page-handler/ls-dir-files!
  479. (fn [{:keys [url]}]
  480. (file-sync-handler/init-remote-graph url graph)
  481. (js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))
  482. {:on-open-dir
  483. (fn [result]
  484. (prn ::on-open-dir result)
  485. (let [empty-dir? (not (seq (:files result)))
  486. root (:path result)]
  487. (cond
  488. (string/blank? root)
  489. (p/rejected (js/Error. nil)) ;; cancel pick a directory
  490. empty-dir?
  491. (p/resolved nil)
  492. :else ; dir is not empty
  493. (-> (if (util/electron?)
  494. (ipc/ipc :readGraphTxIdInfo root)
  495. (fs-util/read-graphs-txid-info root))
  496. (p/then (fn [^js info]
  497. (when (or (nil? info)
  498. (nil? (second info))
  499. (not= (second info) (:GraphUUID graph)))
  500. (if (js/confirm "This directory is not empty, are you sure to sync the remote graph to it? Make sure to back up the directory first.")
  501. (p/resolved nil)
  502. (p/rejected (js/Error. nil))))))))))}) ;; cancel pick a non-empty directory
  503. (p/catch (fn [])))))
  504. [:div.text-xs.opacity-50.px-1.flex-row.flex.items-center.p-2
  505. (ui/icon "alert-circle")
  506. [:span.ml-1 " An empty directory or an existing remote graph!"]]])
  507. (defn pick-dest-to-sync-panel [graph]
  508. (fn []
  509. (pick-local-graph-for-sync graph)))
  510. (rum/defc page-history-list
  511. [graph-uuid page-entity set-list-ready? set-page]
  512. (let [[version-files set-version-files] (rum/use-state nil)
  513. [current-page set-current-page] (rum/use-state nil)
  514. [loading? set-loading?] (rum/use-state false)
  515. set-page-fn (fn [page-meta]
  516. (set-current-page page-meta)
  517. (set-page page-meta))
  518. get-version-key #(or (:VersionUUID %) (:relative-path %))]
  519. ;; fetch version files
  520. (rum/use-effect!
  521. (fn []
  522. (when-not loading?
  523. (async/go
  524. (set-loading? true)
  525. (try
  526. (let [files (async/<! (file-sync-handler/<fetch-page-file-versions graph-uuid page-entity))]
  527. (set-version-files files)
  528. (set-page-fn (first files))
  529. (set-list-ready? true))
  530. (finally (set-loading? false)))))
  531. #())
  532. [])
  533. [:div.version-list
  534. (if loading?
  535. [:div.p-4 (ui/loading)]
  536. (for [version version-files]
  537. (let [version-uuid (get-version-key version)
  538. local? (some? (:relative-path version))]
  539. [:div.version-list-item {:key version-uuid}
  540. [:a.item-link.block.fade-link.flex.justify-between
  541. {:title version-uuid
  542. :class (util/classnames
  543. [{:active (and current-page (= version-uuid (get-version-key current-page)))}])
  544. :on-click #(set-page-fn version)}
  545. [:div.text-sm.pt-1
  546. (ui/humanity-time-ago
  547. (or (:CreateTime version)
  548. (:create-time version)) nil)]
  549. [:small.opacity-50.translate-y-1.flex.items-center.space-x-1
  550. (if local?
  551. [:<> (ui/icon "git-commit") [:span "local"]]
  552. [:<> (ui/icon "cloud") [:span "remote"]])]]])))]))
  553. (rum/defc pick-page-histories-for-sync
  554. [repo-url graph-uuid page-name page-entity]
  555. (let [[selected-page set-selected-page] (rum/use-state nil)
  556. get-version-key #(or (:VersionUUID %) (:relative-path %))
  557. file-uuid (:FileUUID selected-page)
  558. version-uuid (:VersionUUID selected-page)
  559. [version-content set-version-content] (rum/use-state nil)
  560. [list-ready? set-list-ready?] (rum/use-state false)
  561. [content-ready? set-content-ready?] (rum/use-state false)
  562. *ref-contents (rum/use-ref (atom {}))
  563. original-page-name (or (:block/original-name page-entity) page-name)]
  564. (rum/use-effect!
  565. #(when selected-page
  566. (set-content-ready? false)
  567. (let [k (get-version-key selected-page)
  568. loaded-contents @(rum/deref *ref-contents)]
  569. (if (contains? loaded-contents k)
  570. (do
  571. (set-version-content (get loaded-contents k))
  572. (js/setTimeout (fn [] (set-content-ready? true)) 100))
  573. ;; without cache
  574. (let [load-file (fn [repo-url file]
  575. (-> (fs-util/read-repo-file repo-url file)
  576. (p/then
  577. (fn [content]
  578. (set-version-content content)
  579. (set-content-ready? true)
  580. (swap! (rum/deref *ref-contents) assoc k content)))))]
  581. (if (and file-uuid version-uuid)
  582. ;; read remote content
  583. (async/go
  584. (let [downloaded-path (async/<! (file-sync-handler/download-version-file graph-uuid file-uuid version-uuid true))]
  585. (when downloaded-path
  586. (load-file repo-url downloaded-path))))
  587. ;; read local content
  588. (when-let [relative-path (:relative-path selected-page)]
  589. (load-file repo-url relative-path)))))))
  590. [selected-page])
  591. (rum/use-effect!
  592. (fn []
  593. (state/update-state! :editor/hidden-editors #(conj % page-name))
  594. ;; clear effect
  595. (fn []
  596. (state/update-state! :editor/hidden-editors #(disj % page-name))))
  597. [page-name])
  598. [:div.cp__file-sync-page-histories.flex-wrap
  599. {:class (util/classnames [{:is-list-ready list-ready?}])}
  600. [:h1.absolute.top-0.left-0.text-xl.px-4.py-4.leading-4
  601. (ui/icon "history")
  602. " History for page "
  603. [:span.font-medium original-page-name]]
  604. ;; history versions
  605. [:div.cp__file-sync-page-histories-left.flex-wrap
  606. ;; sidebar lists
  607. (page-history-list graph-uuid page-entity set-list-ready? set-selected-page)
  608. ;; content detail
  609. [:article
  610. (when-let [inst-id (and selected-page (get-version-key selected-page))]
  611. (if content-ready?
  612. [:div.relative.raw-content-editor
  613. (lazy-editor/editor
  614. nil inst-id {:data-lang "markdown"}
  615. version-content {:lineWrapping true :readOnly true :lineNumbers true})
  616. [:div.absolute.top-1.right-1.opacity-50.hover:opacity-100
  617. (ui/button "Restore"
  618. :small? true
  619. :on-click #(state/pub-event! [:file-sync-graph/restore-file (state/get-current-repo) page-entity version-content]))]]
  620. [:span.flex.p-15.items-center.justify-center (ui/loading "")]))]]
  621. ;; current version
  622. [:div.cp__file-sync-page-histories-right
  623. [:h1.title.text-xl
  624. "Current version"]
  625. (page/page-blocks-cp (state/get-current-repo) page-entity nil)]
  626. ;; ready loading
  627. [:div.flex.items-center.h-full.justify-center.w-full.absolute.ready-loading
  628. (ui/loading)]]))
  629. (defn pick-page-histories-panel [graph-uuid page-name]
  630. (fn []
  631. (if-let [page-entity (db-model/get-page page-name)]
  632. (pick-page-histories-for-sync (state/get-current-repo) graph-uuid page-name page-entity)
  633. (ui/admonition :warning (str "The page (" page-name ") does not exist!")))))
  634. (rum/defc onboarding-welcome-logseq-sync
  635. [close-fn]
  636. (let [[loading? set-loading?] (rum/use-state false)]
  637. [:div.cp__file-sync-welcome-logseq-sync
  638. [:span.head-bg
  639. [:strong "CLOSED BETA"]]
  640. [:h1.text-2xl.font-bold.flex-col.sm:flex-row
  641. [:span.opacity-80 "Welcome to "]
  642. [:span.pl-2.dark:text-white.text-gray-800 "Logseq Sync! 👋"]]
  643. [:h2
  644. "No more cloud storage worries. With Logseq's encrypted file syncing, "
  645. [:br]
  646. "you'll always have your notes backed up and available in real-time on any device."]
  647. [:div.pt-6.flex.justify-center.space-x-2.sm:justify-end
  648. (ui/button "Later" :on-click close-fn :background "gray" :class "opacity-60")
  649. (ui/button "Start syncing"
  650. :disabled loading?
  651. :on-click (fn []
  652. (set-loading? true)
  653. (let [result (:user/info @state/state)
  654. ex-time (:ExpireTime result)]
  655. (if (and (number? ex-time)
  656. (< (* ex-time 1000) (js/Date.now)))
  657. (do
  658. (vreset! *beta-unavailable? true)
  659. (maybe-onboarding-show :unavailable))
  660. ;; Logseq sync available
  661. (maybe-onboarding-show :sync-initiate))
  662. (close-fn)
  663. (set-loading? false))))]]))
  664. (rum/defc onboarding-unavailable-file-sync
  665. [close-fn]
  666. [:div.cp__file-sync-unavailable-logseq-sync
  667. [:span.head-bg]
  668. [:h1.text-2xl.font-bold
  669. [:span.pr-2.dark:text-white.text-gray-800 "Logseq Sync"]
  670. [:span.opacity-80 "is not yet available for you. 😔 "]]
  671. [:h2
  672. "Thanks for creating an account! To ensure that our file syncing service runs well when we release it"
  673. [:br]
  674. "to our users, we need a little more time to test it. That’s why we decided to first roll it out only to our "
  675. [:br]
  676. "charitable OpenCollective sponsors and backers. We can notify you once it becomes available for you."]
  677. [:div.pt-6.flex.justify-end.space-x-2
  678. (ui/button "Close" :on-click close-fn :background "gray" :class "opacity-60")]])
  679. (rum/defc onboarding-congrats-successful-sync
  680. [close-fn]
  681. [:div.cp__file-sync-related-normal-modal
  682. [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "checkup-list" {:size 28})]]
  683. [:h1.text-xl.font-semibold.opacity-90.text-center.py-2
  684. [:span.dark:opacity-80 "Congrats on your first successful sync!"]]
  685. [:h2.text-center.dark:opacity-70.text-sm.opacity-90
  686. [:div "By using this graph with Logseq Sync you can now transition seamlessly between your different "]
  687. [:div
  688. [:span "devices. Go to the "]
  689. [:span.dark:text-white "All Graphs "]
  690. [:span "pages to manage your remote graph or switch to another local graph "]]
  691. [:div "and sync it as well."]]
  692. [:div.cloud-tip.rounded-md.mt-6.py-4
  693. [:div.items-center.opacity-90.flex.justify-center
  694. [:span.pr-2.flex (ui/icon "bell-ringing" {:class "font-semibold"})]
  695. [:strong "Logseq Sync is still in Beta and we're working on a Pro plan!"]]]
  696. ;; [:ul.flex.py-6.px-4
  697. ;; [:li.it
  698. ;; [:h1.dark:text-white "10"]
  699. ;; [:h2 "Remote Graphs"]]
  700. ;; [:li.it
  701. ;; [:h1.dark:text-white "5G"]
  702. ;; [:h2 "Storage per Graph"]]
  703. ;; [:li.it
  704. ;; [:h1.dark:text-white "50G"]
  705. ;; [:h2 "Total Storage"]]]
  706. [:div.pt-6.flex.justify-end.space-x-2
  707. (ui/button "Done" :on-click close-fn)]])
  708. (defn open-icloud-graph-clone-picker
  709. ([] (open-icloud-graph-clone-picker (state/get-current-repo)))
  710. ([repo]
  711. (when (and repo (mobile-util/in-iCloud-container-path? repo))
  712. (state/set-modal!
  713. (fn [close-fn]
  714. (clone-local-icloud-graph-panel repo (util/node-path.basename repo) close-fn))
  715. {:close-btn? false :center? true}))))
  716. (defn make-onboarding-panel
  717. [type]
  718. (fn [close-fn]
  719. (case type
  720. :welcome
  721. (onboarding-welcome-logseq-sync close-fn)
  722. :unavailable
  723. (onboarding-unavailable-file-sync close-fn)
  724. :congrats
  725. (onboarding-congrats-successful-sync close-fn)
  726. [:p
  727. [:h1.text-xl.font-bold "Not handled!"]
  728. [:a.button {:on-click close-fn} "Got it!"]])))
  729. (defn maybe-onboarding-show
  730. [type]
  731. (when-not (get (state/sub :file-sync/onboarding-state) (keyword type))
  732. (try
  733. (let [current-repo (state/get-current-repo)
  734. demo-repo? (= current-repo config/demo-repo)
  735. login? (boolean (state/sub :auth/id-token))]
  736. (when login?
  737. (case type
  738. :welcome
  739. (when (or demo-repo?
  740. (:GraphUUID (repo-handler/get-detail-graph-info current-repo)))
  741. (throw (js/Error. "current repo have been local or remote graph")))
  742. (:sync-initiate :sync-learn :sync-history)
  743. (do (quick-tour/ready
  744. (fn []
  745. (quick-tour/start-file-sync type)
  746. (state/set-state! [:file-sync/onboarding-state type] true)))
  747. (throw (js/Error. nil)))
  748. :default)
  749. (state/pub-event! [:file-sync/onboarding-tip type])
  750. (state/set-state! [:file-sync/onboarding-state (keyword type)] true)))
  751. (catch :default e
  752. (js/console.warn "[onboarding SKIP] " (name type) e)))))