mirror of
https://github.com/logseq/logseq.git
synced 2026-02-01 22:47:36 +00:00
Merge branch 'master' into enhance/assets-improvements
This commit is contained in:
6
deps/db/src/logseq/db.cljs
vendored
6
deps/db/src/logseq/db.cljs
vendored
@@ -133,7 +133,11 @@
|
||||
(when-let [f @*transact-invalid-callback]
|
||||
(f tx-report errors))
|
||||
(throw (ex-info "DB write failed with invalid data" {:tx-data tx-data
|
||||
:pipeline-tx-data (:tx-data tx-report)}))))
|
||||
:errors errors
|
||||
:pipeline-tx-data (map
|
||||
(fn [[e a v t]]
|
||||
[e a v t])
|
||||
(:tx-data tx-report))}))))
|
||||
tx-report)
|
||||
(d/transact! conn tx-data tx-meta)))
|
||||
(catch :default e
|
||||
|
||||
@@ -8,15 +8,73 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
|
||||
var window: UIWindow?
|
||||
var navController: UINavigationController?
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Multi-stack routing state
|
||||
// ---------------------------------------------------------
|
||||
|
||||
/// Currently active logical stack id (must match CLJS :stack, e.g. "home", "capture", "goto").
|
||||
private var activeStackId: String = "home"
|
||||
|
||||
/// Per-stack path stacks, including the active one.
|
||||
/// Example: ["home": ["/", "/page/A"], "capture": ["/__stack__/capture"]]
|
||||
private var stackPathStacks: [String: [String]] = [
|
||||
"home": ["/"]
|
||||
]
|
||||
|
||||
/// Mirror of the active stack's paths.
|
||||
private var pathStack: [String] = ["/"]
|
||||
private var ignoreRoutePopCount = 0
|
||||
|
||||
/// Used to ignore JS-driven pops when we're popping in response to a native gesture.
|
||||
private var ignoreRoutePopCount: Int = 0
|
||||
|
||||
/// Temporary snapshot image for smooth pop transitions.
|
||||
private var popSnapshotView: UIView?
|
||||
|
||||
// Each stack has its own native VC stack, just like paths.
|
||||
private var stackViewControllerStacks: [String: [UIViewController]] = [:]
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Helpers
|
||||
// ---------------------------------------------------------
|
||||
|
||||
private func normalizedPath(_ raw: String?) -> String {
|
||||
guard let raw = raw, !raw.isEmpty else { return "/" }
|
||||
return raw
|
||||
}
|
||||
|
||||
/// Returns the current native path stack for a given logical stack id,
|
||||
/// or initialises a sensible default if none exists yet.
|
||||
private func paths(for stackId: String) -> [String] {
|
||||
if let existing = stackPathStacks[stackId], !existing.isEmpty {
|
||||
return existing
|
||||
}
|
||||
|
||||
if stackId == "home" {
|
||||
return ["/"]
|
||||
} else {
|
||||
// Virtual stacks (e.g. capture, search, goto) default to a stack-root path.
|
||||
return ["/__stack__/\(stackId)"]
|
||||
}
|
||||
}
|
||||
|
||||
/// Updates the stored paths for a given stack id and keeps `pathStack`
|
||||
/// consistent if this is the active stack.
|
||||
private func setPaths(_ paths: [String], for stackId: String) {
|
||||
stackPathStacks[stackId] = paths
|
||||
if stackId == activeStackId {
|
||||
pathStack = paths
|
||||
}
|
||||
}
|
||||
|
||||
private func setViewControllers(_ vcs: [UIViewController], for stackId: String) {
|
||||
stackViewControllerStacks[stackId] = vcs
|
||||
}
|
||||
|
||||
private func viewControllers(for stackId: String) -> [UIViewController] {
|
||||
stackViewControllerStacks[stackId] ?? []
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: UIApplication lifecycle
|
||||
// ---------------------------------------------------------
|
||||
@@ -124,7 +182,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Navigation operations
|
||||
// MARK: Navigation operations (within active stack)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
private func emptyNavStack(path: String) {
|
||||
@@ -137,10 +195,12 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
|
||||
let vc = NativePageViewController(path: path, push: false)
|
||||
pathStack = [path]
|
||||
setPaths(pathStack, for: activeStackId)
|
||||
|
||||
nav.setViewControllers([vc], animated: false)
|
||||
SharedWebViewController.instance.clearPlaceholder()
|
||||
SharedWebViewController.instance.attach(to: vc)
|
||||
|
||||
}
|
||||
|
||||
private func pushIfNeeded(path: String, animated: Bool) {
|
||||
@@ -154,7 +214,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
|
||||
let vc = NativePageViewController(path: path, push: true)
|
||||
pathStack.append(path)
|
||||
setPaths(pathStack, for: activeStackId)
|
||||
|
||||
nav.pushViewController(vc, animated: animated)
|
||||
|
||||
}
|
||||
|
||||
private func replaceTop(path: String) {
|
||||
@@ -162,9 +225,10 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
guard let nav = navController else { return }
|
||||
|
||||
_ = pathStack.popLast()
|
||||
let vc = NativePageViewController(path: path, push: false)
|
||||
pathStack.append(path)
|
||||
setPaths(pathStack, for: activeStackId)
|
||||
|
||||
let vc = NativePageViewController(path: path, push: false)
|
||||
var stack = nav.viewControllers
|
||||
if stack.isEmpty {
|
||||
stack = [vc]
|
||||
@@ -172,6 +236,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
stack[stack.count - 1] = vc
|
||||
}
|
||||
nav.setViewControllers(stack, animated: false)
|
||||
|
||||
}
|
||||
|
||||
private func popIfNeeded(animated: Bool) {
|
||||
@@ -179,6 +244,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
|
||||
if nav.viewControllers.count > 1 {
|
||||
_ = pathStack.popLast()
|
||||
setPaths(pathStack, for: activeStackId)
|
||||
nav.popViewController(animated: animated)
|
||||
}
|
||||
}
|
||||
@@ -208,13 +274,17 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
|
||||
if isPop {
|
||||
// -----------------------------
|
||||
// POP — keep your existing logic
|
||||
// POP — update per-stack pathStack, then notify JS.
|
||||
// -----------------------------
|
||||
let previousStack = pathStack
|
||||
if pathStack.count > 1 { _ = pathStack.popLast() }
|
||||
|
||||
if pathStack.count > 1 {
|
||||
_ = pathStack.popLast()
|
||||
}
|
||||
if let last = pathStack.last, last != toVC.targetPath {
|
||||
pathStack[pathStack.count - 1] = toVC.targetPath
|
||||
}
|
||||
setPaths(pathStack, for: activeStackId)
|
||||
|
||||
popSnapshotView?.removeFromSuperview()
|
||||
popSnapshotView = nil
|
||||
@@ -227,9 +297,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
popSnapshotView = iv
|
||||
}
|
||||
|
||||
coordinator.animate(alongsideTransition: nil) { ctx in
|
||||
coordinator.animate(alongsideTransition: nil) { [weak self] ctx in
|
||||
guard let self else { return }
|
||||
|
||||
guard !ctx.isCancelled else {
|
||||
self.pathStack = previousStack
|
||||
self.setPaths(previousStack, for: self.activeStackId)
|
||||
|
||||
if let fromVC {
|
||||
SharedWebViewController.instance.attach(to: fromVC)
|
||||
}
|
||||
@@ -237,12 +311,13 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
return
|
||||
}
|
||||
|
||||
if let webView = SharedWebViewController.instance.bridgeController.bridge?.webView,
|
||||
webView.canGoBack {
|
||||
self.ignoreRoutePopCount += 1
|
||||
webView.goBack()
|
||||
} else {
|
||||
self.ignoreRoutePopCount += 1
|
||||
// 🔑 DO NOT call webView.goBack().
|
||||
// Tell JS explicitly that native popped.
|
||||
self.ignoreRoutePopCount += 1
|
||||
|
||||
if let bridge = SharedWebViewController.instance.bridgeController.bridge {
|
||||
let js = "window.LogseqNative && window.LogseqNative.onNativePop && window.LogseqNative.onNativePop();"
|
||||
bridge.webView?.evaluateJavaScript(js, completionHandler: nil)
|
||||
}
|
||||
|
||||
SharedWebViewController.instance.attach(
|
||||
@@ -264,8 +339,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
// -----------------------------
|
||||
// PUSH / RESET
|
||||
// -----------------------------
|
||||
// Attach the shared webview to the *destination* page
|
||||
// before/during the animation so it can start rendering immediately.
|
||||
SharedWebViewController.instance.attach(
|
||||
to: toVC,
|
||||
leavePlaceholderInPreviousParent: fromVC != nil
|
||||
@@ -273,10 +346,8 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
|
||||
coordinator.animate(alongsideTransition: nil) { ctx in
|
||||
if ctx.isCancelled, let fromVC {
|
||||
// If the push is cancelled (interactive back), put the webview back.
|
||||
SharedWebViewController.instance.attach(to: fromVC)
|
||||
} else {
|
||||
// Transition completed → clear any placeholders.
|
||||
SharedWebViewController.instance.clearPlaceholder()
|
||||
}
|
||||
}
|
||||
@@ -294,7 +365,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
SharedWebViewController.instance.clearPlaceholder()
|
||||
SharedWebViewController.instance.attach(to: current)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func navigationController(
|
||||
@@ -308,20 +378,103 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Route Observation
|
||||
// MARK: Route Observation (JS -> Native)
|
||||
// ---------------------------------------------------------
|
||||
|
||||
private func observeRouteChanges() {
|
||||
NotificationCenter.default.addObserver(
|
||||
forName: UILocalPlugin.routeChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
forName: UILocalPlugin.routeChangeNotification,
|
||||
object: nil,
|
||||
queue: .main
|
||||
) { [weak self] notification in
|
||||
guard let self else { return }
|
||||
guard let nav = self.navController else { return }
|
||||
|
||||
let path = self.normalizedPath(notification.userInfo?["path"] as? String)
|
||||
let rawPath = notification.userInfo?["path"] as? String
|
||||
let path = self.normalizedPath(rawPath)
|
||||
let navigationType = (notification.userInfo?["navigationType"] as? String) ?? "push"
|
||||
let stackId = (notification.userInfo?["stack"] as? String) ?? "home"
|
||||
let previousStackId = self.activeStackId
|
||||
|
||||
// 🚫 Fast-path: ignore duplicate replace for same stack/path
|
||||
if stackId == self.activeStackId,
|
||||
navigationType == "replace",
|
||||
path == self.pathStack.last {
|
||||
return
|
||||
}
|
||||
|
||||
// ⚡️ Fast-path: cancel search → home root.
|
||||
// We do NOT rebuild nav stack and we do NOT reattach the WebView.
|
||||
// JS will just navigate the existing shared WKWebView to "/".
|
||||
if previousStackId == "search",
|
||||
stackId == "home"{
|
||||
|
||||
// Just update bookkeeping so future home pushes/pop work correctly.
|
||||
self.activeStackId = "home"
|
||||
self.setPaths(["/"], for: "home")
|
||||
self.setPaths(["/__stack__/search"], for: "search")
|
||||
|
||||
nav.setViewControllers([], animated: false)
|
||||
self.setViewControllers([], for: "home")
|
||||
|
||||
// 👈 Do NOTHING to nav.viewControllers or SharedWebViewController here.
|
||||
return
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 1️⃣ Stack switch: home ↔ search ↔ capture...
|
||||
// ============================================
|
||||
if stackId != self.activeStackId {
|
||||
self.setPaths(self.pathStack, for: previousStackId)
|
||||
|
||||
// Load saved paths for target stack
|
||||
var newPaths = self.paths(for: stackId)
|
||||
|
||||
// 🔑 Special rules for shaping the new stack
|
||||
if stackId == "home", path == "/" {
|
||||
// 👉 ALWAYS reset home to a single root VC.
|
||||
newPaths = ["/"]
|
||||
} else if newPaths.isEmpty {
|
||||
// First time visiting this stack
|
||||
newPaths = [path]
|
||||
} else if let last = newPaths.last, last != path {
|
||||
// Same history, but different top path → align the top.
|
||||
newPaths[newPaths.count - 1] = path
|
||||
}
|
||||
|
||||
self.activeStackId = stackId
|
||||
self.pathStack = newPaths
|
||||
self.setPaths(newPaths, for: stackId)
|
||||
|
||||
// Rebuild native stack for these paths
|
||||
var vcs: [UIViewController] = []
|
||||
for (idx, p) in newPaths.enumerated() {
|
||||
let vc = NativePageViewController(path: p, push: idx > 0)
|
||||
vcs.append(vc)
|
||||
}
|
||||
|
||||
nav.setViewControllers(vcs, animated: false)
|
||||
self.setViewControllers(vcs, for: stackId)
|
||||
|
||||
if let lastVC = vcs.last as? NativePageViewController {
|
||||
// Defer & avoid redundant attach.
|
||||
DispatchQueue.main.async {
|
||||
if let bridge = SharedWebViewController.instance.bridgeController.bridge,
|
||||
let webView = bridge.webView,
|
||||
webView.isDescendant(of: lastVC.view) {
|
||||
} else {
|
||||
SharedWebViewController.instance.attach(to: lastVC)
|
||||
}
|
||||
SharedWebViewController.instance.clearPlaceholder()
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// ============================================
|
||||
// 2️⃣ Navigation *within* active stack
|
||||
// ============================================
|
||||
switch navigationType {
|
||||
case "reset":
|
||||
self.emptyNavStack(path: path)
|
||||
@@ -334,7 +487,6 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
self.ignoreRoutePopCount -= 1
|
||||
return
|
||||
}
|
||||
|
||||
if self.pathStack.count > 1 {
|
||||
self.popIfNeeded(animated: true)
|
||||
}
|
||||
@@ -344,6 +496,7 @@ class AppDelegate: UIResponder, UIApplicationDelegate, UINavigationControllerDel
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
@@ -372,6 +525,10 @@ extension NSUserActivity {
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------
|
||||
// MARK: Convenience
|
||||
// ---------------------------------------------------------
|
||||
|
||||
extension AppDelegate {
|
||||
func donateQuickAddShortcut() {
|
||||
let a = NSUserActivity.quickAdd
|
||||
|
||||
@@ -320,7 +320,6 @@ private struct SearchTabHost26: View {
|
||||
wasSearching = false
|
||||
selectedTab.wrappedValue = .content(0)
|
||||
store.selectedId = firstId
|
||||
LiquidTabsPlugin.shared?.notifyTabSelected(id: firstId)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -644,26 +644,30 @@ private func scoreTranscript(_ text: String, locale: Locale) -> Int {
|
||||
}
|
||||
|
||||
@objc func routeDidChange(_ call: CAPPluginCall) {
|
||||
let route = call.getObject("route") as? [String: Any]
|
||||
let path = call.getString("path")
|
||||
let push = call.getBool("push") ?? true
|
||||
let navigationType = call.getString("navigationType") ?? (push ? "push" : "replace")
|
||||
let navigationType = call.getString("navigationType") ?? "push"
|
||||
let push = call.getBool("push") ?? (navigationType == "push")
|
||||
let path = call.getString("path") ?? "/"
|
||||
|
||||
var entry: [String: Any] = [:]
|
||||
if let path = path {
|
||||
entry["path"] = path
|
||||
}
|
||||
if let route = route {
|
||||
entry["route"] = route
|
||||
}
|
||||
entry["push"] = push
|
||||
entry["navigationType"] = navigationType
|
||||
// ✅ read stack from JS, default to "home" only if missing
|
||||
let stack = call.getString("stack") ?? "home"
|
||||
|
||||
NotificationCenter.default.post(
|
||||
name: UILocalPlugin.routeChangeNotification,
|
||||
object: nil,
|
||||
userInfo: entry
|
||||
)
|
||||
#if DEBUG
|
||||
print("📬 UILocal.routeDidChange call from JS")
|
||||
print(" navigationType=\(navigationType) push=\(push) stack=\(stack) path=\(path)")
|
||||
#endif
|
||||
|
||||
DispatchQueue.main.async {
|
||||
NotificationCenter.default.post(
|
||||
name: UILocalPlugin.routeChangeNotification,
|
||||
object: nil,
|
||||
userInfo: [
|
||||
"navigationType": navigationType,
|
||||
"push": push,
|
||||
"stack": stack, // 👈 forward it
|
||||
"path": path
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
|
||||
1
scripts/resources/clojuredocs-export.json
Normal file
1
scripts/resources/clojuredocs-export.json
Normal file
File diff suppressed because one or more lines are too long
@@ -0,0 +1,200 @@
|
||||
(ns logseq.tasks.db-graph.create-graph-with-clojuredocs
|
||||
"Script that convert clojuredocs-export.json into logseq graph data."
|
||||
(:require ["fs" :as fs]
|
||||
[babashka.cli :as cli]
|
||||
[cljs.pprint :as pp]
|
||||
[clojure.edn :as edn]
|
||||
[clojure.string :as string]
|
||||
[datascript.core :as d]
|
||||
[logseq.db.common.sqlite-cli :as sqlite-cli]
|
||||
[logseq.outliner.cli :as outliner-cli]
|
||||
[nbb.classpath :as cp]
|
||||
[nbb.core :as nbb]))
|
||||
|
||||
(def spec
|
||||
"Options spec"
|
||||
{:help {:alias :h
|
||||
:desc "Print help"}
|
||||
:config {:alias :c
|
||||
:coerce edn/read-string
|
||||
:desc "EDN map to add to config.edn"}
|
||||
:export {:alias :e
|
||||
:desc "Exports graph to clojuredocs.edn"}
|
||||
:verbose {:alias :v
|
||||
:desc "Verbose mode"}})
|
||||
|
||||
(defn example=>block
|
||||
[author->block-uuid example]
|
||||
(when-let [body (:body example)]
|
||||
(let [author-block-uuid (author->block-uuid (:login (:author example)))]
|
||||
{:block/title body
|
||||
:build/tags [:logseq.class/Code-block]
|
||||
:build/properties (cond-> {:logseq.property.node/display-type :code
|
||||
:logseq.property.code/lang "Clojure"}
|
||||
author-block-uuid
|
||||
(assoc :user.property/author-X_lTJqwD [:block/uuid author-block-uuid]))})))
|
||||
|
||||
(defn convert-var-to-page
|
||||
[type->block-uuid author->block-uuid
|
||||
{:keys [ns name type see-alsos examples notes arglists doc library-url] :as _clj-var}]
|
||||
{:page
|
||||
{:build/properties
|
||||
{:user.property/library-url-ip8W5T7M library-url
|
||||
:user.property/type-Un-Aypix
|
||||
[:block/uuid (type->block-uuid type)]},
|
||||
:block/title (str "`" name "` (" ns ")")}
|
||||
:blocks
|
||||
[{:block/title "Doc",
|
||||
:build/properties {:logseq.property/heading 3},
|
||||
:build/children
|
||||
(mapv
|
||||
(fn [line]
|
||||
{:block/title line
|
||||
:build/properties {}})
|
||||
(string/split-lines doc))}
|
||||
{:block/title "Examples",
|
||||
:build/properties {:logseq.property/heading 3},
|
||||
:build/children
|
||||
(vec (keep (partial example=>block author->block-uuid) examples))}
|
||||
{:block/title "Notes",
|
||||
:build/properties {:logseq.property/heading 3}
|
||||
:build/children
|
||||
(mapv
|
||||
(fn [note]
|
||||
(let [body (or (:body note) "")
|
||||
author-block-uuid (author->block-uuid (:login (:author note)))]
|
||||
{:block/title body
|
||||
:build/tags [:logseq.class/Quote-block]
|
||||
:build/properties (cond-> {:logseq.property.node/display-type :quote}
|
||||
author-block-uuid
|
||||
(assoc :user.property/author-X_lTJqwD [:block/uuid author-block-uuid]))}))
|
||||
notes)}
|
||||
{:block/title "Arglists",
|
||||
:build/properties {:logseq.property/heading 3}
|
||||
:build/children
|
||||
(vec
|
||||
(when-let [arglists-content (with-out-str (pp/pprint arglists))]
|
||||
[{:block/title arglists-content
|
||||
:build/tags [:logseq.class/Code-block]
|
||||
:build/properties {:logseq.property.node/display-type :code
|
||||
:logseq.property.code/lang "Clojure"}}]))}
|
||||
;; {:block/title "See also",
|
||||
;; :build/properties {:logseq.property/heading 3}
|
||||
;; :build/children
|
||||
;; (vec
|
||||
;; (when-let [arglists-content (with-out-str (pp/pprint see-alsos))]
|
||||
;; [{:block/title arglists-content
|
||||
;; :build/tags [:logseq.class/Code-block]
|
||||
;; :build/properties {:logseq.property.node/display-type :code
|
||||
;; :logseq.property.code/lang "Clojure"}}]))}
|
||||
]})
|
||||
|
||||
(defn convert-type-pages
|
||||
"return {:pages ..., :type->block-uuid ...}"
|
||||
[clj-vars]
|
||||
(let [all-types (set (map :type clj-vars))
|
||||
type->block-uuid
|
||||
(into {} (map (fn [tp] [tp (random-uuid)])) all-types)
|
||||
pages
|
||||
(map
|
||||
(fn [[tp block-uuid]]
|
||||
{:page
|
||||
{:block/uuid block-uuid
|
||||
:build/keep-uuid? true
|
||||
:block/title tp},
|
||||
:blocks
|
||||
[]})
|
||||
type->block-uuid)]
|
||||
{:pages pages :type->block-uuid type->block-uuid}))
|
||||
|
||||
(defn convert-author-pages
|
||||
"return {:pages ..., :author->block-uuid ...}"
|
||||
[clj-vars]
|
||||
(let [author-name (comp :login :author)
|
||||
author-names
|
||||
(set
|
||||
(mapcat
|
||||
(fn [clj-var]
|
||||
(set
|
||||
(map
|
||||
author-name
|
||||
(concat (:see-alsos clj-var)
|
||||
(:examples clj-var)
|
||||
(:notes clj-var)))))
|
||||
clj-vars))
|
||||
author->block-uuid
|
||||
(into {} (map (fn [author] [author (random-uuid)])) author-names)
|
||||
pages
|
||||
(map
|
||||
(fn [[author block-uuid]]
|
||||
{:page
|
||||
{:block/uuid block-uuid
|
||||
:build/keep-uuid? true
|
||||
:block/title author}
|
||||
:blocks
|
||||
[]})
|
||||
author->block-uuid)]
|
||||
{:pages pages :author->block-uuid author->block-uuid}))
|
||||
|
||||
(def properties
|
||||
{:user.property/type-Un-Aypix
|
||||
{:logseq.property/type :node,
|
||||
:block/title "type",
|
||||
:db/cardinality :db.cardinality/one},
|
||||
:user.property/library-url-ip8W5T7M
|
||||
{:db/cardinality :db.cardinality/one,
|
||||
:logseq.property/type :url,
|
||||
:block/title "library-url"}
|
||||
:user.property/author-X_lTJqwD
|
||||
{:logseq.property/type :node
|
||||
:block/title "author"
|
||||
:db/cardinality :db.cardinality/one}})
|
||||
|
||||
(defn convert
|
||||
[clj-vars]
|
||||
(let [{type-pages :pages type->block-uuid :type->block-uuid} (convert-type-pages clj-vars)
|
||||
{author-pages :pages author->block-uuid :author->block-uuid} (convert-author-pages clj-vars)]
|
||||
{:properties properties
|
||||
:classes {}
|
||||
:pages-and-blocks
|
||||
(vec
|
||||
(concat
|
||||
type-pages
|
||||
author-pages
|
||||
(map (partial convert-var-to-page type->block-uuid author->block-uuid) clj-vars)))}))
|
||||
|
||||
(comment
|
||||
(def clojuredocs-json (js->clj (js/JSON.parse (fs/readFileSync "resources/clojuredocs-export.json"))
|
||||
:keywordize-keys true))
|
||||
(def result (convert (filter #(= "clojure.core" (:ns %)) (:vars clojuredocs-json))))
|
||||
(fs/writeFileSync "cljdocs.edn" (with-out-str (pp/pprint result))))
|
||||
|
||||
(defn -main [args]
|
||||
(let [[graph-dir] args
|
||||
options (cli/parse-opts args {:spec spec})
|
||||
_ (when (or (nil? graph-dir) (:help options))
|
||||
(println (str "Usage: $0 GRAPH-NAME [OPTIONS]\nOptions:\n"
|
||||
(cli/format-opts {:spec spec})))
|
||||
(js/process.exit 1))
|
||||
init-conn-args (sqlite-cli/->open-db-args graph-dir)
|
||||
db-name (if (= 1 (count init-conn-args)) (first init-conn-args) (second init-conn-args))
|
||||
conn (apply outliner-cli/init-conn
|
||||
(conj init-conn-args {:additional-config (:config options)
|
||||
:classpath (cp/get-classpath)}))
|
||||
clojuredocs-json (js->clj (js/JSON.parse (fs/readFileSync "resources/clojuredocs-export.json"))
|
||||
:keywordize-keys true)
|
||||
clj-vars (:vars clojuredocs-json)
|
||||
result-edn (convert clj-vars)]
|
||||
(println "Generating" (count (:pages-and-blocks result-edn)) "pages")
|
||||
(if (:export options)
|
||||
(do (println "Generating clojuredocs.edn ...")
|
||||
(fs/writeFileSync "clojuredocs.edn" (with-out-str (pp/pprint result-edn))))
|
||||
(do (println "Transacting to graph ...")
|
||||
(let [{:keys [init-tx block-props-tx]} (outliner-cli/build-blocks-tx result-edn)]
|
||||
(d/transact! conn init-tx)
|
||||
(d/transact! conn block-props-tx)
|
||||
(println "Transacted" (count (d/datoms @conn :eavt)) "datoms")
|
||||
(println "Created graph " (str "'" db-name "'") "!"))))))
|
||||
|
||||
(when (= nbb/*file* (nbb/invoked-file))
|
||||
(-main *command-line-args*))
|
||||
@@ -58,11 +58,8 @@
|
||||
"Quick add"]
|
||||
add-button])
|
||||
(if mobile?
|
||||
[:main#app-container-wrapper.ls-fold-button-on-right
|
||||
[:div#app-container.pt-2
|
||||
[:div#main-container.flex.flex-1
|
||||
[:div.w-full.mt-4
|
||||
(page-blocks add-page)]]]]
|
||||
[:div.w-full.mt-4
|
||||
(page-blocks add-page)]
|
||||
[:div.content {:class "block -ml-6"}
|
||||
(page-blocks add-page)])
|
||||
(when-not mobile? add-button)]))))
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
[clojure.string :as string]
|
||||
[dommy.core :refer-macros [sel by-id]]
|
||||
[frontend.config :as config]
|
||||
[frontend.handler.notification :as notification]
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.handler.user :as user]
|
||||
[frontend.modules.shortcut.core :as shortcut]
|
||||
@@ -38,15 +37,14 @@
|
||||
|
||||
(rum/defc user-pane
|
||||
[_sign-out! user]
|
||||
(let [session (:signInUserSession user)
|
||||
username (:username user)]
|
||||
(let [session (:signInUserSession user)]
|
||||
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(when session
|
||||
(user/login-callback session)
|
||||
(notification/show! (str "Hi, " username " :)") :success)
|
||||
(shui/dialog-close!)
|
||||
(shui/popup-hide!)
|
||||
(when (= :user-login (state/get-current-route))
|
||||
(route-handler/redirect! {:to :home}))))
|
||||
[])
|
||||
|
||||
@@ -1583,7 +1583,7 @@
|
||||
"Save incoming(pasted) assets to assets directory.
|
||||
|
||||
Returns: asset entities"
|
||||
[repo files & {:keys [pdf-area? last-edit-block]}]
|
||||
[repo files & {:keys [pdf-area? last-edit-block save-to-page]}]
|
||||
(p/let [[repo-dir asset-dir-rpath] (assets-handler/ensure-assets-dir! repo)
|
||||
today-page-name (date/today)
|
||||
today-page-e (db-model/get-journal-page today-page-name)
|
||||
@@ -1598,8 +1598,12 @@
|
||||
blocks (remove nil? blocks*)
|
||||
edit-block (or (state/get-edit-block) last-edit-block)
|
||||
insert-to-current-block-page? (and (:block/uuid edit-block) (not pdf-area?))
|
||||
target (if insert-to-current-block-page?
|
||||
target (cond
|
||||
insert-to-current-block-page?
|
||||
edit-block
|
||||
save-to-page
|
||||
save-to-page
|
||||
:else
|
||||
today-page)]
|
||||
(when-not target
|
||||
(throw (ex-info "invalid target" {:files files
|
||||
@@ -1610,6 +1614,7 @@
|
||||
(ui-outliner-tx/transact!
|
||||
{:outliner-op :insert-blocks}
|
||||
(outliner-op/insert-blocks! blocks target {:keep-uuid? true
|
||||
:bottom? true
|
||||
:sibling? (= edit-block target)
|
||||
:replace-empty-target? true}))
|
||||
(map (fn [b] (db/entity [:block/uuid (:block/uuid b)])) blocks)))))
|
||||
|
||||
@@ -192,7 +192,7 @@
|
||||
(js/console.error "instrument data-map should only contains [:type :payload]"))
|
||||
(posthog/capture type payload))
|
||||
|
||||
(defmethod handle :capture-error [[_ {:keys [error payload]}]]
|
||||
(defmethod handle :capture-error [[_ {:keys [error payload extra]}]]
|
||||
(let [[user-uuid graph-uuid tx-id] @sync/graphs-txid
|
||||
payload (merge
|
||||
{:schema-version (str db-schema/version)
|
||||
@@ -204,7 +204,8 @@
|
||||
:db-based (config/db-based-graph? (state/get-current-repo))}
|
||||
payload)]
|
||||
(Sentry/captureException error
|
||||
(bean/->js {:tags payload}))))
|
||||
(bean/->js {:tags payload
|
||||
:extra extra}))))
|
||||
|
||||
(defmethod handle :exec-plugin-cmd [[_ {:keys [pid cmd action]}]]
|
||||
(commands/exec-plugin-simple-command! pid cmd action))
|
||||
|
||||
@@ -931,7 +931,8 @@
|
||||
[["Invalid DB!"] :error])
|
||||
(worker-util/post-message :capture-error
|
||||
{:error (ex-info "Invalid DB" {})
|
||||
:payload {:errors (str errors)}})))
|
||||
:payload {}
|
||||
:extra {:errors (str errors)}})))
|
||||
|
||||
(defn init
|
||||
"web worker entry"
|
||||
|
||||
@@ -6,7 +6,6 @@
|
||||
[frontend.state :as state]
|
||||
[frontend.util :as util]
|
||||
[logseq.common.util :as common-util]
|
||||
[mobile.navigation :as mobile-nav]
|
||||
[mobile.state :as mobile-state]))
|
||||
|
||||
;; Capacitor plugin instance:
|
||||
@@ -98,8 +97,9 @@
|
||||
(js/console.log "Native search query" q)
|
||||
(reset! mobile-state/*search-input q)
|
||||
(reset! mobile-state/*search-last-input-at (common-util/time-ms))
|
||||
(when (= :page (state/get-current-route))
|
||||
(mobile-nav/reset-route!))))
|
||||
(comment
|
||||
(when (= :page (state/get-current-route))
|
||||
(mobile-nav/reset-route!)))))
|
||||
(add-keyboard-hack-listener!)))
|
||||
|
||||
(defn configure
|
||||
|
||||
@@ -30,8 +30,7 @@
|
||||
|
||||
(rum/defc journals
|
||||
[]
|
||||
(ui-component/classic-app-container-wrap
|
||||
(journal/all-journals)))
|
||||
(journal/all-journals))
|
||||
|
||||
(rum/defc home-inner < rum/static
|
||||
[db-restoring?]
|
||||
@@ -118,13 +117,13 @@
|
||||
;; - Journals layer keeps its own scroll container and is always in the DOM.
|
||||
;; - Page/other-tab layer keeps its own independent scroll container.
|
||||
;; Both are absolutely positioned and stacked; we toggle visibility.
|
||||
[:div.h-full.relative
|
||||
;; Journals scroll container (keep-alive)
|
||||
[:div.w-full.relative
|
||||
;; Journals scroll container (keep-alive)
|
||||
[:div#app-main-home.pl-3.pr-2.absolute.inset-0
|
||||
{:class (when-not home? "invisible pointer-events-none")}
|
||||
(home)]
|
||||
|
||||
;; Other pages: search, settings, specific page, etc.
|
||||
;; Other pages: search, settings, specific page, etc.
|
||||
(when-not home?
|
||||
(other-page view tab route-match))]))
|
||||
|
||||
@@ -141,7 +140,7 @@
|
||||
(when-let [element (util/app-scroll-container-node)]
|
||||
(common-handler/listen-to-scroll! element)))
|
||||
[])
|
||||
[:div.h-full
|
||||
[:<>
|
||||
(mobile-header/header current-repo tab)
|
||||
(main-content tab route-match)]))
|
||||
|
||||
@@ -156,13 +155,11 @@
|
||||
show-action-bar? (state/sub :mobile/show-action-bar?)
|
||||
{:keys [open? content-fn opts]} (rum/react mobile-state/*popup-data)
|
||||
show-popup? (and open? content-fn)
|
||||
fold-button-on-right? (state/enable-fold-button-right?)
|
||||
route-match (state/sub :route-match)]
|
||||
[:div#app-main.w-full.h-full
|
||||
{:class (util/classnames
|
||||
[{:ls-fold-button-on-right fold-button-on-right?}])}
|
||||
[:div.w-full.h-full {:class (when show-popup? "invisible")}
|
||||
(app current-repo route-match)]
|
||||
[:main#app-container-wrapper.ls-fold-button-on-right
|
||||
[:div#app-container {:class (when show-popup? "invisible")}
|
||||
[:div#main-container.flex.flex-1.overflow-x-hidden
|
||||
(app current-repo route-match)]]
|
||||
(when show-popup?
|
||||
[:div.ls-layer
|
||||
(popup/popup opts content-fn)])
|
||||
|
||||
@@ -14,7 +14,11 @@ html.is-ios {
|
||||
font-size: calc(var(--ls-mobile-font-size) * var(--ls-mobile-font-scale, 1));
|
||||
}
|
||||
|
||||
#app-main {
|
||||
#app-container-wrapper, #app-container, #main-container {
|
||||
@apply w-full h-full;
|
||||
}
|
||||
|
||||
#app-container-wrapper {
|
||||
padding-top: env(safe-area-inset-top);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
}
|
||||
@@ -306,6 +310,7 @@ body, #root {
|
||||
/* Both layers use their own independent scroll containers. */
|
||||
#app-main-home, #main-content-container
|
||||
{
|
||||
@apply overflow-x-hidden;
|
||||
height: 100%;
|
||||
overflow-y: auto;
|
||||
-webkit-overflow-scrolling: touch;
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
[cljs-time.local :as tl]
|
||||
[clojure.string :as string]
|
||||
[frontend.date :as date]
|
||||
[frontend.db :as db]
|
||||
[frontend.db.model :as db-model]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.mobile.util :as mobile-util]
|
||||
@@ -12,6 +13,8 @@
|
||||
[frontend.util :as util]
|
||||
[goog.functions :as gfun]
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.common.config :as common-config]
|
||||
[logseq.db :as ldb]
|
||||
[logseq.shui.hooks :as hooks]
|
||||
[logseq.shui.ui :as shui]
|
||||
[mobile.init :as init]
|
||||
@@ -62,9 +65,12 @@
|
||||
{:formatter-str audio-file-format})
|
||||
"."))]
|
||||
(p/let [file (js/File. [blob] filename #js {:type (.-type blob)})
|
||||
result (editor-handler/db-based-save-assets! (state/get-current-repo)
|
||||
[file]
|
||||
{:last-edit-block @*last-edit-block})
|
||||
capture? (= "capture" @mobile-state/*tab)
|
||||
insert-opts (cond->
|
||||
{:last-edit-block @*last-edit-block}
|
||||
capture?
|
||||
(assoc :save-to-page (ldb/get-built-in-page (db/get-db) common-config/quick-add-page-name)))
|
||||
result (editor-handler/db-based-save-assets! (state/get-current-repo) [file] insert-opts)
|
||||
asset-entity (first result)]
|
||||
(when (nil? asset-entity)
|
||||
(log/error ::empty-asset-entity {}))
|
||||
|
||||
@@ -26,11 +26,12 @@
|
||||
(shui/button
|
||||
{:variant :default
|
||||
:class "text-1xl flex flex-1 w-full my-8"
|
||||
:on-click #(shui/dialog-open! login/page-impl
|
||||
{:close-btn? false
|
||||
:label "user-login"
|
||||
:align :top
|
||||
:content-props {:class "app-login-modal"}})}
|
||||
:on-click #(shui/popup-show!
|
||||
nil
|
||||
(fn []
|
||||
[:div.w-full.h-full
|
||||
(login/page-impl)])
|
||||
{:id :login})}
|
||||
"Login")
|
||||
;; Logged in: account cell
|
||||
[:div.mobile-setting-item
|
||||
|
||||
@@ -11,13 +11,6 @@
|
||||
(defonce transition-group (r/adapt-class TransitionGroup))
|
||||
(defonce css-transition (r/adapt-class CSSTransition))
|
||||
|
||||
(rum/defc classic-app-container-wrap
|
||||
[content]
|
||||
[:main#app-container-wrapper.ls-fold-button-on-right
|
||||
[:div#app-container.pt-2
|
||||
[:div#main-container.flex.flex-1.overflow-x-hidden
|
||||
[:div.w-full content]]]])
|
||||
|
||||
(rum/defc notification-clear-all
|
||||
[]
|
||||
[:div.ui__notifications-content
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
|
||||
(defn set-router!
|
||||
[]
|
||||
(mobile-nav/install-navigation-hooks!)
|
||||
(let [router (rf/router routes nil)]
|
||||
(rfe/start!
|
||||
router
|
||||
@@ -75,7 +76,9 @@
|
||||
{:route {:to route-name
|
||||
:path-params (:path-params route)
|
||||
:query-params (:query-params route)}
|
||||
:path path})
|
||||
:route-match route
|
||||
:path path
|
||||
:stack (mobile-nav/current-stack)})
|
||||
(route-handler/set-route-match! route)))
|
||||
|
||||
;; set to false to enable HistoryAPI
|
||||
@@ -87,6 +90,7 @@
|
||||
;; so it is available even in :advanced release builds
|
||||
(prn "[Mobile] init!")
|
||||
(log/add-handler mobile-state/log-append!)
|
||||
(mobile-nav/install-native-bridge!)
|
||||
(set-router!)
|
||||
(init/init!)
|
||||
(fhandler/start! render!))
|
||||
|
||||
@@ -22,6 +22,21 @@
|
||||
(def *last-shared-url (atom nil))
|
||||
(def *last-shared-seconds (atom 0))
|
||||
|
||||
(defn- handle-incoming-url!
|
||||
[url]
|
||||
(p/then
|
||||
state/app-ready-promise
|
||||
(fn []
|
||||
(when (and url
|
||||
(or
|
||||
(string/starts-with? url "https://logseq.com/mobile/")
|
||||
(string/starts-with? url "logseq://mobile/")
|
||||
(not (and (= @*last-shared-url url)
|
||||
(<= (- (.getSeconds (js/Date.)) @*last-shared-seconds) 1)))))
|
||||
(reset! *last-shared-url url)
|
||||
(reset! *last-shared-seconds (.getSeconds (js/Date.)))
|
||||
(deeplink/deeplink url)))))
|
||||
|
||||
(defn- ios-init!
|
||||
"Initialize iOS-specified event listeners"
|
||||
[]
|
||||
@@ -80,18 +95,14 @@
|
||||
(.addListener App "appUrlOpen"
|
||||
(fn [^js data]
|
||||
(log/info ::app-url-open data)
|
||||
(p/then
|
||||
state/app-ready-promise
|
||||
(fn []
|
||||
(when-let [url (.-url data)]
|
||||
(when (or
|
||||
(string/starts-with? url "https://logseq.com/mobile/")
|
||||
(string/starts-with? url "logseq://mobile/")
|
||||
(not (and (= @*last-shared-url url)
|
||||
(<= (- (.getSeconds (js/Date.)) @*last-shared-seconds) 1))))
|
||||
(reset! *last-shared-url url)
|
||||
(reset! *last-shared-seconds (.getSeconds (js/Date.)))
|
||||
(deeplink/deeplink url)))))))
|
||||
(when-let [url (.-url data)]
|
||||
(handle-incoming-url! url))))
|
||||
|
||||
(-> (.getLaunchUrl App)
|
||||
(p/then (fn [^js data]
|
||||
(when-let [url (.-url data)]
|
||||
(log/info ::launch-url data)
|
||||
(handle-incoming-url! url)))))
|
||||
|
||||
(.addListener Keyboard "keyboardWillShow"
|
||||
(fn [^js info]
|
||||
|
||||
@@ -4,13 +4,30 @@
|
||||
[frontend.handler.route :as route-handler]
|
||||
[frontend.mobile.util :as mobile-util]
|
||||
[lambdaisland.glogi :as log]
|
||||
[promesa.core :as p]))
|
||||
[promesa.core :as p]
|
||||
[reitit.frontend.easy :as rfe]))
|
||||
|
||||
;; Each tab owns a navigation stack
|
||||
(defonce navigation-source (atom nil))
|
||||
(defonce ^:private initialised? (atom false))
|
||||
(defonce ^:private initialised-stacks (atom {}))
|
||||
(def ^:private primary-stack "home")
|
||||
(defonce ^:private active-stack (atom primary-stack))
|
||||
(defonce ^:private stack-history (atom {}))
|
||||
(defonce ^:private pending-navigation (atom nil))
|
||||
(defonce ^:private hooks-installed? (atom false))
|
||||
|
||||
;; Track whether the latest change came from a native back gesture / popstate.
|
||||
(.addEventListener js/window "popstate" (fn [_] (reset! navigation-source :pop)))
|
||||
(.addEventListener js/window "popstate" (fn [_]
|
||||
(reset! navigation-source :pop)))
|
||||
|
||||
(defn current-stack
|
||||
[]
|
||||
@active-stack)
|
||||
|
||||
(defn set-current-stack!
|
||||
[stack]
|
||||
(when (some? stack)
|
||||
(reset! active-stack stack)))
|
||||
|
||||
(defn- strip-fragment [href]
|
||||
(when (string? href)
|
||||
@@ -18,14 +35,129 @@
|
||||
(string/replace-first #"^#/" "/")
|
||||
(string/replace-first #"^#" ""))))
|
||||
|
||||
(defn- navigation-type [push?]
|
||||
(let [src @navigation-source]
|
||||
(defn- current-path
|
||||
[]
|
||||
(let [p (strip-fragment (.-hash js/location))]
|
||||
(if (string/blank? p) "/" p)))
|
||||
|
||||
(defn- stack-defaults
|
||||
[stack]
|
||||
(let [name (keyword stack)
|
||||
path (if (= stack primary-stack) "/" (str "/__stack__/" stack))]
|
||||
{:path path
|
||||
:route (when (= stack primary-stack)
|
||||
{:to :home
|
||||
:path-params {}
|
||||
:query-params {}})
|
||||
:route-match {:data {:name (if (= stack primary-stack) :home name)}
|
||||
:parameters {:path {} :query {}}}}))
|
||||
|
||||
(defn- record-navigation-intent!
|
||||
[{:keys [type stack]}]
|
||||
(let [stack (or stack @active-stack primary-stack)]
|
||||
(reset! pending-navigation {:type type
|
||||
:stack stack})))
|
||||
|
||||
(defonce orig-push-state rfe/push-state)
|
||||
(defn push-state
|
||||
"Sets the new route, leaving previous route in history."
|
||||
([k]
|
||||
(push-state k nil nil))
|
||||
([k params]
|
||||
(push-state k params nil))
|
||||
([k params query]
|
||||
(record-navigation-intent! {:type :push
|
||||
:stack @active-stack})
|
||||
(orig-push-state k params query)))
|
||||
|
||||
(defonce orig-replace-state rfe/replace-state)
|
||||
(defn- replace-state
|
||||
([k]
|
||||
(replace-state k nil nil))
|
||||
([k params]
|
||||
(replace-state k params nil))
|
||||
([k params query]
|
||||
(record-navigation-intent! {:type :replace
|
||||
:stack @active-stack})
|
||||
(orig-replace-state k params query)))
|
||||
|
||||
(defn install-navigation-hooks!
|
||||
"Wrap reitit navigation helpers so we know whether a change was push or replace.
|
||||
Also tags navigation with the active stack so native can keep per-stack history."
|
||||
[]
|
||||
(when (compare-and-set! hooks-installed? false true)
|
||||
(set! rfe/push-state push-state)
|
||||
(set! rfe/replace-state replace-state)))
|
||||
|
||||
(defn- consume-navigation-intent!
|
||||
[]
|
||||
(let [intent @pending-navigation]
|
||||
(reset! pending-navigation nil)
|
||||
intent))
|
||||
|
||||
(defn- ensure-stack
|
||||
[stack]
|
||||
(swap! stack-history #(if (contains? % stack)
|
||||
%
|
||||
(assoc % stack {:history [(stack-defaults stack)]})))
|
||||
stack)
|
||||
|
||||
(defn- stack-top
|
||||
[stack]
|
||||
(-> @stack-history (get stack) :history last))
|
||||
|
||||
(defn- remember-route!
|
||||
[stack nav-type route path route-match]
|
||||
(when stack
|
||||
(let [stack (ensure-stack stack)
|
||||
path (or path (current-path))
|
||||
entry (when path {:path path :route route :route-match route-match})
|
||||
update-history
|
||||
(fn [history]
|
||||
(let [history (vec history)
|
||||
last-path (:path (last history))]
|
||||
(case nav-type
|
||||
"pop" (if (> (count history) 1) (vec (butlast history)) history)
|
||||
"replace" (if (seq history)
|
||||
(conj (vec (butlast history)) entry)
|
||||
[entry])
|
||||
"push" (if (= last-path (:path entry))
|
||||
(conj (vec (butlast history)) entry)
|
||||
(conj history entry))
|
||||
history)))]
|
||||
(when entry
|
||||
(swap! stack-history update stack (fn [{:keys [history]}]
|
||||
{:history (update-history history)}))
|
||||
(swap! initialised-stacks assoc stack true)))))
|
||||
|
||||
(defn reset-stack-history!
|
||||
[stack]
|
||||
(when stack
|
||||
(swap! stack-history assoc stack {:history [(stack-defaults stack)]})
|
||||
(swap! initialised-stacks dissoc stack)))
|
||||
|
||||
(defn- next-navigation!
|
||||
[{:keys [push stack nav-type]}]
|
||||
(let [src @navigation-source
|
||||
intent (consume-navigation-intent!)
|
||||
stack (or stack (:stack intent) @active-stack primary-stack)
|
||||
first? (not (get @initialised-stacks stack))
|
||||
nav-type (or nav-type
|
||||
(cond
|
||||
(= src :pop) "pop"
|
||||
(false? push) "replace"
|
||||
(= (:type intent) :replace) "replace"
|
||||
first? "replace"
|
||||
(= (:type intent) :push) "push"
|
||||
(true? push) "push"
|
||||
:else "push"))]
|
||||
(reset! navigation-source nil)
|
||||
(cond
|
||||
(= src :pop) "pop"
|
||||
(false? push?) "replace"
|
||||
(compare-and-set! initialised? false true) "replace" ;; first load
|
||||
:else "push")))
|
||||
|
||||
(when first?
|
||||
(swap! initialised-stacks assoc stack true))
|
||||
{:navigation-type nav-type
|
||||
:push? (= nav-type "push")
|
||||
:stack stack}))
|
||||
|
||||
(defn- notify-route-payload!
|
||||
[payload]
|
||||
@@ -36,23 +168,87 @@
|
||||
:payload payload})))))
|
||||
|
||||
(defn notify-route-change!
|
||||
"Inform native iOS layer of a route change to keep native stack in sync.
|
||||
{route {to keyword, path-params map, query-params map}
|
||||
path string ;; optional, e.g. \"/page/Today\"
|
||||
push boolean? ;; optional, explicit push vs replace hint}"
|
||||
[{:keys [route path push]}]
|
||||
(when (and (mobile-util/native-ios?)
|
||||
mobile-util/ui-local)
|
||||
(let [nav-type (navigation-type push)
|
||||
payload (cond-> {:navigationType nav-type
|
||||
:push (not= nav-type "replace")}
|
||||
route (assoc :route route)
|
||||
(or path (.-hash js/location))
|
||||
(assoc :path (strip-fragment (or path (.-hash js/location)))))]
|
||||
(notify-route-payload! payload))))
|
||||
"Inform native iOS layer of a route change to keep native stack in sync."
|
||||
[{:keys [route route-match path push stack]}]
|
||||
(let [{:keys [navigation-type push? stack]} (next-navigation! {:push push
|
||||
:nav-type (:navigation-type route-match)
|
||||
:stack (or stack (current-stack))})
|
||||
stack (or stack (current-stack))
|
||||
path (or path (current-path))]
|
||||
(set-current-stack! stack)
|
||||
(remember-route! stack navigation-type route path route-match)
|
||||
(when (and (mobile-util/native-ios?)
|
||||
mobile-util/ui-local)
|
||||
(let [payload (cond-> {:navigationType navigation-type
|
||||
:push push?
|
||||
:stack stack}
|
||||
route (assoc :route route)
|
||||
path (assoc :path (strip-fragment path)))]
|
||||
(notify-route-payload! payload)))))
|
||||
|
||||
(defn reset-route!
|
||||
(comment
|
||||
(defn reset-route!
|
||||
[]
|
||||
(route-handler/redirect-to-home! false)
|
||||
(let [stack (current-stack)]
|
||||
(reset-stack-history! stack)
|
||||
(notify-route-payload!
|
||||
{:navigationType "reset"
|
||||
:push false
|
||||
:stack stack}))))
|
||||
|
||||
(defn switch-stack!
|
||||
"Activate a stack and restore its last known route."
|
||||
[stack]
|
||||
(when stack
|
||||
(let [stack (ensure-stack stack)
|
||||
current @active-stack]
|
||||
(set-current-stack! stack)
|
||||
(when-let [{:keys [path route route-match]} (stack-top stack)]
|
||||
(let [route-match (or route-match (:route-match (stack-defaults stack)))
|
||||
path (or path (current-path))]
|
||||
(route-handler/set-route-match! route-match)
|
||||
(when (= current "search")
|
||||
;; reset to :home
|
||||
(orig-replace-state :home nil nil))
|
||||
(notify-route-change!
|
||||
{:route {:to (or (get-in route [:data :name])
|
||||
(get-in route-match [:data :name]))
|
||||
:path-params (or (:path-params route)
|
||||
(get-in route-match [:parameters :path]))
|
||||
:query-params (or (:query-params route)
|
||||
(get-in route-match [:parameters :query]))}
|
||||
:path path
|
||||
:stack stack
|
||||
:push false}))))))
|
||||
|
||||
(defn pop-stack!
|
||||
"Pop one route from the current stack, update router via replace-state.
|
||||
Called when native UINavigationController pops (back gesture / back button)."
|
||||
[]
|
||||
(route-handler/redirect-to-home! false)
|
||||
(notify-route-payload!
|
||||
{:navigationType "reset"}))
|
||||
(let [stack (current-stack)
|
||||
{:keys [history]} (get @stack-history stack)
|
||||
history (vec history)]
|
||||
(when (> (count history) 1)
|
||||
(let [new-history (subvec history 0 (dec (count history)))
|
||||
{:keys [route-match]} (peek new-history)
|
||||
route-match (or route-match (:route-match (stack-defaults stack)))
|
||||
route-name (get-in route-match [:data :name])
|
||||
path-params (get-in route-match [:parameters :path])
|
||||
query-params (get-in route-match [:parameters :query])]
|
||||
|
||||
(swap! stack-history assoc stack {:history new-history})
|
||||
|
||||
;; Pretend this came from a pop for next-navigation!
|
||||
(reset! navigation-source :pop)
|
||||
|
||||
;; Use *original* replace-state to avoid recording a :replace intent.
|
||||
(orig-replace-state route-name path-params query-params)
|
||||
|
||||
(route-handler/set-route-match! route-match)))))
|
||||
|
||||
(defn ^:export install-native-bridge!
|
||||
[]
|
||||
(set! (.-LogseqNative js/window)
|
||||
(clj->js
|
||||
{:onNativePop (fn [] (pop-stack!))})))
|
||||
|
||||
@@ -19,5 +19,4 @@
|
||||
["/export"
|
||||
{:name :export
|
||||
:view (fn []
|
||||
[:div.mt-8
|
||||
(export/export)])}]])
|
||||
(export/export))}]])
|
||||
|
||||
@@ -1,12 +1,9 @@
|
||||
(ns mobile.state
|
||||
"Mobile state"
|
||||
(:require [frontend.rum :as r]
|
||||
[frontend.state :as state]))
|
||||
[frontend.state :as state]
|
||||
[mobile.navigation :as mobile-nav]))
|
||||
|
||||
(defonce *tab (atom "home"))
|
||||
(defn set-tab! [tab]
|
||||
(reset! *tab tab))
|
||||
(defn use-tab [] (r/use-atom *tab))
|
||||
(defonce *search-input (atom ""))
|
||||
(defn use-search-input []
|
||||
(r/use-atom *search-input))
|
||||
@@ -14,6 +11,18 @@
|
||||
(defn use-search-last-input-at []
|
||||
(r/use-atom *search-last-input-at))
|
||||
|
||||
(defonce *tab (atom "home"))
|
||||
(defn set-tab! [tab]
|
||||
(let [prev @*tab]
|
||||
;; When leaving the search tab, clear its stack so reopening starts fresh.
|
||||
(when (and (= prev "search")
|
||||
(not= tab "search"))
|
||||
(reset! *search-input "")
|
||||
(mobile-nav/reset-stack-history! "search"))
|
||||
(reset! *tab tab)
|
||||
(mobile-nav/switch-stack! tab)))
|
||||
(defn use-tab [] (r/use-atom *tab))
|
||||
|
||||
(defonce *popup-data (atom nil))
|
||||
(defn set-popup!
|
||||
[data]
|
||||
|
||||
Reference in New Issue
Block a user