|
@@ -0,0 +1,674 @@
|
|
|
+(ns frontend.components.file-sync
|
|
|
+ (:require [cljs.core.async :as async]
|
|
|
+ [clojure.string :as string]
|
|
|
+ [electron.ipc :as ipc]
|
|
|
+ [frontend.components.lazy-editor :as lazy-editor]
|
|
|
+ [frontend.components.onboarding.quick-tour :as quick-tour]
|
|
|
+ [frontend.components.page :as page]
|
|
|
+ [frontend.config :as config]
|
|
|
+ [frontend.db :as db]
|
|
|
+ [frontend.db.model :as db-model]
|
|
|
+ [frontend.fs :as fs]
|
|
|
+ [frontend.fs.sync :as fs-sync]
|
|
|
+ [frontend.handler.file-sync :refer [*beta-unavailable?] :as file-sync-handler]
|
|
|
+ [frontend.handler.notification :as notifications]
|
|
|
+ [frontend.handler.page :as page-handler]
|
|
|
+ [frontend.handler.repo :as repo-handler]
|
|
|
+ [frontend.handler.user :as user-handler]
|
|
|
+ [frontend.handler.web.nfs :as web-nfs]
|
|
|
+ [frontend.mobile.util :as mobile-util]
|
|
|
+ [frontend.state :as state]
|
|
|
+ [frontend.ui :as ui]
|
|
|
+ [frontend.util :as util]
|
|
|
+ [frontend.util.fs :as fs-util]
|
|
|
+ [logseq.graph-parser.config :as gp-config]
|
|
|
+ [promesa.core :as p]
|
|
|
+ [reitit.frontend.easy :as rfe]
|
|
|
+ [rum.core :as rum]))
|
|
|
+
|
|
|
+(declare maybe-onboarding-show)
|
|
|
+(declare open-icloud-graph-clone-picker)
|
|
|
+
|
|
|
+(rum/defc clone-local-icloud-graph-panel
|
|
|
+ [repo graph-name close-fn]
|
|
|
+
|
|
|
+ (rum/use-effect!
|
|
|
+ #(some->> (state/sub :file-sync/jstour-inst)
|
|
|
+ (.complete))
|
|
|
+ [])
|
|
|
+
|
|
|
+ (let [graph-dir (config/get-repo-dir repo)
|
|
|
+ [selected-path set-selected-path] (rum/use-state "")
|
|
|
+ selected-path? (and (not (string/blank? selected-path))
|
|
|
+ (not (mobile-util/iCloud-container-path? selected-path)))
|
|
|
+ on-confirm (fn []
|
|
|
+ (when-let [dest-dir (and selected-path?
|
|
|
+ ;; avoid using `util/node-path.join` to join mobile path since it replaces `file:///abc` to `file:/abc`
|
|
|
+ (str (string/replace selected-path #"/+$" "") "/" graph-name))]
|
|
|
+ (-> (cond
|
|
|
+ (util/electron?)
|
|
|
+ (ipc/ipc :copyDirectory graph-dir dest-dir)
|
|
|
+
|
|
|
+ (mobile-util/native-ios?)
|
|
|
+ (fs/copy! repo graph-dir dest-dir)
|
|
|
+
|
|
|
+ :else
|
|
|
+ nil)
|
|
|
+ (.then #(do
|
|
|
+ (notifications/show! (str "Cloned to => " dest-dir) :success)
|
|
|
+ (web-nfs/ls-dir-files-with-path! dest-dir)
|
|
|
+ (repo-handler/remove-repo! {:url repo})
|
|
|
+ (close-fn)))
|
|
|
+ (.catch #(js/console.error %)))))]
|
|
|
+
|
|
|
+ [:div.cp__file-sync-related-normal-modal
|
|
|
+ [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "folders")]]
|
|
|
+
|
|
|
+ [:h1.text-xl.font-semibold.opacity-90.text-center.py-2
|
|
|
+ "Clone your local graph away from " [:strong "☁️"] " iCloud!"]
|
|
|
+ [:h2.text-center.opacity-70.text-xs.leading-5
|
|
|
+ "Unfortunately, Logseq Sync and iCloud don't work perfectly together at the moment. To make sure"
|
|
|
+ [:br]
|
|
|
+ "You can always delete the remote graph at a later point."]
|
|
|
+
|
|
|
+ [:div.folder-tip.flex.flex-col.items-center
|
|
|
+ [:h3
|
|
|
+ [:span (ui/icon "folder") [:label.pl-0.5 (js/decodeURIComponent graph-name)]]]
|
|
|
+ [:h4.px-6 (config/get-string-repo-dir repo)]
|
|
|
+
|
|
|
+ (when (not (string/blank? selected-path))
|
|
|
+ [:h5.text-xs.pt-1.-mb-1.flex.items-center.leading-none
|
|
|
+ (if (mobile-util/iCloud-container-path? selected-path)
|
|
|
+ [:span.inline-block.pr-1.text-red-600.scale-75 (ui/icon "alert-circle")]
|
|
|
+ [:span.inline-block.pr-1.text-green-600.scale-75 (ui/icon "circle-check")])
|
|
|
+ selected-path])
|
|
|
+
|
|
|
+ [:div.out-icloud
|
|
|
+ (ui/button
|
|
|
+ [:span.inline-flex.items-center.leading-none.opacity-90
|
|
|
+ "Select new parent folder outside of iCloud" (ui/icon "arrow-right")]
|
|
|
+
|
|
|
+ :on-click
|
|
|
+ (fn []
|
|
|
+ ;; TODO: support mobile
|
|
|
+ (cond
|
|
|
+ (util/electron?)
|
|
|
+ (p/let [path (ipc/ipc "openDialog")]
|
|
|
+ (set-selected-path path))
|
|
|
+
|
|
|
+ (mobile-util/native-ios?)
|
|
|
+ (p/let [{:keys [path _localDocumentsPath]}
|
|
|
+ (p/chain
|
|
|
+ (.pickFolder mobile-util/folder-picker)
|
|
|
+ #(js->clj % :keywordize-keys true))]
|
|
|
+ (set-selected-path path))
|
|
|
+
|
|
|
+ :else
|
|
|
+ nil)))]]
|
|
|
+
|
|
|
+ [:p.flex.items-center.space-x-2.pt-6.flex.justify-center.sm:justify-end.-mb-2
|
|
|
+ (ui/button "Cancel" :background "gray" :class "opacity-50" :on-click close-fn)
|
|
|
+ (ui/button "Clone graph" :disabled (not selected-path?) :on-click on-confirm)]]))
|
|
|
+
|
|
|
+(rum/defc create-remote-graph-panel
|
|
|
+ [repo graph-name close-fn]
|
|
|
+
|
|
|
+ (rum/use-effect!
|
|
|
+ #(some->> (state/sub :file-sync/jstour-inst)
|
|
|
+ (.complete))
|
|
|
+ [])
|
|
|
+
|
|
|
+ (let [on-confirm
|
|
|
+ (fn []
|
|
|
+ (async/go
|
|
|
+ (close-fn)
|
|
|
+ (if (mobile-util/iCloud-container-path? repo)
|
|
|
+ (open-icloud-graph-clone-picker repo)
|
|
|
+ (do
|
|
|
+ (state/set-state! [:ui/loading? :graph/create-remote?] true)
|
|
|
+ (when-let [GraphUUID (get (async/<! (file-sync-handler/create-graph graph-name)) 2)]
|
|
|
+ (async/<! (fs-sync/sync-start))
|
|
|
+ (state/set-state! [:ui/loading? :graph/create-remote?] false)
|
|
|
+ ;; update existing repo
|
|
|
+ (state/set-repos! (map (fn [r]
|
|
|
+ (if (= (:url r) repo)
|
|
|
+ (assoc r
|
|
|
+ :GraphUUID GraphUUID
|
|
|
+ :GraphName graph-name
|
|
|
+ :remote? true)
|
|
|
+ r))
|
|
|
+ (state/get-repos))))))))]
|
|
|
+
|
|
|
+ [:div.cp__file-sync-related-normal-modal
|
|
|
+ [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "cloud-upload")]]
|
|
|
+
|
|
|
+ [:h1.text-xl.font-semibold.opacity-90.text-center.py-2
|
|
|
+ "Are you sure you want to create a new remote graph?"]
|
|
|
+ [:h2.text-center.opacity-70.text-xs
|
|
|
+ "By continuing this action you will create an encrypted cloud version of your current local graph." [:br]
|
|
|
+ "You can always delete the remote graph at a later point."]
|
|
|
+
|
|
|
+ [:div.folder-tip.flex.flex-col.items-center
|
|
|
+ [:h3
|
|
|
+ [:span (ui/icon "folder") [:label.pl-0.5 graph-name]]
|
|
|
+ [:span.opacity-50.scale-75 (ui/icon "arrow-right")]
|
|
|
+ [:span (ui/icon "cloud-lock")]]
|
|
|
+ [:h4.px-4 (config/get-string-repo-dir repo)]]
|
|
|
+
|
|
|
+ [:p.flex.items-center.space-x-2.pt-6.flex.justify-center.sm:justify-end.-mb-2
|
|
|
+ (ui/button "Cancel" :background "gray" :class "opacity-50" :on-click close-fn)
|
|
|
+ (ui/button "Create remote graph" :on-click on-confirm)]]))
|
|
|
+
|
|
|
+(rum/defcs ^:large-vars/cleanup-todo indicator <
|
|
|
+ rum/reactive
|
|
|
+ {:will-mount (fn [state]
|
|
|
+ (let [unsub-fn (file-sync-handler/setup-file-sync-event-listeners)]
|
|
|
+ (assoc state ::unsub-events unsub-fn)))
|
|
|
+ :will-unmount (fn [state]
|
|
|
+ (apply (::unsub-events state) nil)
|
|
|
+ state)}
|
|
|
+ [_state]
|
|
|
+ (let [_ (state/sub :auth/id-token)
|
|
|
+ current-repo (state/get-current-repo)
|
|
|
+ creating-remote-graph? (state/sub [:ui/loading? :graph/create-remote?])
|
|
|
+ sync-state (state/sub [:file-sync/sync-state current-repo])
|
|
|
+ _ (rum/react file-sync-handler/refresh-file-sync-component)
|
|
|
+ synced-file-graph? (file-sync-handler/synced-file-graph? current-repo)
|
|
|
+ uploading-files (:current-local->remote-files sync-state)
|
|
|
+ downloading-files (:current-remote->local-files sync-state)
|
|
|
+ queuing-files (:queued-local->remote-files sync-state)
|
|
|
+
|
|
|
+ status (:state sync-state)
|
|
|
+ status (or (nil? status) (keyword (name status)))
|
|
|
+ off? (or (nil? sync-state) (fs-sync/sync-state--stopped? sync-state))
|
|
|
+ full-syncing? (contains? #{:local->remote-full-sync :remote->local-full-sync} status)
|
|
|
+ syncing? (or full-syncing? (contains? #{:local->remote :remote->local} status))
|
|
|
+ idle? (contains? #{:idle} status)
|
|
|
+ need-password? (contains? #{:need-password} status)
|
|
|
+ queuing? (and idle? (boolean (seq queuing-files)))
|
|
|
+ no-active-files? (empty? (concat downloading-files queuing-files uploading-files))
|
|
|
+ create-remote-graph-fn #(when (and current-repo (not (config/demo-graph? current-repo)))
|
|
|
+ (let [graph-name
|
|
|
+ (js/decodeURI (util/node-path.basename current-repo))
|
|
|
+
|
|
|
+ confirm-fn
|
|
|
+ (fn [close-fn]
|
|
|
+ (create-remote-graph-panel current-repo graph-name close-fn))]
|
|
|
+
|
|
|
+ (state/set-modal! confirm-fn {:center? true :close-btn? false})))
|
|
|
+ turn-on #(async/go
|
|
|
+ (cond
|
|
|
+ @*beta-unavailable?
|
|
|
+ (state/pub-event! [:file-sync/onboarding-tip :unavailable])
|
|
|
+
|
|
|
+ ;; current graph belong to other user, do nothing
|
|
|
+ (and (first @fs-sync/graphs-txid)
|
|
|
+ (not (fs-sync/check-graph-belong-to-current-user (user-handler/user-uuid)
|
|
|
+ (first @fs-sync/graphs-txid))))
|
|
|
+ nil
|
|
|
+
|
|
|
+ (and synced-file-graph?
|
|
|
+ (async/<! (fs-sync/<check-remote-graph-exists (second @fs-sync/graphs-txid))))
|
|
|
+ (fs-sync/sync-start)
|
|
|
+
|
|
|
+
|
|
|
+ ;; remote graph already has been deleted, clear repos first, then create-remote-graph
|
|
|
+ synced-file-graph? ; <check-remote-graph-exists -> false
|
|
|
+ (do (state/set-repos!
|
|
|
+ (map (fn [r]
|
|
|
+ (if (= (:url r) current-repo)
|
|
|
+ (dissoc r :GraphUUID :GraphName :remote?)
|
|
|
+ r))
|
|
|
+ (state/get-repos)))
|
|
|
+ (create-remote-graph-fn))
|
|
|
+
|
|
|
+ :else
|
|
|
+ (create-remote-graph-fn)))]
|
|
|
+
|
|
|
+ (if creating-remote-graph?
|
|
|
+ (ui/loading "")
|
|
|
+ [:div.cp__file-sync-indicator
|
|
|
+ (when (and (not config/publishing?)
|
|
|
+ (user-handler/logged-in?))
|
|
|
+
|
|
|
+ (ui/dropdown-with-links
|
|
|
+ (fn [{:keys [toggle-fn]}]
|
|
|
+ (if (not off?)
|
|
|
+ [:a.button.cloud.on
|
|
|
+ {:on-click toggle-fn
|
|
|
+ :class (util/classnames [{:syncing syncing?
|
|
|
+ :is-full full-syncing?
|
|
|
+ :queuing queuing?
|
|
|
+ :idle (and (not queuing?) idle?)}])}
|
|
|
+ [:span.flex.items-center
|
|
|
+ (ui/icon "cloud"
|
|
|
+ {:style {:fontSize ui/icon-size}})]]
|
|
|
+
|
|
|
+ [:a.button.cloud.off
|
|
|
+ {:on-click turn-on}
|
|
|
+ (ui/icon "cloud-off" {:style {:fontSize ui/icon-size}})]))
|
|
|
+
|
|
|
+ (cond-> []
|
|
|
+ synced-file-graph?
|
|
|
+ (concat
|
|
|
+ (if (and no-active-files? idle?)
|
|
|
+ [{:item [:div.flex.justify-center.w-full.py-2
|
|
|
+ [:span.opacity-60 "Everything is synced!"]]
|
|
|
+ :as-link? false}]
|
|
|
+ (if need-password?
|
|
|
+ [{:title [:div.file-item
|
|
|
+ (ui/icon "lock") "Password is required"]
|
|
|
+ :options {:on-click #(state/pub-event! [:file-sync/restart])}}]
|
|
|
+ [{:title [:div.file-item.is-first ""]
|
|
|
+ :options {:class "is-first-placeholder"}}]))
|
|
|
+
|
|
|
+ (map (fn [f] {:title [:div.file-item
|
|
|
+ {:key (str "downloading-" f)}
|
|
|
+ (js/decodeURIComponent f)]
|
|
|
+ :key (str "downloading-" f)
|
|
|
+ :icon (ui/icon "arrow-narrow-down")}) downloading-files)
|
|
|
+ (map (fn [e] (let [icon (case (.-type e)
|
|
|
+ "add" "plus"
|
|
|
+ "unlink" "minus"
|
|
|
+ "edit")
|
|
|
+ path (fs-sync/relative-path e)]
|
|
|
+ {:title [:div.file-item
|
|
|
+ {:key (str "queue-" path)}
|
|
|
+ (js/decodeURIComponent path)]
|
|
|
+ :key (str "queue-" path)
|
|
|
+ :icon (ui/icon icon)})) (take 10 queuing-files))
|
|
|
+ (map (fn [f] {:title [:div.file-item
|
|
|
+ {:key (str "uploading-" f)}
|
|
|
+ (js/decodeURIComponent f)]
|
|
|
+ :key (str "uploading-" f)
|
|
|
+ :icon (ui/icon "arrow-up")}) uploading-files)
|
|
|
+
|
|
|
+ (when sync-state
|
|
|
+ (map-indexed (fn [i f] (:time f)
|
|
|
+ (let [path (:path f)
|
|
|
+ ext (string/lower-case (util/get-file-ext path))
|
|
|
+ _supported? (gp-config/mldoc-support? ext)
|
|
|
+ full-path (util/node-path.join (config/get-repo-dir current-repo) path)
|
|
|
+ page-name (db/get-file-page full-path)]
|
|
|
+ {:title [:div.files-history.cursor-pointer
|
|
|
+ {:key i :class (when (= i 0) "is-first")
|
|
|
+ :on-click (fn []
|
|
|
+ (if page-name
|
|
|
+ (rfe/push-state :page {:name page-name})
|
|
|
+ (rfe/push-state :file {:path full-path})))}
|
|
|
+ [:span.file-sync-item (js/decodeURIComponent (:path f))]
|
|
|
+ [:div.opacity-50 (ui/humanity-time-ago (:time f) nil)]]}))
|
|
|
+ (take 10 (:history sync-state))))))
|
|
|
+
|
|
|
+ {:links-header
|
|
|
+ [:<>
|
|
|
+ (when (and synced-file-graph? queuing?)
|
|
|
+ [:div.head-ctls
|
|
|
+ (ui/button "Sync now"
|
|
|
+ :class "block cursor-pointer"
|
|
|
+ :small? true
|
|
|
+ :on-click #(async/offer! fs-sync/immediately-local->remote-chan true))])
|
|
|
+
|
|
|
+ ;(when config/dev?
|
|
|
+ ; [:strong.debug-status (str status)])
|
|
|
+ ]}))])))
|
|
|
+
|
|
|
+(rum/defc pick-local-graph-for-sync [graph]
|
|
|
+ (rum/use-effect!
|
|
|
+ (fn []
|
|
|
+ (file-sync-handler/set-wait-syncing-graph graph)
|
|
|
+ #(file-sync-handler/set-wait-syncing-graph nil))
|
|
|
+ [graph])
|
|
|
+
|
|
|
+ [:div.cp__file-sync-related-normal-modal
|
|
|
+ [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "cloud-download")]]
|
|
|
+
|
|
|
+ [:h1.mb-5.text-2xl.text-center.font-bold "Sync a remote graph to local"]
|
|
|
+
|
|
|
+ [:div.folder-tip.flex.flex-col.items-center
|
|
|
+ {:style {:border-bottom-right-radius 0 :border-bottom-left-radius 0}}
|
|
|
+ [:h3
|
|
|
+ [:span.flex.space-x-2.leading-none.pb-1
|
|
|
+ (ui/icon "cloud-lock")
|
|
|
+ [:span (:GraphName graph)]
|
|
|
+ [:span.scale-75 (ui/icon "arrow-right")]
|
|
|
+ [:span (ui/icon "folder")]]]
|
|
|
+ [:h4.px-2.-mb-1.5 [:strong "UUID: "] (:GraphUUID graph)]]
|
|
|
+
|
|
|
+ [:div.-mt-1
|
|
|
+ (ui/button
|
|
|
+ (str "Open a local directory")
|
|
|
+ :class "w-full rounded-t-none py-4"
|
|
|
+ :on-click #(-> (page-handler/ls-dir-files!
|
|
|
+ (fn [{:keys [url]}]
|
|
|
+ (file-sync-handler/init-remote-graph url)
|
|
|
+ ;; TODO: wait for switch done
|
|
|
+ (js/setTimeout (fn [] (repo-handler/refresh-repos!)) 200))
|
|
|
+
|
|
|
+ {:empty-dir?-or-pred
|
|
|
+ (fn [ret]
|
|
|
+ (let [empty-dir? (nil? (second ret))]
|
|
|
+ (if-let [root (first ret)]
|
|
|
+
|
|
|
+ ;; verify directory
|
|
|
+ (-> (if empty-dir?
|
|
|
+ (p/resolved nil)
|
|
|
+ (if (util/electron?)
|
|
|
+ (ipc/ipc :readGraphTxIdInfo root)
|
|
|
+ (fs-util/read-graph-txid-info root)))
|
|
|
+
|
|
|
+ (p/then (fn [^js info]
|
|
|
+ (when (and (not empty-dir?)
|
|
|
+ (or (nil? info)
|
|
|
+ (nil? (second info))
|
|
|
+ (not= (second info) (:GraphUUID graph))))
|
|
|
+ (throw (js/Error. "AssertDirectoryError"))))))
|
|
|
+
|
|
|
+ ;; cancel pick a directory
|
|
|
+ (throw (js/Error. nil)))))})
|
|
|
+
|
|
|
+ (p/catch (fn [^js e]
|
|
|
+ (when (= "AssertDirectoryError" (.-message e))
|
|
|
+ (notifications/show! "Please select an empty directory or an existing remote graph!" :error))))))
|
|
|
+ [:p.text-xs.opacity-50.px-1 (ui/icon "alert-circle") " An empty directory or an existing remote graph!"]]])
|
|
|
+
|
|
|
+(defn pick-dest-to-sync-panel [graph]
|
|
|
+ (fn []
|
|
|
+ (pick-local-graph-for-sync graph)))
|
|
|
+
|
|
|
+(rum/defc page-history-list
|
|
|
+ [graph-uuid page-entity set-list-ready? set-page]
|
|
|
+
|
|
|
+ (let [[version-files set-version-files] (rum/use-state nil)
|
|
|
+ [current-page set-current-page] (rum/use-state nil)
|
|
|
+ [loading? set-loading?] (rum/use-state false)
|
|
|
+
|
|
|
+ set-page-fn (fn [page-meta]
|
|
|
+ (set-current-page page-meta)
|
|
|
+ (set-page page-meta))
|
|
|
+
|
|
|
+ get-version-key #(or (:VersionUUID %) (:relative-path %))]
|
|
|
+
|
|
|
+ ;; fetch version files
|
|
|
+ (rum/use-effect!
|
|
|
+ (fn []
|
|
|
+ (when-not loading?
|
|
|
+ (async/go
|
|
|
+ (set-loading? true)
|
|
|
+ (try
|
|
|
+ (let [files (async/<! (file-sync-handler/fetch-page-file-versions graph-uuid page-entity))]
|
|
|
+ (set-version-files files)
|
|
|
+ (set-page-fn (first files))
|
|
|
+ (set-list-ready? true))
|
|
|
+ (finally (set-loading? false)))))
|
|
|
+ #())
|
|
|
+ [])
|
|
|
+
|
|
|
+ [:div.version-list
|
|
|
+ (if loading?
|
|
|
+ [:div.p-4 (ui/loading "Loading...")]
|
|
|
+ (for [version version-files]
|
|
|
+ (let [version-uuid (get-version-key version)
|
|
|
+ _local? (some? (:relative-path version))]
|
|
|
+ [:div.version-list-item {:key version-uuid}
|
|
|
+ [:a.item-link.block.fade-link.flex.justify-between
|
|
|
+ {:title version-uuid
|
|
|
+ :class (util/classnames
|
|
|
+ [{:active (and current-page (= version-uuid (get-version-key current-page)))}])
|
|
|
+ :on-click #(set-page-fn version)}
|
|
|
+
|
|
|
+ [:div.text-sm.pt-1
|
|
|
+ (ui/humanity-time-ago
|
|
|
+ (or (:CreateTime version)
|
|
|
+ (:create-time version)) nil)]
|
|
|
+ [:small.opacity-50.translate-y-1
|
|
|
+ (if _local?
|
|
|
+ [:<> (ui/icon "git-commit") " local"]
|
|
|
+ [:<> (ui/icon "cloud") " remote"])]]])))]))
|
|
|
+
|
|
|
+(rum/defc pick-page-histories-for-sync
|
|
|
+ [repo-url graph-uuid page-name page-entity]
|
|
|
+ (let [[selected-page set-selected-page] (rum/use-state nil)
|
|
|
+ get-version-key #(or (:VersionUUID %) (:relative-path %))
|
|
|
+ file-uuid (:FileUUID selected-page)
|
|
|
+ version-uuid (:VersionUUID selected-page)
|
|
|
+ [version-content set-version-content] (rum/use-state nil)
|
|
|
+ [list-ready? set-list-ready?] (rum/use-state false)
|
|
|
+ [content-ready? set-content-ready?] (rum/use-state false)
|
|
|
+ *ref-contents (rum/use-ref (atom {}))
|
|
|
+ original-page-name (or (:block/original-name page-entity) page-name)]
|
|
|
+
|
|
|
+ (rum/use-effect!
|
|
|
+ #(when selected-page
|
|
|
+ (set-content-ready? false)
|
|
|
+ (let [k (get-version-key selected-page)
|
|
|
+ loaded-contents @(rum/deref *ref-contents)]
|
|
|
+ (if (contains? loaded-contents k)
|
|
|
+ (do
|
|
|
+ (set-version-content (get loaded-contents k))
|
|
|
+ (js/setTimeout (fn [] (set-content-ready? true)) 100))
|
|
|
+
|
|
|
+ ;; without cache
|
|
|
+ (let [load-file (fn [repo-url file]
|
|
|
+ (-> (fs-util/read-repo-file repo-url file)
|
|
|
+ (p/then
|
|
|
+ (fn [content]
|
|
|
+ (set-version-content content)
|
|
|
+ (set-content-ready? true)
|
|
|
+ (swap! (rum/deref *ref-contents) assoc k content)))))]
|
|
|
+ (if (and file-uuid version-uuid)
|
|
|
+ ;; read remote content
|
|
|
+ (async/go
|
|
|
+ (let [downloaded-path (async/<! (file-sync-handler/download-version-file graph-uuid file-uuid version-uuid true))]
|
|
|
+ (when downloaded-path
|
|
|
+ (load-file repo-url downloaded-path))))
|
|
|
+
|
|
|
+ ;; read local content
|
|
|
+ (when-let [relative-path (:relative-path selected-page)]
|
|
|
+ (load-file repo-url relative-path)))))))
|
|
|
+ [selected-page])
|
|
|
+
|
|
|
+ (rum/use-effect!
|
|
|
+ (fn []
|
|
|
+ (state/update-state! :editor/hidden-editors #(conj % page-name))
|
|
|
+
|
|
|
+ ;; clear effect
|
|
|
+ (fn []
|
|
|
+ (state/update-state! :editor/hidden-editors #(disj % page-name))))
|
|
|
+ [page-name])
|
|
|
+
|
|
|
+ [:div.cp__file-sync-page-histories.flex-wrap
|
|
|
+ {:class (util/classnames [{:is-list-ready list-ready?}])}
|
|
|
+
|
|
|
+ [:h1.absolute.top-0.left-0.text-xl.px-4.py-4.leading-4
|
|
|
+ (ui/icon "history")
|
|
|
+ " History for page "
|
|
|
+ [:span.font-medium original-page-name]]
|
|
|
+
|
|
|
+ ;; history versions
|
|
|
+ [:div.cp__file-sync-page-histories-left.flex-wrap
|
|
|
+ ;; sidebar lists
|
|
|
+ (page-history-list graph-uuid page-entity set-list-ready? set-selected-page)
|
|
|
+
|
|
|
+ ;; content detail
|
|
|
+ [:article
|
|
|
+ (when-let [inst-id (and selected-page (get-version-key selected-page))]
|
|
|
+ (if content-ready?
|
|
|
+ [:div.relative.raw-content-editor
|
|
|
+ (lazy-editor/editor
|
|
|
+ nil inst-id {:data-lang "markdown"}
|
|
|
+ version-content {:lineWrapping true :readOnly true :lineNumbers true})
|
|
|
+ [:div.absolute.top-1.right-1.opacity-50.hover:opacity-100
|
|
|
+ (ui/button "Restore"
|
|
|
+ :small? true
|
|
|
+ :on-click #(state/pub-event! [:file-sync-graph/restore-file (state/get-current-repo) page-entity version-content]))]]
|
|
|
+ [:span.flex.p-15.items-center.justify-center (ui/loading "")]))]]
|
|
|
+
|
|
|
+ ;; current version
|
|
|
+ [:div.cp__file-sync-page-histories-right
|
|
|
+ [:h1.title.text-xl
|
|
|
+ "Current version"]
|
|
|
+ (page/page-blocks-cp (state/get-current-repo) page-entity nil)]
|
|
|
+
|
|
|
+ ;; ready loading
|
|
|
+ [:div.flex.items-center.h-full.justify-center.w-full.absolute.ready-loading
|
|
|
+ (ui/loading "Loading...")]]))
|
|
|
+
|
|
|
+(defn pick-page-histories-panel [graph-uuid page-name]
|
|
|
+ (fn []
|
|
|
+ (if-let [page-entity (db-model/get-page page-name)]
|
|
|
+ (pick-page-histories-for-sync (state/get-current-repo) graph-uuid page-name page-entity)
|
|
|
+ (ui/admonition :warning (str "The page (" page-name ") does not exist!")))))
|
|
|
+
|
|
|
+(rum/defc onboarding-welcome-logseq-sync
|
|
|
+ [close-fn]
|
|
|
+
|
|
|
+ (let [[loading? set-loading?] (rum/use-state false)]
|
|
|
+ [:div.cp__file-sync-welcome-logseq-sync
|
|
|
+ [:span.head-bg
|
|
|
+
|
|
|
+ [:strong "CLOSED BETA"]]
|
|
|
+
|
|
|
+ [:h1.text-2xl.font-bold.flex-col.sm:flex-row
|
|
|
+ [:span.opacity-80 "Welcome to "]
|
|
|
+ [:span.pl-2.dark:text-white.text-gray-800 "Logseq Sync! 👋"]]
|
|
|
+
|
|
|
+ [:h2
|
|
|
+ "No more cloud storage worries. With Logseq's encrypted file syncing, "
|
|
|
+ [:br]
|
|
|
+ "you'll always have your notes backed up and available in real-time on any device."]
|
|
|
+
|
|
|
+ [:div.pt-6.flex.justify-center.space-x-2.sm:justify-end
|
|
|
+ (ui/button "Later" :on-click close-fn :background "gray" :class "opacity-60")
|
|
|
+ (ui/button "Start syncing"
|
|
|
+ :disabled loading?
|
|
|
+ :on-click (fn []
|
|
|
+ (set-loading? true)
|
|
|
+ (let [result (:user/info @state/state)
|
|
|
+ ex-time (:ExpireTime result)]
|
|
|
+ (if (and (number? ex-time)
|
|
|
+ (< (* ex-time 1000) (js/Date.now)))
|
|
|
+ (do
|
|
|
+ (vreset! *beta-unavailable? true)
|
|
|
+ (maybe-onboarding-show :unavailable))
|
|
|
+
|
|
|
+ ;; Logseq sync available
|
|
|
+ (maybe-onboarding-show :sync-initiate))
|
|
|
+ (close-fn)
|
|
|
+ (set-loading? false))
|
|
|
+ ))]]))
|
|
|
+
|
|
|
+(rum/defc onboarding-unavailable-file-sync
|
|
|
+ [close-fn]
|
|
|
+
|
|
|
+ [:div.cp__file-sync-unavailable-logseq-sync
|
|
|
+ [:span.head-bg]
|
|
|
+
|
|
|
+ [:h1.text-2xl.font-bold
|
|
|
+ [:span.pr-2.dark:text-white.text-gray-800 "Logseq Sync"]
|
|
|
+ [:span.opacity-80 "is not yet available for you. 😔 "]]
|
|
|
+
|
|
|
+ [:h2
|
|
|
+ "Thanks for creating an account! To ensure that our file syncing service runs well when we release it"
|
|
|
+ [:br]
|
|
|
+ "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 "
|
|
|
+ [:br]
|
|
|
+ "charitable OpenCollective sponsors. We can notify you once it becomes available for you."]
|
|
|
+
|
|
|
+ [:div.pt-6.flex.justify-end.space-x-2
|
|
|
+ (ui/button "Close" :on-click close-fn :background "gray" :class "opacity-60")]])
|
|
|
+
|
|
|
+(rum/defc onboarding-congrats-successful-sync
|
|
|
+ [close-fn]
|
|
|
+
|
|
|
+ [:div.cp__file-sync-related-normal-modal
|
|
|
+ [:div.flex.justify-center.pb-4 [:span.icon-wrap (ui/icon "checkup-list")]]
|
|
|
+
|
|
|
+ [:h1.text-xl.font-semibold.opacity-90.text-center.py-2
|
|
|
+ [:span.dark:opacity-80 "Congrats on your first successful sync!"]]
|
|
|
+
|
|
|
+ [:h2.text-center.dark:opacity-70.text-sm.opacity-90
|
|
|
+ [:div "By using this graph with Logseq Sync you can now transition seamlessly between your different "]
|
|
|
+ [:div
|
|
|
+ [:span "devices. Go to the "]
|
|
|
+ [:span.dark:text-white "All Graphs "]
|
|
|
+ [:span "pages to manage your remote graph or switch to another local graph "]]
|
|
|
+ [:div "and sync it as well."]]
|
|
|
+
|
|
|
+ [:div.cloud-tip.rounded-md.mt-6.py-4
|
|
|
+ [:div.items-center.opacity-90.flex.justify-center
|
|
|
+ [:span.pr-2 (ui/icon "bell-ringing" {:class "font-semibold"})]
|
|
|
+ [:strong "Logseq Sync is still in Beta and we're working on a Pro plan!"]]
|
|
|
+
|
|
|
+ ;; [:ul.flex.py-6.px-4
|
|
|
+ ;; [:li.it
|
|
|
+ ;; [:h1.dark:text-white "10"]
|
|
|
+ ;; [:h2 "Remote Graphs"]]
|
|
|
+ ;; [:li.it
|
|
|
+ ;; [:h1.dark:text-white "5G"]
|
|
|
+ ;; [:h2 "Storage per Graph"]]
|
|
|
+
|
|
|
+ ;; [:li.it
|
|
|
+ ;; [:h1.dark:text-white "50G"]
|
|
|
+ ;; [:h2 "Total Storage"]]]
|
|
|
+ ]
|
|
|
+
|
|
|
+ [:div.pt-6.flex.justify-end.space-x-2
|
|
|
+ (ui/button "Done" :on-click close-fn)]])
|
|
|
+
|
|
|
+(defn open-icloud-graph-clone-picker
|
|
|
+ ([] (open-icloud-graph-clone-picker (state/get-current-repo)))
|
|
|
+ ([repo]
|
|
|
+ (when (and repo (mobile-util/iCloud-container-path? repo))
|
|
|
+ (state/set-modal!
|
|
|
+ (fn [close-fn]
|
|
|
+ (clone-local-icloud-graph-panel repo (util/node-path.basename repo) close-fn))
|
|
|
+ {:close-btn? false :center? true}))))
|
|
|
+
|
|
|
+(defn make-onboarding-panel
|
|
|
+ [type]
|
|
|
+
|
|
|
+ (fn [close-fn]
|
|
|
+
|
|
|
+ (case type
|
|
|
+ :welcome
|
|
|
+ (onboarding-welcome-logseq-sync close-fn)
|
|
|
+
|
|
|
+ :unavailable
|
|
|
+ (onboarding-unavailable-file-sync close-fn)
|
|
|
+
|
|
|
+ :congrats
|
|
|
+ (onboarding-congrats-successful-sync close-fn)
|
|
|
+
|
|
|
+ [:p
|
|
|
+ [:h1.text-xl.font-bold "Not handled!"]
|
|
|
+ [:a.button {:on-click close-fn} "Got it!"]])))
|
|
|
+
|
|
|
+(defn maybe-onboarding-show
|
|
|
+ [type]
|
|
|
+ (when-not (get (state/sub :file-sync/onboarding-state) (keyword type))
|
|
|
+ (try
|
|
|
+ (let [current-repo (state/get-current-repo)
|
|
|
+ local-repo? (= current-repo config/local-repo)
|
|
|
+ login? (boolean (state/sub :auth/id-token))]
|
|
|
+
|
|
|
+ (when login?
|
|
|
+ (case type
|
|
|
+
|
|
|
+ :welcome
|
|
|
+ (when (or local-repo?
|
|
|
+ (:GraphUUID (repo-handler/get-detail-graph-info current-repo)))
|
|
|
+ (throw (js/Error. "current repo have been local or remote graph")))
|
|
|
+
|
|
|
+ (:sync-initiate :sync-learn :sync-history)
|
|
|
+ (do (quick-tour/ready
|
|
|
+ (fn []
|
|
|
+ (quick-tour/start-file-sync type)
|
|
|
+ (state/set-state! [:file-sync/onboarding-state type] true)))
|
|
|
+ (throw (js/Error. nil)))
|
|
|
+ :default)
|
|
|
+
|
|
|
+ (state/pub-event! [:file-sync/onboarding-tip type])
|
|
|
+ (state/set-state! [:file-sync/onboarding-state (keyword type)] true)))
|
|
|
+ (catch js/Error e
|
|
|
+ (js/console.warn "[onboarding SKIP] " (name type) e)))))
|