mirror of
https://github.com/logseq/logseq.git
synced 2026-04-24 14:14:55 +00:00
enhance: native selection bar
This commit is contained in:
@@ -42,6 +42,7 @@
|
||||
D3D62A0A275C92880003FBDC /* FileContainer.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A09275C92880003FBDC /* FileContainer.swift */; };
|
||||
D3D62A0C275C928F0003FBDC /* FileContainer.m in Sources */ = {isa = PBXBuildFile; fileRef = D3D62A0B275C928F0003FBDC /* FileContainer.m */; };
|
||||
D3F4A5B62F1234567890ABCE /* NativeBottomSheetPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F4A5B62F1234567890ABCD /* NativeBottomSheetPlugin.swift */; };
|
||||
D3F4A5B62F1234567890ABD1 /* NativeSelectionActionBarPlugin.swift in Sources */ = {isa = PBXBuildFile; fileRef = D3F4A5B62F1234567890ABCF /* NativeSelectionActionBarPlugin.swift */; };
|
||||
FE647FF427BDFEDE00F3206B /* FsWatcher.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF327BDFEDE00F3206B /* FsWatcher.swift */; };
|
||||
FE647FF627BDFEF500F3206B /* FsWatcher.m in Sources */ = {isa = PBXBuildFile; fileRef = FE647FF527BDFEF500F3206B /* FsWatcher.m */; };
|
||||
FE96D6102A1B811A001ECE32 /* SharedData.swift in Sources */ = {isa = PBXBuildFile; fileRef = FE96D60F2A1B811A001ECE32 /* SharedData.swift */; };
|
||||
@@ -123,6 +124,7 @@
|
||||
D3D62A09275C92880003FBDC /* FileContainer.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = FileContainer.swift; sourceTree = "<group>"; };
|
||||
D3D62A0B275C928F0003FBDC /* FileContainer.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; path = FileContainer.m; sourceTree = "<group>"; };
|
||||
D3F4A5B62F1234567890ABCD /* NativeBottomSheetPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeBottomSheetPlugin.swift; sourceTree = "<group>"; };
|
||||
D3F4A5B62F1234567890ABCF /* NativeSelectionActionBarPlugin.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NativeSelectionActionBarPlugin.swift; sourceTree = "<group>"; };
|
||||
DE5650F4AD4E2242AB9C012D /* Pods-Logseq.debug.xcconfig */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = text.xcconfig; name = "Pods-Logseq.debug.xcconfig"; path = "Target Support Files/Pods-Logseq/Pods-Logseq.debug.xcconfig"; sourceTree = "<group>"; };
|
||||
FE647FF327BDFEDE00F3206B /* FsWatcher.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FsWatcher.swift; sourceTree = "<group>"; };
|
||||
FE647FF527BDFEF500F3206B /* FsWatcher.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = FsWatcher.m; sourceTree = "<group>"; };
|
||||
@@ -211,6 +213,7 @@
|
||||
CBF2D2D92DE83CB0006338BE /* UILocalPlugin.swift */,
|
||||
ABCDEF0123456789000000AA /* NativeTopBarPlugin.swift */,
|
||||
D3F4A5B62F1234567890ABCD /* NativeBottomSheetPlugin.swift */,
|
||||
D3F4A5B62F1234567890ABCF /* NativeSelectionActionBarPlugin.swift */,
|
||||
5FF86329283B5ADB0047731B /* Utils.swift */,
|
||||
5FF8632B283B5BFD0047731B /* Utils.m */,
|
||||
D32752BF2754C5AB0039291C /* AppDebug.entitlements */,
|
||||
@@ -463,6 +466,7 @@
|
||||
CBF2D2DA2DE83CB8006338BE /* UILocalPlugin.swift in Sources */,
|
||||
ABCDEF0123456789000000AB /* NativeTopBarPlugin.swift in Sources */,
|
||||
D3F4A5B62F1234567890ABCE /* NativeBottomSheetPlugin.swift in Sources */,
|
||||
D3F4A5B62F1234567890ABD1 /* NativeSelectionActionBarPlugin.swift in Sources */,
|
||||
A1B2C3D41E2F3A4B5C6D7E90 /* NativePageViewController.swift in Sources */,
|
||||
D3C620AA2ED4B9A80009CCDA /* Theme.swift in Sources */,
|
||||
A1B2C3D41E2F3A4B5C6D7E92 /* SharedWebViewController.swift in Sources */,
|
||||
|
||||
@@ -15,6 +15,7 @@ import UIKit
|
||||
bridge?.registerPluginInstance(NativeTopBarPlugin())
|
||||
bridge?.registerPluginInstance(LiquidTabsPlugin())
|
||||
bridge?.registerPluginInstance(NativeBottomSheetPlugin())
|
||||
bridge?.registerPluginInstance(NativeSelectionActionBarPlugin())
|
||||
}
|
||||
|
||||
public override func viewDidLoad() {
|
||||
|
||||
329
ios/App/App/NativeSelectionActionBarPlugin.swift
Normal file
329
ios/App/App/NativeSelectionActionBarPlugin.swift
Normal file
@@ -0,0 +1,329 @@
|
||||
import Capacitor
|
||||
import UIKit
|
||||
|
||||
// MARK: - Model for a single selection action coming from JS
|
||||
|
||||
private struct NativeSelectionAction {
|
||||
let id: String
|
||||
let title: String
|
||||
let systemIcon: String?
|
||||
|
||||
init?(jsObject: JSObject) {
|
||||
guard let id = jsObject["id"] as? String,
|
||||
let title = jsObject["title"] as? String else { return nil }
|
||||
self.id = id
|
||||
self.title = title
|
||||
self.systemIcon = jsObject["systemIcon"] as? String
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Native selection action bar UI
|
||||
|
||||
private class NativeSelectionActionBarView: UIView {
|
||||
/// Callback when an action is tapped. Sends the action id.
|
||||
var onActionTapped: ((String) -> Void)?
|
||||
|
||||
/// Blurred background container.
|
||||
private let blurView: UIVisualEffectView = {
|
||||
let view = UIVisualEffectView(effect: UIBlurEffect(style: .systemMaterial))
|
||||
view.translatesAutoresizingMaskIntoConstraints = false
|
||||
view.layer.cornerRadius = 16
|
||||
view.clipsToBounds = true
|
||||
view.isUserInteractionEnabled = true // ensure the blur container receives touch events
|
||||
return view
|
||||
}()
|
||||
|
||||
/// Horizontal stack that holds all action buttons.
|
||||
private let stackView: UIStackView = {
|
||||
let stack = UIStackView()
|
||||
stack.axis = .horizontal
|
||||
stack.alignment = .center
|
||||
stack.distribution = .fillEqually
|
||||
stack.spacing = 8
|
||||
stack.isLayoutMarginsRelativeArrangement = true
|
||||
stack.layoutMargins = UIEdgeInsets(top: 12, left: 12, bottom: 12, right: 12)
|
||||
stack.translatesAutoresizingMaskIntoConstraints = false
|
||||
stack.isUserInteractionEnabled = true // stack should also pass touches to its subviews
|
||||
return stack
|
||||
}()
|
||||
|
||||
// MARK: - Init
|
||||
|
||||
override init(frame: CGRect) {
|
||||
super.init(frame: frame)
|
||||
setupView()
|
||||
}
|
||||
|
||||
required init?(coder: NSCoder) {
|
||||
super.init(coder: coder)
|
||||
setupView()
|
||||
}
|
||||
|
||||
// MARK: - Public API
|
||||
|
||||
/// Present the bar on top of a host view with given actions and colors.
|
||||
func present(on host: UIView,
|
||||
actions: [NativeSelectionAction],
|
||||
tintColor: UIColor?,
|
||||
backgroundColor: UIColor?) {
|
||||
configure(actions: actions, tintColor: tintColor, backgroundColor: backgroundColor)
|
||||
attachIfNeeded(to: host)
|
||||
animateInIfNeeded()
|
||||
}
|
||||
|
||||
/// Dismiss with a small fade/transform animation.
|
||||
func dismiss() {
|
||||
UIView.animate(withDuration: 0.15,
|
||||
delay: 0,
|
||||
options: [.curveEaseIn],
|
||||
animations: {
|
||||
self.alpha = 0
|
||||
// Use a small translation for a subtle dismiss effect.
|
||||
self.transform = CGAffineTransform(translationX: 0, y: 8)
|
||||
}, completion: { _ in
|
||||
self.removeFromSuperview()
|
||||
self.transform = .identity
|
||||
self.alpha = 1
|
||||
})
|
||||
}
|
||||
|
||||
// MARK: - Private helpers
|
||||
|
||||
/// Base visual setup: background, shadow, subview hierarchy and constraints.
|
||||
private func setupView() {
|
||||
backgroundColor = .clear
|
||||
isUserInteractionEnabled = true // container must be interactive
|
||||
|
||||
// Shadow that appears around the blurred background.
|
||||
layer.cornerRadius = 16
|
||||
layer.masksToBounds = false
|
||||
layer.shadowColor = UIColor.black.cgColor
|
||||
layer.shadowOpacity = 0.12
|
||||
layer.shadowOffset = CGSize(width: 0, height: 6)
|
||||
layer.shadowRadius = 14
|
||||
|
||||
addSubview(blurView)
|
||||
NSLayoutConstraint.activate([
|
||||
blurView.leadingAnchor.constraint(equalTo: leadingAnchor),
|
||||
blurView.trailingAnchor.constraint(equalTo: trailingAnchor),
|
||||
blurView.topAnchor.constraint(equalTo: topAnchor),
|
||||
blurView.bottomAnchor.constraint(equalTo: bottomAnchor)
|
||||
])
|
||||
|
||||
blurView.contentView.addSubview(stackView)
|
||||
NSLayoutConstraint.activate([
|
||||
stackView.leadingAnchor.constraint(equalTo: blurView.contentView.leadingAnchor),
|
||||
stackView.trailingAnchor.constraint(equalTo: blurView.contentView.trailingAnchor),
|
||||
stackView.topAnchor.constraint(equalTo: blurView.contentView.topAnchor),
|
||||
stackView.bottomAnchor.constraint(equalTo: blurView.contentView.bottomAnchor)
|
||||
])
|
||||
}
|
||||
|
||||
/// Rebuilds the stack buttons for the current set of actions.
|
||||
private func configure(actions: [NativeSelectionAction],
|
||||
tintColor: UIColor?,
|
||||
backgroundColor: UIColor?) {
|
||||
// Remove old buttons.
|
||||
stackView.arrangedSubviews.forEach { $0.removeFromSuperview() }
|
||||
|
||||
let tint = tintColor ?? .label
|
||||
// Background color behind the blur. This helps match the Logseq background.
|
||||
blurView.backgroundColor = backgroundColor ?? UIColor.logseqBackground.withAlphaComponent(0.94)
|
||||
|
||||
actions.forEach { action in
|
||||
let button = makeButton(for: action, tintColor: tint)
|
||||
stackView.addArrangedSubview(button)
|
||||
}
|
||||
}
|
||||
|
||||
/// Attaches the bar to the given host view, pinned to the bottom with safe area.
|
||||
private func attachIfNeeded(to host: UIView) {
|
||||
guard superview !== host else { return }
|
||||
removeFromSuperview()
|
||||
|
||||
host.addSubview(self)
|
||||
host.bringSubviewToFront(self) // ensure this bar is above other subviews (e.g. WKWebView)
|
||||
translatesAutoresizingMaskIntoConstraints = false
|
||||
|
||||
NSLayoutConstraint.activate([
|
||||
leadingAnchor.constraint(equalTo: host.leadingAnchor, constant: 12),
|
||||
trailingAnchor.constraint(equalTo: host.trailingAnchor, constant: -12),
|
||||
bottomAnchor.constraint(equalTo: host.safeAreaLayoutGuide.bottomAnchor, constant: -12)
|
||||
])
|
||||
}
|
||||
|
||||
/// Simple fade-in animation when the bar appears.
|
||||
private func animateInIfNeeded() {
|
||||
// Only animate if we're currently visible and not already animated.
|
||||
guard alpha == 1 else { return }
|
||||
|
||||
alpha = 0
|
||||
transform = CGAffineTransform(translationX: 0, y: 8)
|
||||
UIView.animate(withDuration: 0.2,
|
||||
delay: 0,
|
||||
options: [.curveEaseOut, .allowUserInteraction],
|
||||
animations: {
|
||||
self.alpha = 1
|
||||
self.transform = .identity
|
||||
})
|
||||
}
|
||||
|
||||
/// Creates a single button for an action (icon + label in a vertical stack).
|
||||
private func makeButton(for action: NativeSelectionAction, tintColor: UIColor) -> UIControl {
|
||||
let control = UIControl()
|
||||
control.accessibilityIdentifier = action.id
|
||||
control.translatesAutoresizingMaskIntoConstraints = false
|
||||
control.isUserInteractionEnabled = true
|
||||
|
||||
// Icon
|
||||
let iconView = UIImageView()
|
||||
iconView.contentMode = .scaleAspectFit
|
||||
iconView.tintColor = tintColor
|
||||
iconView.image = UIImage(systemName: action.systemIcon ?? "circle")
|
||||
iconView.translatesAutoresizingMaskIntoConstraints = false
|
||||
iconView.heightAnchor.constraint(equalToConstant: 22).isActive = true
|
||||
iconView.widthAnchor.constraint(equalToConstant: 22).isActive = true
|
||||
|
||||
// Title label
|
||||
let label = UILabel()
|
||||
label.text = action.title
|
||||
label.textAlignment = .center
|
||||
label.font = UIFont.systemFont(ofSize: 12, weight: .semibold)
|
||||
label.textColor = tintColor
|
||||
label.numberOfLines = 1
|
||||
|
||||
// Vertical stack containing icon + label.
|
||||
let column = UIStackView(arrangedSubviews: [iconView, label])
|
||||
column.axis = .vertical
|
||||
column.alignment = .center
|
||||
column.spacing = 6
|
||||
column.translatesAutoresizingMaskIntoConstraints = false
|
||||
column.isUserInteractionEnabled = false // let the UIControl handle touches instead of the stack
|
||||
|
||||
control.addSubview(column)
|
||||
NSLayoutConstraint.activate([
|
||||
column.leadingAnchor.constraint(equalTo: control.leadingAnchor),
|
||||
column.trailingAnchor.constraint(equalTo: control.trailingAnchor),
|
||||
column.topAnchor.constraint(equalTo: control.topAnchor, constant: 4),
|
||||
column.bottomAnchor.constraint(equalTo: control.bottomAnchor, constant: -4)
|
||||
])
|
||||
|
||||
// Add targets for tap handling.
|
||||
control.addTarget(self, action: #selector(handleTap(_:)), for: .touchUpInside)
|
||||
|
||||
return control
|
||||
}
|
||||
|
||||
// MARK: - Touch handling
|
||||
|
||||
/// Called on touchUpInside, triggers the callback with the action id.
|
||||
@objc private func handleTap(_ sender: UIControl) {
|
||||
guard let id = sender.accessibilityIdentifier else { return }
|
||||
onActionTapped?(id)
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Capacitor plugin
|
||||
|
||||
@objc(NativeSelectionActionBarPlugin)
|
||||
public class NativeSelectionActionBarPlugin: CAPPlugin, CAPBridgedPlugin {
|
||||
public let identifier = "NativeSelectionActionBarPlugin"
|
||||
public let jsName = "NativeSelectionActionBarPlugin"
|
||||
public let pluginMethods: [CAPPluginMethod] = [
|
||||
CAPPluginMethod(name: "present", returnType: CAPPluginReturnPromise),
|
||||
CAPPluginMethod(name: "dismiss", returnType: CAPPluginReturnPromise)
|
||||
]
|
||||
|
||||
private var actionBar: NativeSelectionActionBarView?
|
||||
|
||||
/// Called from JS to show/update the selection bar.
|
||||
@objc func present(_ call: CAPPluginCall) {
|
||||
let rawActions = call.getArray("actions", JSObject.self) ?? []
|
||||
let actions = rawActions.compactMap(NativeSelectionAction.init(jsObject:))
|
||||
let tintColor = call.getString("tintColor")?.toUIColor(defaultColor: .label)
|
||||
let backgroundColor = call.getString("backgroundColor")?.toUIColor(
|
||||
defaultColor: UIColor.logseqBackground.withAlphaComponent(0.94)
|
||||
)
|
||||
|
||||
DispatchQueue.main.async {
|
||||
guard let host = self.hostView() else {
|
||||
call.reject("Host view not found")
|
||||
return
|
||||
}
|
||||
|
||||
// If actions are empty, hide the bar instead.
|
||||
guard !actions.isEmpty else {
|
||||
self.actionBar?.dismiss()
|
||||
self.actionBar = nil
|
||||
call.resolve()
|
||||
return
|
||||
}
|
||||
|
||||
let bar = self.actionBar ?? NativeSelectionActionBarView()
|
||||
bar.onActionTapped = { [weak self] id in
|
||||
print("action id", id)
|
||||
self?.notifyListeners("action", data: ["id": id])
|
||||
}
|
||||
bar.present(on: host,
|
||||
actions: actions,
|
||||
tintColor: tintColor,
|
||||
backgroundColor: backgroundColor)
|
||||
self.actionBar = bar
|
||||
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
/// Called from JS to hide the selection bar.
|
||||
@objc func dismiss(_ call: CAPPluginCall) {
|
||||
DispatchQueue.main.async {
|
||||
self.actionBar?.dismiss()
|
||||
self.actionBar = nil
|
||||
call.resolve()
|
||||
}
|
||||
}
|
||||
|
||||
/// Attempts to find the appropriate host view to attach the bar to.
|
||||
private func hostView() -> UIView? {
|
||||
if let parent = bridge?.viewController?.parent?.view {
|
||||
return parent
|
||||
}
|
||||
return bridge?.viewController?.view
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Helpers
|
||||
|
||||
private extension String {
|
||||
/// Converts a hex color string (e.g. "#RRGGBB" or "#RRGGBBAA") to UIColor.
|
||||
func toUIColor(defaultColor: UIColor) -> UIColor {
|
||||
var hexString = self.trimmingCharacters(in: .whitespacesAndNewlines).uppercased()
|
||||
if hexString.hasPrefix("#") {
|
||||
hexString.removeFirst()
|
||||
}
|
||||
|
||||
var rgbValue: UInt64 = 0
|
||||
guard Scanner(string: hexString).scanHexInt64(&rgbValue) else {
|
||||
return defaultColor
|
||||
}
|
||||
|
||||
switch hexString.count {
|
||||
case 6: // RRGGBB
|
||||
return UIColor(
|
||||
red: CGFloat((rgbValue & 0xFF0000) >> 16) / 255.0,
|
||||
green: CGFloat((rgbValue & 0x00FF00) >> 8) / 255.0,
|
||||
blue: CGFloat(rgbValue & 0x0000FF) / 255.0,
|
||||
alpha: 1.0
|
||||
)
|
||||
case 8: // RRGGBBAA
|
||||
return UIColor(
|
||||
red: CGFloat((rgbValue & 0xFF000000) >> 24) / 255.0,
|
||||
green: CGFloat((rgbValue & 0x00FF0000) >> 16) / 255.0,
|
||||
blue: CGFloat((rgbValue & 0x0000FF00) >> 8) / 255.0,
|
||||
alpha: CGFloat(rgbValue & 0x000000FF) / 255.0
|
||||
)
|
||||
default:
|
||||
return defaultColor
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -25,10 +25,12 @@
|
||||
(defonce ui-local (registerPlugin "UILocal"))
|
||||
(defonce native-top-bar nil)
|
||||
(defonce native-bottom-sheet nil)
|
||||
(defonce native-selection-action-bar nil)
|
||||
(defonce ios-utils nil)
|
||||
(when (native-ios?)
|
||||
(set! native-top-bar (registerPlugin "NativeTopBarPlugin"))
|
||||
(set! native-bottom-sheet (registerPlugin "NativeBottomSheetPlugin"))
|
||||
(set! native-selection-action-bar (registerPlugin "NativeSelectionActionBarPlugin"))
|
||||
(set! ios-utils (registerPlugin "Utils")))
|
||||
|
||||
(defn hide-splash []
|
||||
|
||||
@@ -2,42 +2,101 @@
|
||||
"Selection action bar, activated when swipe on a block"
|
||||
(:require [frontend.db :as db]
|
||||
[frontend.handler.editor :as editor-handler]
|
||||
[frontend.mobile.util :as mobile-util]
|
||||
[frontend.state :as state]
|
||||
[frontend.ui :as ui]
|
||||
[frontend.util.url :as url-util]
|
||||
[logseq.shui.hooks :as hooks]
|
||||
[rum.core :as rum]))
|
||||
|
||||
(defn- action-command
|
||||
[icon description command-handler]
|
||||
(let [callback
|
||||
(fn []
|
||||
(state/set-state! :mobile/show-action-bar? false)
|
||||
(editor-handler/clear-selection!))]
|
||||
[:button.bottom-action.flex-row
|
||||
{:on-click (fn [_event]
|
||||
(command-handler)
|
||||
(callback))}
|
||||
(ui/icon icon {:style {:fontSize 23}})
|
||||
[:div.description description]]))
|
||||
(defn- dismiss-action-bar!
|
||||
[]
|
||||
(.dismiss ^js mobile-util/native-selection-action-bar))
|
||||
|
||||
(rum/defcs action-bar < rum/reactive
|
||||
[state]
|
||||
(let [blocks (->> (state/get-selection-block-ids)
|
||||
(keep (fn [id]
|
||||
(db/entity [:block/uuid id]))))
|
||||
block-ids (map :block/uuid blocks)]
|
||||
[:div.action-bar
|
||||
[:div.action-bar-commands
|
||||
(action-command "copy" "Copy" #(editor-handler/copy-selection-blocks false))
|
||||
(action-command "cut" "Delete" #(editor-handler/cut-selection-blocks false {:mobile-action-bar? true}))
|
||||
(action-command "registered" "Copy ref"
|
||||
(fn [_event] (editor-handler/copy-block-refs)))
|
||||
(action-command "link" "Copy url"
|
||||
(fn [_event] (let [current-repo (state/get-current-repo)
|
||||
tap-f (fn [block-id]
|
||||
(url-util/get-logseq-graph-uuid-url nil current-repo block-id))]
|
||||
(editor-handler/copy-block-ref! (first block-ids) tap-f))))
|
||||
(action-command "x" "Unselect"
|
||||
(fn [_event]
|
||||
(state/clear-selection!)
|
||||
(state/set-state! :mobile/show-action-bar? false)))]]))
|
||||
(defn close-selection-bar!
|
||||
[]
|
||||
(dismiss-action-bar!)
|
||||
(state/set-state! :mobile/show-action-bar? false)
|
||||
(editor-handler/clear-selection!))
|
||||
|
||||
(defn- selected-block-ids
|
||||
[]
|
||||
(->> (state/get-selection-block-ids)
|
||||
(keep (fn [id]
|
||||
(some-> (db/entity [:block/uuid id])
|
||||
:block/uuid)))))
|
||||
|
||||
(defn- selection-actions
|
||||
[]
|
||||
(let [close! close-selection-bar!]
|
||||
[{:id "copy"
|
||||
:label "Copy"
|
||||
:icon "copy"
|
||||
:system-icon "doc.on.doc"
|
||||
:handler (fn []
|
||||
(editor-handler/copy-selection-blocks false)
|
||||
(close!))}
|
||||
{:id "delete"
|
||||
:label "Delete"
|
||||
:icon "cut"
|
||||
:system-icon "trash"
|
||||
:handler (fn []
|
||||
(editor-handler/cut-selection-blocks false {:mobile-action-bar? true})
|
||||
(close!))}
|
||||
{:id "copy-ref"
|
||||
:label "Copy ref"
|
||||
:icon "registered"
|
||||
:system-icon "number.square"
|
||||
:handler (fn []
|
||||
(editor-handler/copy-block-refs)
|
||||
(close!))}
|
||||
{:id "copy-url"
|
||||
:label "Copy url"
|
||||
:icon "link"
|
||||
:system-icon "link"
|
||||
:handler (fn []
|
||||
(let [current-repo (state/get-current-repo)
|
||||
tap-f (fn [block-id]
|
||||
(url-util/get-logseq-graph-uuid-url nil current-repo block-id))]
|
||||
(when-let [block-id (first (selected-block-ids))]
|
||||
(editor-handler/copy-block-ref! block-id tap-f)))
|
||||
(close!))}
|
||||
{:id "unselect"
|
||||
:label "Unselect"
|
||||
:icon "x"
|
||||
:system-icon "xmark"
|
||||
:handler (fn []
|
||||
(state/clear-selection!)
|
||||
(close!))}]))
|
||||
|
||||
(rum/defc action-bar
|
||||
[]
|
||||
(let [actions (selection-actions)
|
||||
handlers-ref (hooks/use-ref nil)]
|
||||
(set! (.-current handlers-ref) (into {} (map (juxt :id :handler) actions)))
|
||||
|
||||
(hooks/use-effect!
|
||||
(fn []
|
||||
(when (and (mobile-util/native-ios?)
|
||||
mobile-util/native-selection-action-bar)
|
||||
(let [listener (.addListener ^js mobile-util/native-selection-action-bar
|
||||
"action"
|
||||
(fn [^js e]
|
||||
(when-let [id (.-id e)]
|
||||
(prn :debug :id id
|
||||
:handler (.-current handlers-ref))
|
||||
(when-let [handler (get (.-current handlers-ref) id)]
|
||||
(handler)))))
|
||||
actions' {:actions (map (fn [{:keys [id label system-icon]}]
|
||||
{:id id
|
||||
:title label
|
||||
:systemIcon system-icon})
|
||||
actions)}]
|
||||
(.present ^js mobile-util/native-selection-action-bar (clj->js actions'))
|
||||
(fn []
|
||||
(dismiss-action-bar!)
|
||||
(cond
|
||||
(and listener (.-remove listener)) ((.-remove listener))
|
||||
listener (.then listener (fn [^js handle] (.remove handle))))))))
|
||||
[])
|
||||
|
||||
[:<>]))
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
[lambdaisland.glogi :as log]
|
||||
[logseq.shui.ui :as shui]
|
||||
[mobile.components.app :as app]
|
||||
[mobile.components.selection-toolbar :as selection-toolbar]
|
||||
[mobile.events]
|
||||
[mobile.init :as init]
|
||||
[mobile.navigation :as mobile-nav]
|
||||
@@ -36,6 +37,7 @@
|
||||
(fn [route]
|
||||
(when (state/get-edit-block)
|
||||
(state/clear-edit!))
|
||||
(selection-toolbar/close-selection-bar!)
|
||||
(let [route-name (get-in route [:data :name])
|
||||
path (-> js/location .-hash (string/replace-first #"^#" ""))
|
||||
pop? (= :pop @mobile-nav/navigation-source)
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
(ns mobile.routes
|
||||
"Routes used in mobile app"
|
||||
(:require [frontend.components.page :as page]))
|
||||
|
||||
(def routes
|
||||
|
||||
Reference in New Issue
Block a user