Files
logseq/ios/App/App/NativeTopBarPlugin.swift
2025-12-04 23:53:00 +08:00

242 lines
8.7 KiB
Swift

import Capacitor
import UIKit
@objc(NativeTopBarPlugin)
public class NativeTopBarPlugin: CAPPlugin, CAPBridgedPlugin {
public let identifier = "NativeTopBarPlugin"
public let jsName = "NativeTopBarPlugin"
public let pluginMethods: [CAPPluginMethod] = [
CAPPluginMethod(name: "configure", returnType: CAPPluginReturnPromise)
]
private class NativeTopBarButton: UIButton {
var buttonId: String = ""
override var intrinsicContentSize: CGSize {
// Keep a consistent tap target; icon size is controlled via SF Symbol configuration
CGSize(width: 36, height: 32)
}
}
private func navigationController() -> UINavigationController? {
if let nav = bridge?.viewController?.parent?.navigationController {
return nav
}
if let nav = bridge?.viewController?.navigationController {
return nav
}
if let appDelegate = UIApplication.shared.delegate as? AppDelegate {
return appDelegate.navController
}
return nil
}
// MARK: - UITabBar lookup
/// Recursively search for a UITabBar in the given view hierarchy.
private func findTabBar(in view: UIView) -> UITabBar? {
if let tabBar = view as? UITabBar {
return tabBar
}
for subview in view.subviews {
if let found = findTabBar(in: subview) {
return found
}
}
return nil
}
// MARK: - Public API
@objc func configure(_ call: CAPPluginCall) {
let title = call.getString("title")
let leftButtons = call.getArray("leftButtons", JSObject.self) ?? []
let rightButtons = call.getArray("rightButtons", JSObject.self) ?? []
let backgroundColorHex = call.getString("backgroundColor")
let tintColorHex = call.getString("tintColor")
let hidden = call.getBool("hidden") ?? false
let titleClickable = call.getBool("titleClickable") ?? false
DispatchQueue.main.async { [weak self] in
guard let self = self else { return }
guard let nav = self.navigationController() else {
call.reject("Navigation controller not found")
return
}
nav.setNavigationBarHidden(hidden, animated: true)
guard !hidden else {
call.resolve()
return
}
// Resolve dynamic colors from JS
let resolvedBackgroundColor =
backgroundColorHex?.toUIColor(defaultColor: .logseqBackground) ?? .logseqBackground
let resolvedTintColor =
tintColorHex?.toUIColor(defaultColor: .label) ?? .label
// --- NAVIGATION BAR (top) ---
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground()
appearance.shadowColor = .clear
appearance.titleTextAttributes = [
.foregroundColor: resolvedTintColor
]
nav.navigationBar.standardAppearance = appearance
nav.navigationBar.scrollEdgeAppearance = appearance
nav.navigationBar.compactAppearance = appearance
nav.navigationBar.tintColor = resolvedTintColor
if let topVC = nav.topViewController {
// Reset any previous custom title view
topVC.navigationItem.titleView = nil
if titleClickable, let title {
let button = NativeTopBarButton(type: .system)
button.buttonId = "title"
button.setTitle(title, for: .normal)
button.titleLabel?.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
button.addTarget(self, action: #selector(self.titleTapped(_:)), for: .touchUpInside)
let baseColor = nav.navigationBar.tintColor ?? UIColor.label
// Use color with alpha instead of button.alpha
button.setTitleColor(baseColor.withAlphaComponent(0.8), for: .normal)
button.setTitleColor(baseColor.withAlphaComponent(1.0), for: .highlighted)
button.setTitleColor(baseColor.withAlphaComponent(0.6), for: .disabled)
button.backgroundColor = .clear
topVC.navigationItem.titleView = button
topVC.navigationItem.title = nil
} else {
let label = UILabel()
label.text = title
label.textAlignment = .center
label.font = UIFont.systemFont(ofSize: 17, weight: .semibold)
let baseColor = nav.navigationBar.tintColor
label.textColor = baseColor?.withAlphaComponent(0.8)
topVC.navigationItem.titleView = label
topVC.navigationItem.title = nil
}
topVC.navigationItem.leftBarButtonItems = self.buildButtons(from: leftButtons)
topVC.navigationItem.rightBarButtonItems = self.buildButtons(from: rightButtons)
}
call.resolve()
}
}
// MARK: - Button building
private func buildButtons(from array: [JSObject]) -> [UIBarButtonItem] {
return array.compactMap { obj in
guard let id = obj["id"] as? String else { return nil }
let systemIconName = (obj["systemIcon"] as? String) ?? "circle"
let button = NativeTopBarButton(type: .system)
button.buttonId = id
// Size: small / medium / large -> SF Symbol pointSize
let symbolConfig = symbolConfiguration(for: obj)
if let image = UIImage(systemName: systemIconName, withConfiguration: symbolConfig) {
button.setImage(image, for: .normal)
}
// Per-button color: prefers "tintColor", then "color"
button.tintColor = tintColor(for: obj)
button.imageView?.contentMode = .scaleAspectFit
button.addTarget(self, action: #selector(buttonTapped(_:)), for: .touchUpInside)
// Fixed tap target; icon itself is sized by SF Symbol config
let container = UIView(frame: CGRect(x: 0, y: 0, width: 36, height: 32))
button.frame = container.bounds
button.autoresizingMask = [.flexibleWidth, .flexibleHeight]
container.addSubview(button)
let item = UIBarButtonItem(customView: container)
// iOS 26+ Liquid Glass: remove the pill/glass background
if #available(iOS 26.0, *) {
button.alpha = 0.8
}
return item
}
}
private func tintColor(for obj: JSObject) -> UIColor {
if let hex = (obj["tintColor"] as? String) ?? (obj["color"] as? String) {
return hex.toUIColor(defaultColor: .label)
}
return .label
}
private func symbolConfiguration(for obj: JSObject) -> UIImage.SymbolConfiguration {
let sizeString = (obj["size"] as? String)?.lowercased()
let pointSize: CGFloat
switch sizeString {
case "small":
pointSize = 8
case "large":
pointSize = 19
default: // "medium" or nil
pointSize = 15
}
return UIImage.SymbolConfiguration(pointSize: pointSize, weight: .semibold)
}
// MARK: - Actions
@objc private func buttonTapped(_ sender: NativeTopBarButton) {
notifyListeners("buttonTapped", data: ["id": sender.buttonId])
}
@objc private func titleTapped(_ sender: NativeTopBarButton) {
notifyListeners("buttonTapped", data: ["id": "title"])
}
}
// MARK: - Color helpers
private extension String {
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
}
}
}